feat: Add payment partial support, comanda printing, and direct print configuration
- Implement partial payment flow with 'Pagamento Parcial' button - Calculate remaining value considering existing payments - Add payment tracking in comanda list and PDV modal - Add 'Imprimir' button for comanda receipt printing - Add print silent mode configuration - Fix product name display in print and list views - Add fallback when no printer is configured
This commit is contained in:
86
ELECTRON_INTEGRATION.md
Normal file
86
ELECTRON_INTEGRATION.md
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
# RRBEC Middleware - Guia de Integração (Electron/Desktop)
|
||||||
|
|
||||||
|
Este guia documenta como a aplicação Electron deve se comunicar com o servidor middleware local em Go.
|
||||||
|
|
||||||
|
## 1. Configurações Base
|
||||||
|
- **URL Base**: `http://localhost:8080/api/v1`
|
||||||
|
- **Porta Default**: `8080` (configurável no arquivo `.env` do servidor).
|
||||||
|
- **Formato**: Todas as requisições e respostas utilizam `application/json`.
|
||||||
|
|
||||||
|
## 2. Autenticação (SimpleAuth)
|
||||||
|
O servidor não utiliza tokens JWT complexos localmente. A autenticação funciona assim:
|
||||||
|
1. Faça login em `/login` enviando `username` e `password`.
|
||||||
|
2. O servidor retornará um objeto de usuário. Capture o valor do campo `id` (inteiro).
|
||||||
|
3. Envie esse valor no cabeçalho HTTP `X-User-ID` em todas as rotas marcadas como **[PROTEGIDO]**.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Endpoints da API
|
||||||
|
|
||||||
|
### [PÚBLICO] Login
|
||||||
|
**POST** `/login`
|
||||||
|
- **Body**: `{ "username": "seu_usuario", "password": "sua_senha" }`
|
||||||
|
- **Retorno**: Objeto User completo.
|
||||||
|
|
||||||
|
### [PÚBLICO] Listar Mesas
|
||||||
|
**GET** `/mesas`
|
||||||
|
- **Retorno**: Array de objetos mesas com `id`, `uuid`, `name`, `active`.
|
||||||
|
|
||||||
|
### [PÚBLICO] Listar Produtos/Estoque
|
||||||
|
**GET** `/products`
|
||||||
|
- **Retorno**: Array de produtos com preços e quantidade em estoque.
|
||||||
|
|
||||||
|
### [PÚBLICO] Listar Categorias
|
||||||
|
**GET** `/categories`
|
||||||
|
|
||||||
|
### [PÚBLICO] Listar Clientes
|
||||||
|
**GET** `/clients`
|
||||||
|
|
||||||
|
### [PÚBLICO] Listar Pedidos (Cozinha/Orders)
|
||||||
|
**GET** `/orders`
|
||||||
|
|
||||||
|
### [PÚBLICO] Listar Tipos de Pagamento
|
||||||
|
**GET** `/payment-types`
|
||||||
|
|
||||||
|
### [PÚBLICO] Listar Pagamentos Realizados
|
||||||
|
**GET** `/payments`
|
||||||
|
|
||||||
|
### [PÚBLICO] Ver Comanda por ID
|
||||||
|
**GET** `/comandas/:id` (Ex: `/api/v1/comandas/9`)
|
||||||
|
- **Retorno**: Detalhes da comanda.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Comandas e Itens (Ações)
|
||||||
|
|
||||||
|
### [PROTEGIDO] Abrir Nova Comanda
|
||||||
|
**POST** `/comandas`
|
||||||
|
- **Headers**: `X-User-ID: <id_do_usuario>`
|
||||||
|
- **Body**: `{ "mesa_id": 1, "client_id": null }`
|
||||||
|
|
||||||
|
### [PROTEGIDO] Lançar Pedido (Adicionar Item)
|
||||||
|
**POST** `/items-comanda`
|
||||||
|
- **Headers**: `X-User-ID: <id_do_usuario>`
|
||||||
|
- **Body**: `{ "comanda_id": 9, "product_id": 50, "applicant": "Nome do Garçom" }`
|
||||||
|
|
||||||
|
### [PROTEGIDO] Deletar Item Individual
|
||||||
|
**DELETE** `/items-comanda/:id`
|
||||||
|
- **Headers**: `X-User-ID: <id_do_usuario>`
|
||||||
|
|
||||||
|
### [PROTEGIDO] Limpar e Fechar Comanda (Apagar Inteira)
|
||||||
|
**POST** `/comandas/:id/apagar`
|
||||||
|
- **Headers**: `X-User-ID: <id_do_usuario>`
|
||||||
|
- **Efeito**: Remove todos os itens da comanda e muda o status para `CLOSED`.
|
||||||
|
|
||||||
|
### [PROTEGIDO] Pagar e Fechar Comanda
|
||||||
|
**POST** `/comandas/:id/pagar`
|
||||||
|
- **Headers**: `X-User-ID: <id_do_usuario>`
|
||||||
|
- **Body**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"value": 50.00,
|
||||||
|
"type_pay_id": 1,
|
||||||
|
"client_id": null
|
||||||
|
}
|
||||||
|
```
|
||||||
|
- **Efeito**: Registra o pagamento localmente e fecha a comanda.
|
||||||
183
src/main/main.js
183
src/main/main.js
@@ -6,7 +6,14 @@ const axios = require('axios');
|
|||||||
const store = new Store();
|
const store = new Store();
|
||||||
|
|
||||||
function getBaseUrl() {
|
function getBaseUrl() {
|
||||||
return store.get('api_url', 'http://localhost:8000/api/v1');
|
const currentUrl = store.get('api_url');
|
||||||
|
// Migração automática da URL antiga para o novo middleware local
|
||||||
|
if (!currentUrl || currentUrl.includes('squareweb.app')) {
|
||||||
|
const newUrl = 'http://localhost:8080/api/v1';
|
||||||
|
store.set('api_url', newUrl);
|
||||||
|
return newUrl;
|
||||||
|
}
|
||||||
|
return currentUrl;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── Janela Principal ────────────────────────────────────────────────────────
|
// ─── Janela Principal ────────────────────────────────────────────────────────
|
||||||
@@ -31,89 +38,45 @@ function createWindow() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
mainWindow.loadFile(path.join(__dirname, '../renderer/index.html'));
|
mainWindow.loadFile(path.join(__dirname, '../renderer/index.html'));
|
||||||
mainWindow.webContents.openDevTools();
|
// mainWindow.webContents.openDevTools();
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── Helpers de Token ────────────────────────────────────────────────────────
|
// ─── Helpers de Autenticação (Middleware Go) ──────────────────────────────────
|
||||||
let isRefreshing = false;
|
|
||||||
let refreshPromise = null;
|
|
||||||
|
|
||||||
function getHeaders() {
|
function getHeaders() {
|
||||||
const token = store.get('access_token');
|
const user = store.get('user');
|
||||||
// console.log('[MAIN] getHeaders - Token exists:', !!token);
|
return user && user.id ? { 'X-User-ID': String(user.id) } : {};
|
||||||
return token ? { Authorization: `Bearer ${token}` } : {};
|
|
||||||
}
|
|
||||||
|
|
||||||
async function refreshAccessToken() {
|
|
||||||
if (isRefreshing) {
|
|
||||||
console.log('[JWT] Refresh already in progress, waiting...');
|
|
||||||
return refreshPromise;
|
|
||||||
}
|
|
||||||
|
|
||||||
const refresh = store.get('refresh_token');
|
|
||||||
if (!refresh) {
|
|
||||||
console.error('[JWT] No refresh token available.');
|
|
||||||
throw new Error('No refresh token');
|
|
||||||
}
|
|
||||||
|
|
||||||
isRefreshing = true;
|
|
||||||
console.log('[JWT] Starting token refresh flow...');
|
|
||||||
|
|
||||||
refreshPromise = axios.post(`${getBaseUrl()}/token/refresh/`, { refresh })
|
|
||||||
.then(res => {
|
|
||||||
const { access, refresh: newRefresh } = res.data;
|
|
||||||
store.set('access_token', access);
|
|
||||||
if (newRefresh) {
|
|
||||||
store.set('refresh_token', newRefresh);
|
|
||||||
console.log('[JWT] Refresh token rotated and updated.');
|
|
||||||
}
|
|
||||||
console.log('[JWT] Access token updated successfully.');
|
|
||||||
return access;
|
|
||||||
})
|
|
||||||
.catch(err => {
|
|
||||||
console.error('[JWT] Refresh Failed:', err.response?.data || err.message);
|
|
||||||
// Limpa tudo se o refresh falhar (refresh_token expirou definitivamente)
|
|
||||||
store.delete('access_token');
|
|
||||||
store.delete('refresh_token');
|
|
||||||
store.delete('user');
|
|
||||||
throw err;
|
|
||||||
})
|
|
||||||
.finally(() => {
|
|
||||||
isRefreshing = false;
|
|
||||||
refreshPromise = null;
|
|
||||||
});
|
|
||||||
|
|
||||||
return refreshPromise;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function requestWithRetry(method, endpoint, data) {
|
async function requestWithRetry(method, endpoint, data) {
|
||||||
const url = `${getBaseUrl()}${endpoint}`;
|
const cleanEndpoint = endpoint.endsWith('/') ? endpoint.slice(0, -1) : endpoint;
|
||||||
console.log(`[API] ${method.toUpperCase()} ${url}`);
|
const url = `${getBaseUrl()}${cleanEndpoint}`;
|
||||||
|
const headers = {
|
||||||
|
...getHeaders(),
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
};
|
||||||
|
|
||||||
|
console.log(`[DEBUG_API] >>> ${method.toUpperCase()} ${url}`);
|
||||||
|
console.log(`[DEBUG_API] >>> Headers:`, JSON.stringify(headers));
|
||||||
|
if (data) console.log(`[DEBUG_API] >>> Body:`, JSON.stringify(data));
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const res = await axios({ method, url, data, headers: getHeaders() });
|
const res = await axios({
|
||||||
|
method,
|
||||||
|
url,
|
||||||
|
data,
|
||||||
|
headers
|
||||||
|
});
|
||||||
return { ok: true, data: res.data };
|
return { ok: true, data: res.data };
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
const status = err.response?.status;
|
const status = err.response?.status;
|
||||||
|
|
||||||
// Se for 401 ou 403, tentamos o refresh uma única vez
|
|
||||||
if ((status === 401 || status === 403) && store.get('refresh_token')) {
|
|
||||||
console.warn(`[API] ${status} Unauthorized/Forbidden on ${endpoint}. Attempting refresh...`);
|
|
||||||
try {
|
|
||||||
await refreshAccessToken();
|
|
||||||
// Tenta a requisição original novamente com o novo header
|
|
||||||
const retryRes = await axios({ method, url, data, headers: getHeaders() });
|
|
||||||
console.log(`[API] Retry successful for ${endpoint}`);
|
|
||||||
return { ok: true, data: retryRes.data };
|
|
||||||
} catch (refreshErr) {
|
|
||||||
console.error(`[API] Retry failed after refresh for ${endpoint}`);
|
|
||||||
if (mainWindow) mainWindow.webContents.send('auth:expired');
|
|
||||||
return { ok: false, error: 'Sessão expirada. Faça login novamente.', expired: true };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const msg = err.response?.data || err.message;
|
const msg = err.response?.data || err.message;
|
||||||
console.error(`[API ERROR] ${status || 'NET'} ${endpoint}:`, msg);
|
console.error(`[DEBUG_API] >>> ERROR ${status || 'NET'} on ${endpoint}:`, msg);
|
||||||
|
|
||||||
|
if (status === 401 || status === 403) {
|
||||||
|
if (mainWindow) mainWindow.webContents.send('auth:expired');
|
||||||
|
return { ok: false, error: 'Sessão expirada ou não autorizado.', expired: true };
|
||||||
|
}
|
||||||
return { ok: false, error: typeof msg === 'object' ? JSON.stringify(msg) : msg };
|
return { ok: false, error: typeof msg === 'object' ? JSON.stringify(msg) : msg };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -121,29 +84,55 @@ async function requestWithRetry(method, endpoint, data) {
|
|||||||
// ─── IPC Handlers (Registrar IMEDIATAMENTE) ──────────────────────────────────
|
// ─── IPC Handlers (Registrar IMEDIATAMENTE) ──────────────────────────────────
|
||||||
ipcMain.handle('auth:login', async (_, { username, password }) => {
|
ipcMain.handle('auth:login', async (_, { username, password }) => {
|
||||||
try {
|
try {
|
||||||
const res = await axios.post(`${getBaseUrl()}/token/`, { username, password });
|
const url = `${getBaseUrl()}/login`;
|
||||||
console.log('[MAIN] Login Successful. User:', res.data.user?.username);
|
console.log(`[DEBUG_LOGIN] >>> Tentando login em: ${url}`);
|
||||||
store.set('access_token', res.data.access);
|
|
||||||
store.set('refresh_token', res.data.refresh);
|
const res = await axios.post(url, { username, password }, {
|
||||||
store.set('user', res.data.user);
|
headers: { 'Content-Type': 'application/json' }
|
||||||
|
});
|
||||||
|
|
||||||
|
const userData = res.data;
|
||||||
|
console.log('[DEBUG_LOGIN] >>> Resposta Completa do Servidor:', JSON.stringify(userData));
|
||||||
|
|
||||||
|
// Busca o ID em qualquer lugar possível (id, user.id, user_id, UID, pk)
|
||||||
|
const userId = userData.id ||
|
||||||
|
(userData.user && userData.user.id) ||
|
||||||
|
userData.user_id ||
|
||||||
|
userData.pk;
|
||||||
|
|
||||||
|
console.log('[DEBUG_LOGIN] >>> ID Capturado:', userId);
|
||||||
|
|
||||||
|
if (!userId) {
|
||||||
|
console.warn('[DEBUG_LOGIN] >>> AVISO: Não encontramos um ID numérico. Verifique a Resposta Completa acima.');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Normaliza para garantir que user.id exista para o getHeaders()
|
||||||
|
if (!userData.id) userData.id = userId;
|
||||||
|
|
||||||
|
store.set('user', userData);
|
||||||
|
store.delete('access_token');
|
||||||
|
store.delete('refresh_token');
|
||||||
|
|
||||||
return { ok: true };
|
return { ok: true };
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('[MAIN] Login Failed:', err.response?.data || err.message);
|
console.error('[DEBUG_LOGIN] >>> Falha no Login:', err.response?.data || err.message);
|
||||||
return { ok: false, error: 'Credenciais inválidas.' };
|
return { ok: false, error: 'Erro de autenticação no servidor local.' };
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.handle('auth:logout', () => {
|
ipcMain.handle('auth:logout', () => {
|
||||||
|
store.delete('user');
|
||||||
store.delete('access_token');
|
store.delete('access_token');
|
||||||
store.delete('refresh_token');
|
store.delete('refresh_token');
|
||||||
store.delete('user');
|
|
||||||
return { ok: true };
|
return { ok: true };
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.handle('auth:check', () => ({ authenticated: !!store.get('access_token') }));
|
ipcMain.handle('auth:check', () => {
|
||||||
|
const user = store.get('user');
|
||||||
|
return { authenticated: !!(user && user.id) };
|
||||||
|
});
|
||||||
|
|
||||||
ipcMain.handle('auth:user', () => {
|
ipcMain.handle('auth:user', () => {
|
||||||
console.log('[MAIN] IPC auth:user requested.');
|
|
||||||
return store.get('user');
|
return store.get('user');
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -160,6 +149,38 @@ ipcMain.handle('config:set-url', (_, url) => {
|
|||||||
return { ok: true };
|
return { ok: true };
|
||||||
});
|
});
|
||||||
|
|
||||||
|
ipcMain.handle('config:get-print-silent', () => store.get('print_silent', false));
|
||||||
|
ipcMain.handle('config:set-print-silent', (_, value) => {
|
||||||
|
store.set('print_silent', value);
|
||||||
|
console.log('[MAIN] Print silent mode:', value);
|
||||||
|
return { ok: true };
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcMain.handle('print:direct', async (_, html) => {
|
||||||
|
const printSilent = store.get('print_silent', false);
|
||||||
|
try {
|
||||||
|
const win = new BrowserWindow({ show: false });
|
||||||
|
win.loadURL(`data:text/html;charset=utf-8,${encodeURIComponent(html)}`);
|
||||||
|
await new Promise(r => win.webContents.once('did-finish-load', r));
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
win.webContents.print({
|
||||||
|
silent: printSilent,
|
||||||
|
printBackground: true,
|
||||||
|
deviceName: ''
|
||||||
|
}, (success, errorType) => {
|
||||||
|
win.close();
|
||||||
|
if (success) {
|
||||||
|
resolve({ ok: true });
|
||||||
|
} else {
|
||||||
|
resolve({ ok: false, error: 'Nenhuma impressora configurada ou disponível.' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
return { ok: false, error: err.message };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// ─── Lifecycle ──────────────────────────────────────────────────────────────
|
// ─── Lifecycle ──────────────────────────────────────────────────────────────
|
||||||
app.whenReady().then(() => {
|
app.whenReady().then(() => {
|
||||||
createWindow();
|
createWindow();
|
||||||
|
|||||||
@@ -10,6 +10,11 @@ contextBridge.exposeInMainWorld('electronAPI', {
|
|||||||
// Config
|
// Config
|
||||||
getConfigUrl: () => ipcRenderer.invoke('config:get-url'),
|
getConfigUrl: () => ipcRenderer.invoke('config:get-url'),
|
||||||
setConfigUrl: (url) => ipcRenderer.invoke('config:set-url', url),
|
setConfigUrl: (url) => ipcRenderer.invoke('config:set-url', url),
|
||||||
|
getPrintSilent: () => ipcRenderer.invoke('config:get-print-silent'),
|
||||||
|
setPrintSilent: (value) => ipcRenderer.invoke('config:set-print-silent', value),
|
||||||
|
|
||||||
|
// Print
|
||||||
|
printDirect: (html) => ipcRenderer.invoke('print:direct', html),
|
||||||
|
|
||||||
// API CRUD
|
// API CRUD
|
||||||
get: (endpoint) => ipcRenderer.invoke('api:get', endpoint),
|
get: (endpoint) => ipcRenderer.invoke('api:get', endpoint),
|
||||||
|
|||||||
@@ -31,28 +31,63 @@ export async function renderClientes(container) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let _clientesData = [];
|
let _clientesData = [];
|
||||||
|
let _comandasData = [];
|
||||||
let _productsMap = {};
|
let _productsMap = {};
|
||||||
|
let _paymentTypes = [];
|
||||||
|
|
||||||
async function loadClientes() {
|
async function loadClientes() {
|
||||||
const wrap = document.getElementById('clientes-table');
|
const wrap = document.getElementById('clientes-table');
|
||||||
if (!wrap) return;
|
if (!wrap) return;
|
||||||
wrap.innerHTML = `<div class="loading-screen"><div class="spinner"></div></div>`;
|
wrap.innerHTML = `<div class="loading-screen"><div class="spinner"></div></div>`;
|
||||||
|
|
||||||
// Carrega clientes e produtos em paralelo para ter os preços
|
// Carrega clientes, produtos, comandas e tipos de pagamento em paralelo
|
||||||
const [res, pRes] = await Promise.all([
|
const [res, pRes, cRes, ptRes] = await Promise.all([
|
||||||
window.electronAPI.get('/clients/'),
|
window.electronAPI.get('/clients'),
|
||||||
window.electronAPI.get('/products/')
|
window.electronAPI.get('/products'),
|
||||||
|
window.electronAPI.get('/comandas'),
|
||||||
|
window.electronAPI.get('/payment-types')
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
if (ptRes.ok) _paymentTypes = ptRes.data;
|
||||||
|
|
||||||
if (pRes.ok) {
|
if (pRes.ok) {
|
||||||
_productsMap = pRes.data.reduce((acc, p) => {
|
_productsMap = pRes.data.reduce((acc, p) => {
|
||||||
acc[p.id] = parseFloat(p.price || 0);
|
acc[String(p.id)] = {
|
||||||
|
name: p.name,
|
||||||
|
price: parseFloat(p.price || 0)
|
||||||
|
};
|
||||||
return acc;
|
return acc;
|
||||||
}, {});
|
}, {});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (cRes.ok) _comandasData = cRes.data;
|
||||||
|
|
||||||
if (!res.ok) { wrap.innerHTML = `<div class="table-empty">Erro ao carregar clientes.</div>`; return; }
|
if (!res.ok) { wrap.innerHTML = `<div class="table-empty">Erro ao carregar clientes.</div>`; return; }
|
||||||
_clientesData = res.data;
|
|
||||||
|
_clientesData = res.data || [];
|
||||||
|
_comandasData = cRes.ok ? (cRes.data || []) : [];
|
||||||
|
|
||||||
|
// Calcula o débito real de cada cliente somando suas comandas FIADO
|
||||||
|
_clientesData.forEach(c => {
|
||||||
|
const fiados = _comandasData.filter(com => {
|
||||||
|
// Baseado no model Go: json:"client"
|
||||||
|
const cid = com.client;
|
||||||
|
return String(cid) === String(c.id) && String(com.status).toUpperCase() === 'FIADO';
|
||||||
|
});
|
||||||
|
|
||||||
|
c.real_debt = fiados.reduce((acc, com) => {
|
||||||
|
const totalComanda = (com.items || []).reduce((sum, item) => {
|
||||||
|
const pInfo = _productsMap[String(item.product)];
|
||||||
|
const preco = pInfo ? pInfo.price : parseFloat(item.product_price || 0);
|
||||||
|
return sum + preco;
|
||||||
|
}, 0);
|
||||||
|
return acc + totalComanda;
|
||||||
|
}, 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
const comDebito = _clientesData.filter(c => c.real_debt > 0);
|
||||||
|
console.log(`[DEBUG_CLIENTS] Cálculo dinâmico finalizado. Sucesso p/ ${comDebito.length} clientes.`);
|
||||||
|
|
||||||
renderClientesTable(_clientesData);
|
renderClientesTable(_clientesData);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -61,8 +96,8 @@ function renderClientesTable(data) {
|
|||||||
if (!wrap) return;
|
if (!wrap) return;
|
||||||
if (!data.length) { wrap.innerHTML = `<div class="table-empty">Nenhum cliente encontrado.</div>`; return; }
|
if (!data.length) { wrap.innerHTML = `<div class="table-empty">Nenhum cliente encontrado.</div>`; return; }
|
||||||
|
|
||||||
// Ordena por maior débito por padrão
|
// Ordena por maior débito por padrão usando o cálculo dinâmico
|
||||||
const sorted = [...data].sort((a, b) => parseFloat(b.debt || 0) - parseFloat(a.debt || 0));
|
const sorted = [...data].sort((a, b) => (b.real_debt || 0) - (a.real_debt || 0));
|
||||||
|
|
||||||
wrap.innerHTML = `
|
wrap.innerHTML = `
|
||||||
<table>
|
<table>
|
||||||
@@ -70,20 +105,20 @@ function renderClientesTable(data) {
|
|||||||
<th>#</th>
|
<th>#</th>
|
||||||
<th>Nome</th>
|
<th>Nome</th>
|
||||||
<th>Contato</th>
|
<th>Contato</th>
|
||||||
<th>Débito</th>
|
<th style="color:var(--primary)">Débito Dinâmico</th>
|
||||||
<th>Status</th>
|
<th>Status</th>
|
||||||
<th>Cadastrado em</th>
|
<th>Cadastrado em</th>
|
||||||
<th>Ações</th>
|
<th>Ações</th>
|
||||||
</tr></thead>
|
</tr></thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
${sorted.map(c => {
|
${sorted.map(c => {
|
||||||
const debt = parseFloat(c.debt || 0);
|
const debt = c.real_debt || 0;
|
||||||
return `<tr>
|
return `<tr>
|
||||||
<td style="color:var(--text-muted)">#${c.id}</td>
|
<td style="color:var(--text-muted)">#${c.id}</td>
|
||||||
<td><strong>${c.name}</strong></td>
|
<td><strong>${c.name}</strong></td>
|
||||||
<td>${c.contact || '–'}</td>
|
<td>${c.contact || '–'}</td>
|
||||||
<td>
|
<td>
|
||||||
<span style="font-weight:600; color: ${debt > 0 ? 'var(--danger)' : 'var(--text-secondary)'}">
|
<span style="font-weight:700; font-size:1.05rem; color: ${debt > 0 ? 'var(--danger)' : 'var(--text-secondary)'}">
|
||||||
R$ ${debt.toFixed(2)}
|
R$ ${debt.toFixed(2)}
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
@@ -95,7 +130,7 @@ function renderClientesTable(data) {
|
|||||||
<td style="font-size:0.82rem;color:var(--text-muted)">${formatDate(c.created_at)}</td>
|
<td style="font-size:0.82rem;color:var(--text-muted)">${formatDate(c.created_at)}</td>
|
||||||
<td>
|
<td>
|
||||||
<div style="display:flex;gap:6px">
|
<div style="display:flex;gap:6px">
|
||||||
<button class="btn btn-info btn-sm btn-hist-cli" data-id="${c.id}" title="Ver Fiados">📜</button>
|
<button class="btn btn-info btn-sm btn-hist-cli" data-id="${c.id}">📜 Ver Fiados</button>
|
||||||
<button class="btn btn-secondary btn-sm btn-edit-cli" data-id="${c.id}">Editar</button>
|
<button class="btn btn-secondary btn-sm btn-edit-cli" data-id="${c.id}">Editar</button>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
@@ -131,7 +166,7 @@ function filtrarClientes() {
|
|||||||
(c.contact || '').toLowerCase().includes(q) ||
|
(c.contact || '').toLowerCase().includes(q) ||
|
||||||
String(c.id).includes(q);
|
String(c.id).includes(q);
|
||||||
|
|
||||||
const debtValue = parseFloat(c.debt || 0);
|
const debtValue = c.real_debt || 0;
|
||||||
const matchDebt = !debtFltr ||
|
const matchDebt = !debtFltr ||
|
||||||
(debtFltr === 'has-debt' ? debtValue > 0 : debtValue === 0);
|
(debtFltr === 'has-debt' ? debtValue > 0 : debtValue === 0);
|
||||||
|
|
||||||
@@ -187,8 +222,8 @@ function abrirModalCliente(cliente = null) {
|
|||||||
if (!data.name) return showToast('Informe o nome do cliente.', 'error');
|
if (!data.name) return showToast('Informe o nome do cliente.', 'error');
|
||||||
|
|
||||||
const r = isEdit
|
const r = isEdit
|
||||||
? await window.electronAPI.put(`/clients/${cliente.id}/`, data)
|
? await window.electronAPI.put(`/clients/${cliente.id}`, data)
|
||||||
: await window.electronAPI.post('/clients/', data);
|
: await window.electronAPI.post('/clients', data);
|
||||||
|
|
||||||
if (r.ok) { showToast(isEdit ? 'Cliente atualizado!' : 'Cliente criado!', 'success'); closeModal(); loadClientes(); }
|
if (r.ok) { showToast(isEdit ? 'Cliente atualizado!' : 'Cliente criado!', 'success'); closeModal(); loadClientes(); }
|
||||||
else showToast(r.error, 'error');
|
else showToast(r.error, 'error');
|
||||||
@@ -200,42 +235,64 @@ async function abrirHistoricoFiados(cliente) {
|
|||||||
title: `📜 Fiados: ${cliente.name}`,
|
title: `📜 Fiados: ${cliente.name}`,
|
||||||
body: `
|
body: `
|
||||||
<div id="fiados-modal-content">
|
<div id="fiados-modal-content">
|
||||||
<div id="fiados-list" class="loading-screen"><div class="spinner"></div></div>
|
<div id="fiados-list"></div>
|
||||||
|
|
||||||
<div id="fiados-summary" class="hidden" style="margin-top:20px; padding-top:15px; border-top:2px solid var(--border); display:flex; justify-content:space-between; align-items:center;">
|
<div id="fiados-summary" class="hidden" style="margin-top:20px; padding-top:15px; border-top:2px solid var(--border); display:flex; flex-direction:column; gap:12px;">
|
||||||
<div>
|
<div style="display:flex; justify-content:space-between; align-items:center">
|
||||||
<div style="font-size:0.85rem; color:var(--text-secondary)">Selecionados: <span id="selected-count">0</span></div>
|
<div>
|
||||||
<div style="font-size:1.2rem; font-weight:700; color:var(--success)">Total: R$ <span id="selected-total">0.00</span></div>
|
<div style="font-size:0.85rem; color:var(--text-secondary)">Selecionados: <span id="selected-count">0</span></div>
|
||||||
|
<div style="font-size:1.2rem; font-weight:700; color:var(--success)">Total: R$ <span id="selected-total">0.00</span></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="width:200px">
|
||||||
|
<label style="font-size:0.75rem; color:var(--text-muted); text-transform:uppercase; font-weight:700">Forma de Pagamento</label>
|
||||||
|
<select id="pay-select-type" class="form-control" style="margin-top:4px">
|
||||||
|
${_paymentTypes.map(pt => `<option value="${pt.id}">${pt.nome || pt.name}</option>`).join('')}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<button class="btn btn-primary btn-md" id="btn-pagar-selecionados" disabled>💳 Pagar Selecionados</button>
|
|
||||||
|
<button class="btn btn-primary btn-md" id="btn-pagar-selecionados" disabled style="width:100%">💳 Pagar Selecionados</button>
|
||||||
</div>
|
</div>
|
||||||
</div>`,
|
</div>`,
|
||||||
footer: `<button class="btn btn-secondary btn-md" onclick="closeModal()">Sair</button>`
|
footer: `<button class="btn btn-secondary btn-md" onclick="closeModal()">Sair</button>`
|
||||||
});
|
});
|
||||||
|
|
||||||
const res = await window.electronAPI.get(`/clients/${cliente.id}/fiados/`);
|
|
||||||
const listContainer = document.getElementById('fiados-list');
|
const listContainer = document.getElementById('fiados-list');
|
||||||
const summary = document.getElementById('fiados-summary');
|
const summary = document.getElementById('fiados-summary');
|
||||||
if (!listContainer) return;
|
if (!listContainer) return;
|
||||||
|
|
||||||
if (!res.ok) {
|
console.log(`[DEBUG_FIADOS] Procurando fiados para cliente ${cliente.id} (${cliente.name})...`);
|
||||||
listContainer.innerHTML = `<div class="table-empty">Erro ao carregar fiados: ${res.error}</div>`;
|
console.log(`[DEBUG_FIADOS] Total de comandas na memória: ${_comandasData.length}`);
|
||||||
return;
|
|
||||||
}
|
// Filtra as comandas FIADO do cliente de forma robusta
|
||||||
|
const fiados = _comandasData.filter(com => {
|
||||||
|
const isFiado = String(com.status).toUpperCase() === 'FIADO';
|
||||||
|
const isMeuClient = String(com.client) === String(cliente.id); // Usando json:"client"
|
||||||
|
return isFiado && isMeuClient;
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(`[DEBUG_FIADOS] Encontradas:`, fiados);
|
||||||
|
|
||||||
const fiados = res.data;
|
|
||||||
if (!fiados.length) {
|
if (!fiados.length) {
|
||||||
listContainer.innerHTML = `<div class="table-empty">Nenhuma comanda pendente para este cliente.</div>`;
|
listContainer.innerHTML = `
|
||||||
|
<div class="table-empty">
|
||||||
|
Nenhuma comanda pendente para este cliente.<br/>
|
||||||
|
<small style="color:var(--text-muted)">Debug: ${_comandasData.length} comandas totais na memória</small>
|
||||||
|
</div>`;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
listContainer.classList.remove('loading-screen');
|
|
||||||
listContainer.style.maxHeight = '400px';
|
listContainer.style.maxHeight = '400px';
|
||||||
listContainer.style.overflowY = 'auto';
|
listContainer.style.overflowY = 'auto';
|
||||||
summary.classList.remove('hidden');
|
summary.classList.remove('hidden');
|
||||||
|
|
||||||
listContainer.innerHTML = fiados.map(f => {
|
listContainer.innerHTML = fiados.map(f => {
|
||||||
const totalComanda = (f.items || []).reduce((acc, it) => acc + (_productsMap[it.product] || 0), 0);
|
const totalComanda = (f.items || []).reduce((acc, it) => {
|
||||||
|
const pInfo = _productsMap[String(it.product)];
|
||||||
|
const preco = pInfo ? pInfo.price : parseFloat(it.product_price || 0);
|
||||||
|
return acc + preco;
|
||||||
|
}, 0);
|
||||||
|
|
||||||
return `
|
return `
|
||||||
<div class="card card-fiado" style="margin-bottom:15px; border-left: 4px solid var(--warning); position:relative; padding-left:50px">
|
<div class="card card-fiado" style="margin-bottom:15px; border-left: 4px solid var(--warning); position:relative; padding-left:50px">
|
||||||
@@ -245,7 +302,7 @@ async function abrirHistoricoFiados(cliente) {
|
|||||||
|
|
||||||
<div style="display:flex; justify-content:space-between; align-items:flex-start; margin-bottom:10px;">
|
<div style="display:flex; justify-content:space-between; align-items:flex-start; margin-bottom:10px;">
|
||||||
<div>
|
<div>
|
||||||
<div style="font-weight:600; font-size:1.1rem;">Comanda #${f.id} - ${f.name || 'Sem nome'}</div>
|
<div style="font-weight:600; font-size:1.1rem;">Comanda #${f.id} — ${f.name || 'Sem nome'}</div>
|
||||||
<div style="font-size:0.8rem; color:var(--text-muted)">Abertura: ${formatDate(f.dt_open)}</div>
|
<div style="font-size:0.8rem; color:var(--text-muted)">Abertura: ${formatDate(f.dt_open)}</div>
|
||||||
</div>
|
</div>
|
||||||
<div style="text-align:right">
|
<div style="text-align:right">
|
||||||
@@ -254,25 +311,26 @@ async function abrirHistoricoFiados(cliente) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style="font-size:0.85rem;">
|
|
||||||
<div style="margin-bottom:5px; color:var(--text-secondary)">Mesa: ${f.mesa_name || '–'} | Lançado por: ${f.user_name || '–'}</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div style="margin-top:10px; border-top:1px solid var(--border); padding-top:10px;">
|
<div style="margin-top:10px; border-top:1px solid var(--border); padding-top:10px;">
|
||||||
<details>
|
<details>
|
||||||
<summary style="font-weight:600; font-size:0.8rem; text-transform:uppercase; color:var(--text-muted); cursor:pointer; outline:none">
|
<summary style="font-weight:600; font-size:0.8rem; text-transform:uppercase; color:var(--text-muted); cursor:pointer; outline:none">
|
||||||
Ver Itens (${(f.items || []).length})
|
Ver Itens (${(f.items || []).length})
|
||||||
</summary>
|
</summary>
|
||||||
<ul style="list-style:none; padding:10px 0 0 0; margin:0; font-size:0.85rem;">
|
<ul style="list-style:none; padding:10px 0 0 0; margin:0; font-size:0.85rem;">
|
||||||
${(f.items || []).map(it => `
|
${(f.items || []).map(it => {
|
||||||
|
const pInfo = _productsMap[String(it.product)];
|
||||||
|
const prodName = pInfo ? pInfo.name : (it.product_name || `Produto #${it.product}`);
|
||||||
|
const prodPreco = pInfo ? pInfo.price : parseFloat(it.product_price || 0);
|
||||||
|
|
||||||
|
return `
|
||||||
<li style="display:flex; justify-content:space-between; padding:4px 0; border-bottom:1px dashed var(--border)">
|
<li style="display:flex; justify-content:space-between; padding:4px 0; border-bottom:1px dashed var(--border)">
|
||||||
<span>• ${it.product_name}</span>
|
<span>• ${prodName}</span>
|
||||||
<div style="text-align:right">
|
<div style="text-align:right">
|
||||||
<span>R$ ${(_productsMap[it.product] || 0).toFixed(2)}</span>
|
<span>R$ ${prodPreco.toFixed(2)}</span>
|
||||||
<div style="font-size:0.7rem; color:var(--text-muted)">${formatDateShort(it.data_time)}</div>
|
<div style="font-size:0.7rem; color:var(--text-muted)">${formatDateShort(it.data_time)}</div>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
`).join('')}
|
`}).join('')}
|
||||||
</ul>
|
</ul>
|
||||||
</details>
|
</details>
|
||||||
</div>
|
</div>
|
||||||
@@ -298,24 +356,45 @@ async function abrirHistoricoFiados(cliente) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
document.getElementById('btn-pagar-selecionados').addEventListener('click', async () => {
|
document.getElementById('btn-pagar-selecionados').addEventListener('click', async () => {
|
||||||
const selecionados = Array.from(listContainer.querySelectorAll('.fiado-check:checked')).map(c => parseInt(c.dataset.id));
|
const checks = Array.from(listContainer.querySelectorAll('.fiado-check:checked'));
|
||||||
|
const selecionados = checks.map(c => ({
|
||||||
|
id: parseInt(c.dataset.id),
|
||||||
|
total: parseFloat(c.dataset.total)
|
||||||
|
}));
|
||||||
|
|
||||||
|
const payTypeId = parseInt(document.getElementById('pay-select-type').value);
|
||||||
const totalPrompt = document.getElementById('selected-total').textContent;
|
const totalPrompt = document.getElementById('selected-total').textContent;
|
||||||
|
|
||||||
if (confirm(`Deseja confirmar o pagamento de R$ ${totalPrompt} referente a ${selecionados.length} comanda(s)?`)) {
|
if (confirm(`Confirmar o recebimento de R$ ${totalPrompt} referente a ${selecionados.length} comanda(s)?`)) {
|
||||||
const btn = document.getElementById('btn-pagar-selecionados');
|
const btn = document.getElementById('btn-pagar-selecionados');
|
||||||
btn.disabled = true;
|
btn.disabled = true;
|
||||||
btn.textContent = 'Processando...';
|
btn.textContent = 'Processando...';
|
||||||
|
|
||||||
const r = await window.electronAPI.post('/clients/pagar_fiados/', { ids: selecionados });
|
let erros = 0;
|
||||||
|
for (const item of selecionados) {
|
||||||
|
// Encontra os detalhes da comanda para a descrição
|
||||||
|
const comanda = _comandasData.find(c => c.id === item.id);
|
||||||
|
const desc = `RECEBIMENTO FIADO — Comanda #${item.id} (${comanda?.name || '–'})`.trim();
|
||||||
|
|
||||||
if (r.ok) {
|
const payload = {
|
||||||
showToast('Pagamento realizado com sucesso!', 'success');
|
value: item.total,
|
||||||
|
type_pay: payTypeId,
|
||||||
|
client: parseInt(cliente.id),
|
||||||
|
description: desc,
|
||||||
|
status: 'CLOSED'
|
||||||
|
};
|
||||||
|
|
||||||
|
const r = await window.electronAPI.post(`/comandas/${item.id}/pagar`, payload);
|
||||||
|
if (!r.ok) erros++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (erros === 0) {
|
||||||
|
showToast('Todos os pagamentos foram processados!', 'success');
|
||||||
closeModal();
|
closeModal();
|
||||||
loadClientes(); // Recarrega a lista para atualizar os débitos
|
loadClientes();
|
||||||
} else {
|
} else {
|
||||||
showToast(r.error || 'Erro ao processar pagamento.', 'error');
|
showToast(`Concluído com ${erros} erro(s). Verifique os recibos.`, 'warning');
|
||||||
btn.disabled = false;
|
loadClientes();
|
||||||
btn.textContent = '💳 Pagar Selecionados';
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,3 +1,123 @@
|
|||||||
|
function imprimirComanda(comanda, pagamentosComanda, totalComanda) {
|
||||||
|
const itens = comanda.items || [];
|
||||||
|
const totalPago = (pagamentosComanda || []).reduce((acc, p) => acc + parseFloat(p.value || 0), 0);
|
||||||
|
const valorRestante = totalComanda - totalPago;
|
||||||
|
|
||||||
|
const dataAtual = new Date().toLocaleString('pt-BR');
|
||||||
|
const nomeEstabelecimento = 'RRBEC - Bar & Restaurante';
|
||||||
|
|
||||||
|
const htmlImpressao = `
|
||||||
|
<div class="print-comanda">
|
||||||
|
<div class="print-header">
|
||||||
|
<div class="print-title">${nomeEstabelecimento}</div>
|
||||||
|
<div class="print-info">COMANDA #${comanda.id}</div>
|
||||||
|
<div class="print-info">Data: ${dataAtual}</div>
|
||||||
|
${comanda.name ? `<div class="print-info">Cliente: ${comanda.name}</div>` : ''}
|
||||||
|
${comanda.mesa_name ? `<div class="print-info">Mesa: ${comanda.mesa_name}</div>` : ''}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="print-items">
|
||||||
|
<div style="font-weight:bold;border-bottom:1px solid #000;padding-bottom:4px;margin-bottom:4px">
|
||||||
|
<span style="float:left">PRODUTO</span>
|
||||||
|
<span style="float:right">VALOR</span>
|
||||||
|
</div>
|
||||||
|
<div style="clear:both"></div>
|
||||||
|
${itens.map(it => {
|
||||||
|
const price = _productsMap[String(it.product)] || 0;
|
||||||
|
const nome = _productsNames[String(it.product)] || it.product_name || `Produto #${it.product}`;
|
||||||
|
return `
|
||||||
|
<div class="print-item">
|
||||||
|
<span>${nome}</span>
|
||||||
|
<span>R$ ${price.toFixed(2)}</span>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}).join('')}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="print-totals">
|
||||||
|
<div style="display:flex;justify-content:space-between;padding:4px 0">
|
||||||
|
<span>TOTAL:</span>
|
||||||
|
<span>R$ ${totalComanda.toFixed(2)}</span>
|
||||||
|
</div>
|
||||||
|
${totalPago > 0 ? `
|
||||||
|
<div style="display:flex;justify-content:space-between;padding:4px 0">
|
||||||
|
<span>PAGO:</span>
|
||||||
|
<span>R$ ${totalPago.toFixed(2)}</span>
|
||||||
|
</div>
|
||||||
|
<div style="display:flex;justify-content:space-between;padding:4px 0;font-weight:bold">
|
||||||
|
<span>RESTANTE:</span>
|
||||||
|
<span>R$ ${valorRestante.toFixed(2)}</span>
|
||||||
|
</div>
|
||||||
|
` : ''}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
${(pagamentosComanda || []).length > 0 ? `
|
||||||
|
<div style="margin-top:10px;border-top:1px dashed #000;padding-top:8px">
|
||||||
|
<div style="font-weight:bold;margin-bottom:4px">PAGAMENTOS:</div>
|
||||||
|
${pagamentosComanda.map(p => {
|
||||||
|
const tipoNome = _paymentTypes.find(t => String(t.id) === String(p.type_pay))?.name ||
|
||||||
|
_paymentTypes.find(t => String(t.id) === String(p.type_pay))?.nome || '–';
|
||||||
|
return `
|
||||||
|
<div style="display:flex;justify-content:space-between;padding:2px 0">
|
||||||
|
<span>${tipoNome}</span>
|
||||||
|
<span>R$ ${parseFloat(p.value || 0).toFixed(2)}</span>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}).join('')}
|
||||||
|
</div>
|
||||||
|
` : ''}
|
||||||
|
|
||||||
|
<div class="print-footer">
|
||||||
|
<div>------------------------</div>
|
||||||
|
<div>Obrigado pela preferência!</div>
|
||||||
|
<div>Volte sempre!</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
const htmlCompleto = `
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>Comanda #${comanda.id}</title>
|
||||||
|
<style>
|
||||||
|
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||||
|
body { font-family: 'Courier New', monospace; font-size: 12px; padding: 10px; width: 80mm; }
|
||||||
|
.print-comanda { display: block; }
|
||||||
|
.print-comanda * { color: black !important; background: transparent !important; }
|
||||||
|
.print-header { text-align: center; border-bottom: 1px dashed #000; padding-bottom: 8px; margin-bottom: 10px; }
|
||||||
|
.print-title { font-size: 16px; font-weight: bold; margin-bottom: 4px; }
|
||||||
|
.print-info { font-size: 11px; margin: 2px 0; }
|
||||||
|
.print-items { padding-bottom: 8px; margin-bottom: 8px; }
|
||||||
|
.print-item { display: flex; justify-content: space-between; padding: 3px 0; }
|
||||||
|
.print-totals { font-weight: bold; }
|
||||||
|
.print-footer { text-align: center; margin-top: 15px; font-size: 10px; }
|
||||||
|
@media print {
|
||||||
|
@page { size: 80mm auto; margin: 0; }
|
||||||
|
body { width: 80mm; }
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
${htmlImpressao}
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
`;
|
||||||
|
|
||||||
|
window.electronAPI.printDirect(htmlCompleto).then(r => {
|
||||||
|
if (r.ok) {
|
||||||
|
showToast('Impressão enviada!', 'success');
|
||||||
|
} else {
|
||||||
|
showToast('Nenhuma impressora configurada. Abrindo diálogo...', 'warning');
|
||||||
|
const printWindow = window.open('', '', 'width=300,height=600');
|
||||||
|
printWindow.document.write(htmlCompleto);
|
||||||
|
printWindow.document.close();
|
||||||
|
printWindow.onload = () => printWindow.print();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export async function renderComandas(container) {
|
export async function renderComandas(container) {
|
||||||
container.innerHTML = `
|
container.innerHTML = `
|
||||||
<div class="page-header">
|
<div class="page-header">
|
||||||
@@ -24,7 +144,7 @@ export async function renderComandas(container) {
|
|||||||
</div>`;
|
</div>`;
|
||||||
|
|
||||||
let mesas = [];
|
let mesas = [];
|
||||||
const mesasRes = await window.electronAPI.get('/mesas/');
|
const mesasRes = await window.electronAPI.get('/mesas');
|
||||||
if (mesasRes.ok) mesas = mesasRes.data;
|
if (mesasRes.ok) mesas = mesasRes.data;
|
||||||
|
|
||||||
await loadComandas(mesas);
|
await loadComandas(mesas);
|
||||||
@@ -37,8 +157,10 @@ export async function renderComandas(container) {
|
|||||||
let _comandasData = [];
|
let _comandasData = [];
|
||||||
let _mesasRef = [];
|
let _mesasRef = [];
|
||||||
let _productsMap = {}; // Cache de preços {id: price}
|
let _productsMap = {}; // Cache de preços {id: price}
|
||||||
|
let _productsNames = {}; // Cache de nomes {id: name}
|
||||||
let _paymentTypes = [];
|
let _paymentTypes = [];
|
||||||
let _clients = [];
|
let _clients = [];
|
||||||
|
let _paymentsMap = {}; // Cache de pagamentos por comanda {comandaId: [pagamentos]}
|
||||||
|
|
||||||
async function loadComandas(mesas) {
|
async function loadComandas(mesas) {
|
||||||
_mesasRef = mesas;
|
_mesasRef = mesas;
|
||||||
@@ -46,12 +168,12 @@ async function loadComandas(mesas) {
|
|||||||
if (!wrap) return;
|
if (!wrap) return;
|
||||||
wrap.innerHTML = `<div class="loading-screen"><div class="spinner"></div></div>`;
|
wrap.innerHTML = `<div class="loading-screen"><div class="spinner"></div></div>`;
|
||||||
|
|
||||||
// Carrega dados necessários em paralelo
|
const [res, pRes, ptRes, cRes, pagsRes] = await Promise.all([
|
||||||
const [res, pRes, ptRes, cRes] = await Promise.all([
|
window.electronAPI.get('/comandas'),
|
||||||
window.electronAPI.get('/comandas/'),
|
window.electronAPI.get('/products'),
|
||||||
window.electronAPI.get('/products/'),
|
window.electronAPI.get('/payment-types'),
|
||||||
window.electronAPI.get('/payment-types/'),
|
window.electronAPI.get('/clients'),
|
||||||
window.electronAPI.get('/clients/')
|
window.electronAPI.get('/payments')
|
||||||
]);
|
]);
|
||||||
|
|
||||||
if (ptRes.ok) _paymentTypes = ptRes.data;
|
if (ptRes.ok) _paymentTypes = ptRes.data;
|
||||||
@@ -59,15 +181,30 @@ async function loadComandas(mesas) {
|
|||||||
|
|
||||||
if (pRes.ok) {
|
if (pRes.ok) {
|
||||||
_productsMap = pRes.data.reduce((acc, p) => {
|
_productsMap = pRes.data.reduce((acc, p) => {
|
||||||
acc[p.id] = parseFloat(p.price || 0);
|
acc[String(p.id)] = parseFloat(p.price || 0);
|
||||||
|
return acc;
|
||||||
|
}, {});
|
||||||
|
_productsNames = pRes.data.reduce((acc, p) => {
|
||||||
|
acc[String(p.id)] = p.name || `Produto #${p.id}`;
|
||||||
return acc;
|
return acc;
|
||||||
}, {});
|
}, {});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!res.ok) { wrap.innerHTML = `<div class="table-empty">Erro ao carregar comandas.</div>`; return; }
|
_paymentsMap = {};
|
||||||
_comandasData = res.data;
|
if (pagsRes.ok) {
|
||||||
|
(pagsRes.data || []).forEach(p => {
|
||||||
|
if (p.comanda) {
|
||||||
|
if (!_paymentsMap[p.comanda]) _paymentsMap[p.comanda] = [];
|
||||||
|
_paymentsMap[p.comanda].push(p);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!res.ok) { wrap.innerHTML = `<div class="table-empty">Erro ao carregar comandas.</div>`; return; }
|
||||||
|
_comandasData = res.ok ? res.data : [];
|
||||||
|
_comandasData.reverse();
|
||||||
|
console.log('[PDV] Comandas carregadas do servidor:', _comandasData);
|
||||||
|
|
||||||
// Aplica o filtro padrão (Ativas) logo no carregamento
|
|
||||||
filtrarComandas();
|
filtrarComandas();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -88,7 +225,6 @@ function renderComandasTable(data) {
|
|||||||
<th>Status</th>
|
<th>Status</th>
|
||||||
<th>Total</th>
|
<th>Total</th>
|
||||||
<th>Aberta em</th>
|
<th>Aberta em</th>
|
||||||
<th>Itens</th>
|
|
||||||
<th>Ações</th>
|
<th>Ações</th>
|
||||||
</tr></thead>
|
</tr></thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
@@ -100,29 +236,33 @@ function renderComandasTable(data) {
|
|||||||
};
|
};
|
||||||
const cfg = statusCfg[c.status] || { label: c.status, badge: 'badge-muted' };
|
const cfg = statusCfg[c.status] || { label: c.status, badge: 'badge-muted' };
|
||||||
const ativa = c.status === 'OPEN' || c.status === 'PAYING';
|
const ativa = c.status === 'OPEN' || c.status === 'PAYING';
|
||||||
const totalComanda = (c.items || []).reduce((acc, item) => acc + (_productsMap[item.product] || 0), 0);
|
const totalComanda = (c.items || []).reduce((acc, item) => acc + (_productsMap[String(item.product)] || 0), 0);
|
||||||
|
|
||||||
return `<tr>
|
const pagamentos = _paymentsMap[c.id] || [];
|
||||||
|
const totalPago = pagamentos.reduce((acc, p) => acc + parseFloat(p.value || 0), 0);
|
||||||
|
const valorRestante = totalComanda - totalPago;
|
||||||
|
const temPagamentos = pagamentos.length > 0;
|
||||||
|
|
||||||
|
return `<tr class="comanda-row" data-id="${c.id}" style="cursor:pointer">
|
||||||
<td><strong>#${c.id}</strong></td>
|
<td><strong>#${c.id}</strong></td>
|
||||||
<td>${c.name || '–'}</td>
|
<td>${c.name || '–'}</td>
|
||||||
<td>${c.mesa_name || `Mesa ${c.mesa}` || '–'}</td>
|
<td>${c.mesa_name || `Mesa ${c.mesa}` || '–'}</td>
|
||||||
<td><span class="badge ${cfg.badge}">${cfg.label}</span></td>
|
<td><span class="badge ${cfg.badge}">${cfg.label}</span></td>
|
||||||
<td><strong style="color:var(--success)">R$ ${totalComanda.toFixed(2)}</strong></td>
|
<td>
|
||||||
|
${temPagamentos ? `<div style="display:flex;flex-direction:column;gap:2px">
|
||||||
|
<span style="text-decoration:line-through;color:var(--text-muted);font-size:0.85rem">R$ ${totalComanda.toFixed(2)}</span>
|
||||||
|
<strong style="color:${valorRestante <= 0 ? 'var(--success)' : 'var(--warning)'}">R$ ${Math.max(0, valorRestante).toFixed(2)}</strong>
|
||||||
|
${temPagamentos ? `<span style="font-size:0.7rem;color:var(--text-muted)">Pago: R$ ${totalPago.toFixed(2)}</span>` : ''}
|
||||||
|
</div>` : `<strong style="color:var(--success)">R$ ${totalComanda.toFixed(2)}</strong>`}
|
||||||
|
</td>
|
||||||
<td>${formatDate(c.dt_open)}</td>
|
<td>${formatDate(c.dt_open)}</td>
|
||||||
<td>
|
|
||||||
<span class="badge badge-info" style="cursor:pointer" data-id="${c.id}" title="Ver itens">
|
|
||||||
${(c.items || []).length} ${(c.items || []).length === 1 ? 'item' : 'itens'}
|
|
||||||
</span>
|
|
||||||
</td>
|
|
||||||
<td>
|
<td>
|
||||||
<div style="display:flex;gap:6px">
|
<div style="display:flex;gap:6px">
|
||||||
<button class="btn btn-secondary btn-sm btn-itens" data-id="${c.id}" title="Itens">🛒</button>
|
<button class="btn btn-secondary btn-sm btn-editar" data-id="${c.id}" title="Editar">✏️</button>
|
||||||
${ativa ? `<button class="btn btn-success btn-sm btn-receber" data-id="${c.id}" title="Receber">💰</button>` : ''}
|
${ativa ? `<button class="btn btn-success btn-sm btn-receber" data-id="${c.id}" title="Receber">💰</button>` : ''}
|
||||||
${ativa && c.status === 'OPEN' ? `<button class="btn btn-warning btn-sm btn-pagar" data-id="${c.id}" title="Avisar Pagamento">⏳</button>` : ''}
|
${ativa && c.status === 'OPEN' ? `<button class="btn btn-warning btn-sm btn-pagar" data-id="${c.id}" title="Avisar Pagamento">⏳</button>` : ''}
|
||||||
${ativa && c.status === 'PAYING' ? `<button class="btn btn-warning btn-sm btn-reopen" data-id="${c.id}" title="Reabrir Comanda">Reabrir</button>` : ''}
|
${ativa && c.status === 'PAYING' ? `<button class="btn btn-warning btn-sm btn-reopen" data-id="${c.id}" title="Reabrir Comanda">Reabrir</button>` : ''}
|
||||||
|
</div>
|
||||||
|
|
||||||
</div>
|
|
||||||
</td>
|
</td>
|
||||||
</tr>`;
|
</tr>`;
|
||||||
}).join('')}
|
}).join('')}
|
||||||
@@ -131,119 +271,177 @@ function renderComandasTable(data) {
|
|||||||
${data.length > 100 ? `<div style="padding:10px; text-align:center; color:var(--text-muted); font-size:0.8rem;">Exibindo apenas as últimas 100 de ${data.length} comandas.</div>` : ''}
|
${data.length > 100 ? `<div style="padding:10px; text-align:center; color:var(--text-muted); font-size:0.8rem;">Exibindo apenas as últimas 100 de ${data.length} comandas.</div>` : ''}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
// Listener para linha toda
|
||||||
|
wrap.querySelectorAll('.comanda-row').forEach(row => {
|
||||||
|
row.addEventListener('click', () => {
|
||||||
|
const comanda = _comandasData.find(c => c.id === parseInt(row.dataset.id));
|
||||||
|
if (comanda) abrirItensComanda(comanda);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
// Listener para Receber
|
// Listener para Receber
|
||||||
wrap.querySelectorAll('.btn-receber').forEach(btn => {
|
wrap.querySelectorAll('.btn-receber').forEach(btn => {
|
||||||
btn.addEventListener('click', () => {
|
btn.addEventListener('click', (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
const comanda = _comandasData.find(c => c.id === parseInt(btn.dataset.id));
|
const comanda = _comandasData.find(c => c.id === parseInt(btn.dataset.id));
|
||||||
if (comanda) abrirModalReceber(comanda);
|
if (comanda) abrirModalPagamento(comanda);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Listener para Editar
|
||||||
|
wrap.querySelectorAll('.btn-editar').forEach(btn => {
|
||||||
|
btn.addEventListener('click', (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
const comanda = _comandasData.find(c => c.id === parseInt(btn.dataset.id));
|
||||||
|
if (comanda) abrirModalNovaComanda(_mesasRef, comanda);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// Listener para botão "Pagar" (muda p/ PAYING)
|
// Listener para botão "Pagar" (muda p/ PAYING)
|
||||||
wrap.querySelectorAll('.btn-pagar').forEach(btn => {
|
wrap.querySelectorAll('.btn-pagar').forEach(btn => {
|
||||||
btn.addEventListener('click', async () => {
|
btn.addEventListener('click', async (e) => {
|
||||||
const r = await window.electronAPI.patch(`/comandas/${btn.dataset.id}/`, { status: 'PAYING' });
|
e.stopPropagation();
|
||||||
|
const r = await window.electronAPI.patch(`/comandas/${btn.dataset.id}`, { status: 'PAYING' });
|
||||||
if (r.ok) { showToast('Comanda em fase de pagamento!', 'info'); loadComandas(_mesasRef); }
|
if (r.ok) { showToast('Comanda em fase de pagamento!', 'info'); loadComandas(_mesasRef); }
|
||||||
else showToast(r.error, 'error');
|
else showToast(r.error, 'error');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// Listener para botão "Reabrir" (muda p/ OPEN)
|
// Listener para botão "Reabrir" (muda p/ OPEN)
|
||||||
wrap.querySelectorAll('.btn-reopen').forEach(btn => {
|
wrap.querySelectorAll('.btn-reopen').forEach(btn => {
|
||||||
btn.addEventListener('click', async () => {
|
btn.addEventListener('click', async (e) => {
|
||||||
const r = await window.electronAPI.patch(`/comandas/${btn.dataset.id}/`, { status: 'OPEN' });
|
e.stopPropagation();
|
||||||
|
const r = await window.electronAPI.patch(`/comandas/${btn.dataset.id}`, { status: 'OPEN' });
|
||||||
if (r.ok) { showToast('Comanda reaberta!', 'info'); loadComandas(_mesasRef); }
|
if (r.ok) { showToast('Comanda reaberta!', 'info'); loadComandas(_mesasRef); }
|
||||||
else showToast(r.error, 'error');
|
else showToast(r.error, 'error');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
async function abrirModalPagamento(comanda, onPaymentComplete) {
|
||||||
|
try {
|
||||||
|
if (!comanda.items) comanda.items = [];
|
||||||
|
const totalBruto = comanda.items.reduce((acc, it) => acc + (_productsMap[String(it.product)] || 0), 0);
|
||||||
|
|
||||||
// Listener para ver itens da comanda
|
const pagamentosAtuais = _paymentsMap[comanda.id] || [];
|
||||||
wrap.querySelectorAll('.btn-itens').forEach(btn => {
|
const totalPago = pagamentosAtuais.reduce((acc, p) => acc + parseFloat(p.value || 0), 0);
|
||||||
btn.addEventListener('click', () => {
|
const valorRestante = Math.max(0, totalBruto - totalPago);
|
||||||
const comanda = _comandasData.find(c => c.id === parseInt(btn.dataset.id));
|
|
||||||
if (comanda) abrirItensComanda(comanda);
|
openModal({
|
||||||
|
title: `💰 Receber Pagamento - Comanda #${comanda.id}`,
|
||||||
|
body: `
|
||||||
|
<div class="form-grid">
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Valor Total (R$)</label>
|
||||||
|
<input type="number" id="pay-value" class="form-control" value="${valorRestante.toFixed(2)}" step="0.01" />
|
||||||
|
${totalPago > 0 ? `<small style="color:var(--text-muted)">Valor restante (já pagos: R$ ${totalPago.toFixed(2)})</small>` : ''}
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Forma de Pagamento</label>
|
||||||
|
<select id="pay-type" class="form-control">
|
||||||
|
${_paymentTypes.map(pt => `<option value="${pt.id}">${pt.name || pt.nome}</option>`).join('')}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Cliente (Opcional)</label>
|
||||||
|
<select id="pay-client" class="form-control">
|
||||||
|
<option value="">Consumidor Final</option>
|
||||||
|
${_clients.filter(c => c.active !== false).map(cl => `
|
||||||
|
<option value="${cl.id}" ${String(comanda.client) === String(cl.id) ? 'selected' : ''}>
|
||||||
|
${cl.name}
|
||||||
|
</option>
|
||||||
|
`).join('')}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="form-group" style="grid-column: span 2">
|
||||||
|
<label>Descrição / Observações</label>
|
||||||
|
<input type="text" id="pay-desc" class="form-control" placeholder="Ex: Pagamento total..." value="Pagamento total" />
|
||||||
|
</div>
|
||||||
|
</div>`,
|
||||||
|
footer: `
|
||||||
|
<button class="btn btn-secondary btn-md" onclick="closeModal()">Cancelar</button>
|
||||||
|
<button class="btn btn-success btn-md" id="btn-confirmar-pagamento">${valorRestante <= 0 ? 'Quitar Dívida' : 'Confirmar Recebimento'}</button>`
|
||||||
});
|
});
|
||||||
});
|
|
||||||
|
|
||||||
// Badge de itens também abre o modal
|
const btnConfirmar = document.getElementById('btn-confirmar-pagamento');
|
||||||
wrap.querySelectorAll('.badge[data-id]').forEach(badge => {
|
|
||||||
badge.addEventListener('click', () => {
|
document.getElementById('pay-value').addEventListener('input', () => {
|
||||||
const comanda = _comandasData.find(c => c.id === parseInt(badge.dataset.id));
|
const valorInformado = parseFloat(document.getElementById('pay-value').value) || 0;
|
||||||
if (comanda) abrirItensComanda(comanda);
|
if (valorInformado < valorRestante) {
|
||||||
|
btnConfirmar.textContent = 'Pagamento Parcial';
|
||||||
|
btnConfirmar.classList.remove('btn-success');
|
||||||
|
btnConfirmar.classList.add('btn-warning');
|
||||||
|
} else {
|
||||||
|
btnConfirmar.textContent = valorRestante <= 0 ? 'Quitar Dívida' : 'Confirmar Recebimento';
|
||||||
|
btnConfirmar.classList.remove('btn-warning');
|
||||||
|
btnConfirmar.classList.add('btn-success');
|
||||||
|
}
|
||||||
});
|
});
|
||||||
});
|
|
||||||
|
|
||||||
// Excluir comanda (Antigo Fechar)
|
btnConfirmar.addEventListener('click', async () => {
|
||||||
wrap.querySelectorAll('.btn-excluir').forEach(btn => {
|
const payTypeId = parseInt(document.getElementById('pay-type').value);
|
||||||
btn.addEventListener('click', async () => {
|
const clientId = document.getElementById('pay-client').value || null;
|
||||||
if (confirm('Deseja realmente EXCLUIR/APAGAR esta comanda?')) {
|
const totalOriginal = parseFloat(document.getElementById('pay-value').value);
|
||||||
const r = await window.electronAPI.post(`/comandas/${btn.dataset.id}/apagar/`, {});
|
|
||||||
if (r.ok) {
|
const tipoPgto = document.getElementById('pay-type').options[document.getElementById('pay-type').selectedIndex].text;
|
||||||
showToast('Comanda excluída!', 'success');
|
const isVale = tipoPgto.toLowerCase().includes('vale');
|
||||||
loadComandas(_mesasRef);
|
const isPagamentoParcial = totalOriginal < valorRestante;
|
||||||
|
|
||||||
|
if (isVale && !clientId) {
|
||||||
|
return showToast('Para pagamentos em Vale, selecione um cliente.', 'warning');
|
||||||
|
}
|
||||||
|
|
||||||
|
const numericClientId = clientId ? parseInt(clientId) : null;
|
||||||
|
|
||||||
|
const payload = {
|
||||||
|
value: isVale ? 0 : totalOriginal,
|
||||||
|
type_pay: payTypeId,
|
||||||
|
client: numericClientId,
|
||||||
|
description: isVale ? `Vale - R$ ${totalOriginal.toFixed(2)}` : (document.getElementById('pay-desc').value || ''),
|
||||||
|
status: isVale ? 'FIADO' : (isPagamentoParcial ? 'OPEN' : 'CLOSED')
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!isVale && (isNaN(payload.value) || payload.value <= 0)) {
|
||||||
|
return showToast('Informe um valor válido.', 'error');
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
btnConfirmar.disabled = true;
|
||||||
|
btnConfirmar.textContent = 'Processando...';
|
||||||
|
|
||||||
|
const rPay = await window.electronAPI.post(`/comandas/${comanda.id}/pagar`, payload);
|
||||||
|
if (!rPay.ok) throw new Error(rPay.error);
|
||||||
|
|
||||||
|
if (isVale) {
|
||||||
|
await new Promise(r => setTimeout(r, 300));
|
||||||
|
const rPatch = await window.electronAPI.patch(`/comandas/${comanda.id}`, {
|
||||||
|
status: 'FIADO',
|
||||||
|
client: numericClientId
|
||||||
|
});
|
||||||
|
if (!rPatch.ok) console.error('[PDV] Erro no patch FIADO:', rPatch.error);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isPagamentoParcial) {
|
||||||
|
showToast('Pagamento parcial registrado!', 'success');
|
||||||
|
} else if (valorRestante <= 0) {
|
||||||
|
showToast('Dívida quitada!', 'success');
|
||||||
} else {
|
} else {
|
||||||
showToast(r.error, 'error');
|
showToast(isVale ? 'Venda registrada como FIADO!' : 'Pagamento realizado!', 'success');
|
||||||
|
}
|
||||||
|
closeModal();
|
||||||
|
loadComandas(_mesasRef);
|
||||||
|
if (onPaymentComplete) onPaymentComplete();
|
||||||
|
} catch (err) {
|
||||||
|
showToast(err.message, 'error');
|
||||||
|
if (btnConfirmar) {
|
||||||
|
btnConfirmar.disabled = false;
|
||||||
|
btnConfirmar.textContent = totalOriginal < valorRestante ? 'Pagamento Parcial' : 'Confirmar Recebimento';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
} catch (err) {
|
||||||
}
|
console.error('[PDV] Erro ao abrir modal de pagamento:', err);
|
||||||
|
showToast('Erro ao abrir tela de pagamento.', 'error');
|
||||||
function abrirModalReceber(comanda) {
|
}
|
||||||
const total = (comanda.items || []).reduce((acc, it) => acc + (_productsMap[it.product] || 0), 0);
|
|
||||||
|
|
||||||
openModal({
|
|
||||||
title: `💰 Receber Pago - Comanda #${comanda.id}`,
|
|
||||||
body: `
|
|
||||||
<div class="form-grid">
|
|
||||||
<div class="form-group">
|
|
||||||
<label>Valor Total (R$)</label>
|
|
||||||
<input type="number" id="pay-value" class="form-control" value="${total.toFixed(2)}" step="0.01" />
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label>Forma de Pagamento</label>
|
|
||||||
<select id="pay-type" class="form-control">
|
|
||||||
${_paymentTypes.map(pt => `<option value="${pt.id}">${pt.name || pt.nome}</option>`).join('')}
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label>Cliente (Opcional)</label>
|
|
||||||
<select id="pay-client" class="form-control">
|
|
||||||
<option value="">Cliente não identificado</option>
|
|
||||||
${_clients.map(cl => `<option value="${cl.id}" ${comanda.client === cl.id ? 'selected' : ''}>${cl.name || cl.nome}</option>`).join('')}
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div class="form-group" style="grid-column: span 2">
|
|
||||||
<label>Descrição / Observações</label>
|
|
||||||
<input type="text" id="pay-desc" class="form-control" placeholder="Ex: Pagamento total..." value="Pagamento total" />
|
|
||||||
</div>
|
|
||||||
</div>`,
|
|
||||||
footer: `
|
|
||||||
<button class="btn btn-secondary btn-md" onclick="closeModal()">Cancelar</button>
|
|
||||||
<button class="btn btn-success btn-md" id="btn-confirmar-pagamento">Confirmar Recebimento</button>`
|
|
||||||
});
|
|
||||||
|
|
||||||
document.getElementById('btn-confirmar-pagamento').addEventListener('click', async () => {
|
|
||||||
const payload = {
|
|
||||||
value: parseFloat(document.getElementById('pay-value').value),
|
|
||||||
type_pay: parseInt(document.getElementById('pay-type').value),
|
|
||||||
client: document.getElementById('pay-client').value || null,
|
|
||||||
description: document.getElementById('pay-desc').value.trim()
|
|
||||||
};
|
|
||||||
|
|
||||||
if (isNaN(payload.value) || payload.value <= 0) {
|
|
||||||
return showToast('Informe um valor válido.', 'error');
|
|
||||||
}
|
|
||||||
|
|
||||||
const r = await window.electronAPI.post(`/comandas/${comanda.id}/pagar/`, payload);
|
|
||||||
if (r.ok) {
|
|
||||||
showToast('Pagamento processado e comanda encerrada!', 'success');
|
|
||||||
closeModal();
|
|
||||||
loadComandas(_mesasRef);
|
|
||||||
} else {
|
|
||||||
showToast(r.error, 'error');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function filtrarComandas() {
|
function filtrarComandas() {
|
||||||
@@ -263,13 +461,19 @@ function filtrarComandas() {
|
|||||||
|
|
||||||
// ─── Modal de Itens (Novo Layout PDV Split) ───────────────────────────────────
|
// ─── Modal de Itens (Novo Layout PDV Split) ───────────────────────────────────
|
||||||
async function abrirItensComanda(comandaIdOrObj) {
|
async function abrirItensComanda(comandaIdOrObj) {
|
||||||
let comanda = typeof comandaIdOrObj === 'object' ? comandaIdOrObj : _comandasData.find(c => c.id === comandaIdOrObj);
|
let comanda = typeof comandaIdOrObj === 'object' ? comandaIdOrObj : _comandasData.find(c => String(c.id) === String(comandaIdOrObj));
|
||||||
|
|
||||||
|
if (!comanda) return showToast('Comanda não encontrada.', 'error');
|
||||||
|
|
||||||
|
console.log(`[PDV] Abrindo comanda #${comanda.id}:`, comanda);
|
||||||
|
console.log(`[PDV] Itens da comanda #${comanda.id}:`, comanda.items || []);
|
||||||
|
|
||||||
const ativa = comanda.status === 'OPEN' || comanda.status === 'PAYING';
|
const ativa = comanda.status === 'OPEN' || comanda.status === 'PAYING';
|
||||||
const podeAdd = comanda.status === 'OPEN'; // Só permite add se ainda não estiver pagando?
|
const podeAdd = comanda.status === 'OPEN';
|
||||||
|
|
||||||
|
|
||||||
// Carrega produtos (ativos)
|
// Carrega produtos (ativos)
|
||||||
const pRes = await window.electronAPI.get('/products/');
|
const pRes = await window.electronAPI.get('/products');
|
||||||
let todosProdutos = pRes.ok ? pRes.data.filter(p => p.active) : [];
|
let todosProdutos = pRes.ok ? pRes.data.filter(p => p.active) : [];
|
||||||
|
|
||||||
openModal({
|
openModal({
|
||||||
@@ -292,16 +496,32 @@ async function abrirItensComanda(comandaIdOrObj) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>`,
|
</div>`,
|
||||||
footer: `<button class="btn btn-secondary btn-md" onclick="closeModal()">Sair do PDV</button>`,
|
footer: `
|
||||||
|
<button class="btn btn-secondary btn-md" id="btn-pdv-imprimir">🖨️ Imprimir</button>
|
||||||
|
<button class="btn btn-secondary btn-md" onclick="closeModal()">Sair do PDV</button>
|
||||||
|
`,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Funções internas de renderização
|
// Carrega pagamentos específicos desta comanda
|
||||||
|
let pagamentosComanda = _paymentsMap[comanda.id] || [];
|
||||||
|
if (!pagamentosComanda.length) {
|
||||||
|
const pagsRes = await window.electronAPI.get('/payments');
|
||||||
|
if (pagsRes.ok) {
|
||||||
|
pagamentosComanda = (pagsRes.data || []).filter(p => String(p.comanda) === String(comanda.id));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calcula totais fora do renderLeft para uso na impressão
|
||||||
|
const totalComanda = (comanda.items || []).reduce((acc, it) => acc + (_productsMap[String(it.product)] || 0), 0);
|
||||||
|
|
||||||
const renderLeft = () => {
|
const renderLeft = () => {
|
||||||
const container = document.getElementById('pdv-items-list');
|
const container = document.getElementById('pdv-items-list');
|
||||||
if (!container) return;
|
if (!container) return;
|
||||||
|
|
||||||
const itens = comanda.items || [];
|
const itens = comanda.items || [];
|
||||||
const totalComanda = itens.reduce((acc, it) => acc + (_productsMap[it.product] || 0), 0);
|
const totalPago = pagamentosComanda.reduce((acc, p) => acc + parseFloat(p.value || 0), 0);
|
||||||
|
const valorRestante = totalComanda - totalPago;
|
||||||
|
const temPagamentos = pagamentosComanda.length > 0;
|
||||||
|
|
||||||
container.innerHTML = `
|
container.innerHTML = `
|
||||||
<div style="flex:1; overflow-y: auto;">
|
<div style="flex:1; overflow-y: auto;">
|
||||||
@@ -315,17 +535,17 @@ async function abrirItensComanda(comandaIdOrObj) {
|
|||||||
</tr></thead>
|
</tr></thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
${itens.map(it => {
|
${itens.map(it => {
|
||||||
const prod = todosProdutos.find(p => p.id === it.product);
|
const prod = todosProdutos.find(p => String(p.id) === String(it.product));
|
||||||
const isCuisine = prod?.cuisine || false;
|
const isCuisine = prod?.cuisine || false;
|
||||||
const tooltip = it.obs ? `title="${it.obs}"` : '';
|
const tooltip = it.obs ? `title="${it.obs}"` : '';
|
||||||
|
|
||||||
return `
|
return `
|
||||||
<tr data-item-id="${it.id}">
|
<tr data-item-id="${it.id}">
|
||||||
<td style="padding:10px 0;border-bottom:1px solid var(--border)" ${tooltip}>
|
<td style="padding:10px 0;border-bottom:1px solid var(--border)" ${tooltip}>
|
||||||
${it.product_name}
|
${prod?.name || it.product_name || `Produto #${it.product}`}
|
||||||
</td>
|
</td>
|
||||||
<td style="padding:10px 0;text-align:right;border-bottom:1px solid var(--border)">
|
<td style="padding:10px 0;text-align:right;border-bottom:1px solid var(--border)">
|
||||||
R$ ${(_productsMap[it.product] || 0).toFixed(2)}
|
R$ ${(_productsMap[String(it.product)] || 0).toFixed(2)}
|
||||||
</td>
|
</td>
|
||||||
${podeAdd ? `
|
${podeAdd ? `
|
||||||
<td style="padding:10px 0;text-align:center;border-bottom:1px solid var(--border)">
|
<td style="padding:10px 0;text-align:center;border-bottom:1px solid var(--border)">
|
||||||
@@ -344,16 +564,50 @@ async function abrirItensComanda(comandaIdOrObj) {
|
|||||||
}).join('')}
|
}).join('')}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
` : `<p style="padding:40px 0;text-align:center;color:var(--text-muted)">Nenhum item adicionado.</p>`}
|
` : `<p style="padding:20px 0;text-align:center;color:var(--text-muted)">Nenhum item adicionado.</p>`}
|
||||||
|
|
||||||
|
${temPagamentos ? `
|
||||||
|
<h4 style="margin:20px 0 12px 0;color:var(--text-secondary);font-size:0.8rem;text-transform:uppercase;border-top:1px solid var(--border);padding-top:16px">Pagamentos Recebidos</h4>
|
||||||
|
<table style="width:100%;font-size:0.85rem">
|
||||||
|
<thead><tr style="color:var(--text-muted);border-bottom:1px solid var(--border)">
|
||||||
|
<th style="text-align:left;padding:6px 0">Forma</th>
|
||||||
|
<th style="text-align:right;padding:6px 0">Valor</th>
|
||||||
|
</tr></thead>
|
||||||
|
<tbody>
|
||||||
|
${pagamentosComanda.map(p => {
|
||||||
|
const tipoNome = _paymentTypes.find(t => String(t.id) === String(p.type_pay))?.name || _paymentTypes.find(t => String(t.id) === String(p.type_pay))?.nome || '–';
|
||||||
|
return `
|
||||||
|
<tr>
|
||||||
|
<td style="padding:8px 0;border-bottom:1px solid var(--border);color:var(--success)">
|
||||||
|
💰 ${tipoNome}
|
||||||
|
</td>
|
||||||
|
<td style="padding:8px 0;text-align:right;border-bottom:1px solid var(--border);color:var(--success)">
|
||||||
|
R$ ${parseFloat(p.value || 0).toFixed(2)}
|
||||||
|
</td>
|
||||||
|
</tr>`;
|
||||||
|
}).join('')}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
` : ''}
|
||||||
</div>
|
</div>
|
||||||
<div style="padding-top:20px;margin-top:auto;border-top:2px solid var(--border)">
|
<div style="padding-top:20px;margin-top:auto;border-top:2px solid var(--border)">
|
||||||
<div style="display:flex;justify-content:space-between;margin-bottom:6px">
|
<div style="display:flex;justify-content:space-between;margin-bottom:6px">
|
||||||
<span style="color:var(--text-secondary)">Total de Itens:</span>
|
<span style="color:var(--text-secondary)">Total de Itens:</span>
|
||||||
<strong>${itens.length}</strong>
|
<strong>${itens.length}</strong>
|
||||||
</div>
|
</div>
|
||||||
|
<div style="display:flex;justify-content:space-between;margin-bottom:4px">
|
||||||
|
<span style="color:var(--text-secondary)">Total da Conta:</span>
|
||||||
|
<strong style="color:${temPagamentos ? 'var(--text-muted);text-decoration:line-through' : 'var(--success)'}">R$ ${totalComanda.toFixed(2)}</strong>
|
||||||
|
</div>
|
||||||
|
${temPagamentos ? `
|
||||||
|
<div style="display:flex;justify-content:space-between;margin-bottom:6px">
|
||||||
|
<span style="color:var(--text-secondary)">Valor Pago:</span>
|
||||||
|
<strong style="color:var(--success)">R$ ${totalPago.toFixed(2)}</strong>
|
||||||
|
</div>
|
||||||
|
` : ''}
|
||||||
<div style="display:flex;justify-content:space-between;margin-bottom:10px">
|
<div style="display:flex;justify-content:space-between;margin-bottom:10px">
|
||||||
<span style="color:var(--text-secondary);font-size:1.1rem">Total da Conta:</span>
|
<span style="color:var(--text-secondary);font-size:1.1rem">${temPagamentos ? 'Restante:' : 'Total:'}</span>
|
||||||
<strong style="color:var(--success);font-size:1.3rem">R$ ${totalComanda.toFixed(2)}</strong>
|
<strong style="color:${valorRestante <= 0 ? 'var(--success)' : 'var(--warning)'};font-size:1.3rem">R$ ${Math.max(0, valorRestante).toFixed(2)}</strong>
|
||||||
</div>
|
</div>
|
||||||
${ativa ? `
|
${ativa ? `
|
||||||
<div style="display:grid; grid-template-columns: 1fr 1fr; gap: 10px; margin-top: 10px;">
|
<div style="display:grid; grid-template-columns: 1fr 1fr; gap: 10px; margin-top: 10px;">
|
||||||
@@ -368,7 +622,7 @@ async function abrirItensComanda(comandaIdOrObj) {
|
|||||||
btn.addEventListener('click', async () => {
|
btn.addEventListener('click', async () => {
|
||||||
const itemId = btn.dataset.id;
|
const itemId = btn.dataset.id;
|
||||||
if (confirm('Deseja realmente excluir este item da comanda?')) {
|
if (confirm('Deseja realmente excluir este item da comanda?')) {
|
||||||
const r = await window.electronAPI.delete(`/items-comanda/${itemId}/`);
|
const r = await window.electronAPI.delete(`/items-comanda/${itemId}`);
|
||||||
if (r.ok) {
|
if (r.ok) {
|
||||||
showToast('Item excluído!', 'success');
|
showToast('Item excluído!', 'success');
|
||||||
comanda.items = comanda.items.filter(it => it.id !== parseInt(itemId));
|
comanda.items = comanda.items.filter(it => it.id !== parseInt(itemId));
|
||||||
@@ -384,34 +638,104 @@ async function abrirItensComanda(comandaIdOrObj) {
|
|||||||
// Listeners de edição de observação
|
// Listeners de edição de observação
|
||||||
container.querySelectorAll('.btn-edit-obs').forEach(btn => {
|
container.querySelectorAll('.btn-edit-obs').forEach(btn => {
|
||||||
btn.addEventListener('click', async () => {
|
btn.addEventListener('click', async () => {
|
||||||
const itemId = parseInt(btn.dataset.id);
|
const itemId = String(btn.dataset.id);
|
||||||
const item = comanda.items.find(it => it.id === itemId);
|
const item = comanda.items.find(it => String(it.id) === itemId);
|
||||||
const prod = todosProdutos.find(p => p.id === item.product);
|
const prod = todosProdutos.find(p => String(p.id) === String(item?.product));
|
||||||
|
|
||||||
if (item && prod) {
|
if (item && prod) {
|
||||||
window.abrirModalObsCozinhaGlobal(prod.name, item.obs, async (novaObs) => {
|
window.abrirModalObsCozinhaGlobal(prod.name, item.obs || '', async (novaObs) => {
|
||||||
if (novaObs === null) return;
|
if (novaObs === null || novaObs === item.obs) return;
|
||||||
const r = await window.electronAPI.patch(`/items-comanda/${itemId}/`, { obs: novaObs });
|
|
||||||
if (r.ok) {
|
// Busca os pedidos para encontrar o ID da order vinculada
|
||||||
showToast('Observação atualizada!', 'success');
|
const ordersRes = await window.electronAPI.get('/orders');
|
||||||
item.obs = novaObs;
|
if (ordersRes.ok) {
|
||||||
renderLeft();
|
const linkedOrder = ordersRes.data.find(o => String(o.productComanda) === itemId);
|
||||||
loadComandas(_mesasRef);
|
|
||||||
} else {
|
if (linkedOrder) {
|
||||||
showToast(r.error, 'error');
|
// PATCH na Order (Cozinha)
|
||||||
|
const r = await window.electronAPI.patch(`/orders/${linkedOrder.id}`, { obs: novaObs });
|
||||||
|
if (r.ok) {
|
||||||
|
item.obs = novaObs; // Atualiza local
|
||||||
|
showToast('Observação enviada para a cozinha!', 'success');
|
||||||
|
renderLeft();
|
||||||
|
} else {
|
||||||
|
showToast('Erro ao atualizar na cozinha.', 'error');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Se não achou a order, tenta atualizar só o item principal como fallback
|
||||||
|
await window.electronAPI.patch(`/items-comanda/${itemId}`, { obs: novaObs });
|
||||||
|
item.obs = novaObs;
|
||||||
|
renderLeft();
|
||||||
|
showToast('Observação salva (item sem vínculo cozinha).', 'info');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Listeners do rodapé do PDV (Receber, Excluir e Imprimir)
|
||||||
|
const bRec = document.getElementById('btn-pdv-receber');
|
||||||
|
if (bRec) {
|
||||||
|
bRec.onclick = () => {
|
||||||
|
closeModal();
|
||||||
|
setTimeout(() => abrirModalPagamento(comanda, () => {
|
||||||
|
setTimeout(() => abrirItensComanda(comanda), 300);
|
||||||
|
}), 300);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const bImp = document.getElementById('btn-pdv-imprimir');
|
||||||
|
if (bImp) {
|
||||||
|
bImp.onclick = () => imprimirComanda(comanda, pagamentosComanda, totalComanda);
|
||||||
|
}
|
||||||
|
|
||||||
|
const bExc = document.getElementById('btn-pdv-excluir');
|
||||||
|
if (bExc) {
|
||||||
|
bExc.onclick = async () => {
|
||||||
|
if (confirm('Deseja realmente EXCLUIR/APAGAR esta comanda?')) {
|
||||||
|
const r = await window.electronAPI.post(`/comandas/${comanda.id}/apagar`, {});
|
||||||
|
if (r.ok) {
|
||||||
|
showToast('Comanda excluída!', 'success');
|
||||||
|
closeModal();
|
||||||
|
loadComandas(_mesasRef);
|
||||||
|
} else {
|
||||||
|
showToast(r.error, 'error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const processarResultadoAdd = (r) => {
|
const processarResultadoAdd = async (r, prod, obs = '') => {
|
||||||
if (r.ok) {
|
if (r.ok) {
|
||||||
if (!comanda.items) comanda.items = [];
|
if (!comanda.items) comanda.items = [];
|
||||||
comanda.items.push(r.data);
|
|
||||||
|
const novoItem = r.data;
|
||||||
|
// Normaliza o campo product caso o servidor retorne product_id
|
||||||
|
if (!novoItem.product && novoItem.product_id) novoItem.product = novoItem.product_id;
|
||||||
|
|
||||||
|
comanda.items.push(novoItem);
|
||||||
renderLeft();
|
renderLeft();
|
||||||
loadComandas(_mesasRef);
|
loadComandas(_mesasRef);
|
||||||
|
|
||||||
|
// Se o produto for de cozinha, cria a order na nova rota
|
||||||
|
if (prod && prod.cuisine) {
|
||||||
|
const orderPayload = {
|
||||||
|
productComanda: novoItem.id, // ID do item vinculado
|
||||||
|
id_product: prod.id,
|
||||||
|
id_comanda: comanda.id,
|
||||||
|
obs: obs || novoItem.obs || ''
|
||||||
|
};
|
||||||
|
|
||||||
|
console.log('[PDV] Criando pedido na cozinha:', orderPayload);
|
||||||
|
const orderRes = await window.electronAPI.post('/orders', orderPayload);
|
||||||
|
if (orderRes.ok) {
|
||||||
|
showToast('Pedido enviado para a cozinha!', 'success');
|
||||||
|
} else {
|
||||||
|
showToast('Item adicionado, mas falhou ao enviar para cozinha.', 'warning');
|
||||||
|
}
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
showToast(r.error, 'error');
|
showToast(r.error, 'error');
|
||||||
}
|
}
|
||||||
@@ -421,25 +745,34 @@ async function abrirItensComanda(comandaIdOrObj) {
|
|||||||
container.querySelectorAll('.pdv-product-card').forEach(card => {
|
container.querySelectorAll('.pdv-product-card').forEach(card => {
|
||||||
card.addEventListener('click', async () => {
|
card.addEventListener('click', async () => {
|
||||||
if (!podeAdd) return showToast('Comanda em fechamento ou fechada.', 'warning');
|
if (!podeAdd) return showToast('Comanda em fechamento ou fechada.', 'warning');
|
||||||
const pId = parseInt(card.dataset.id);
|
const pId = String(card.dataset.id);
|
||||||
const prod = todosProdutos.find(x => x.id === pId);
|
const prod = todosProdutos.find(x => String(x.id) === pId);
|
||||||
|
|
||||||
card.style.transform = 'scale(0.95)';
|
card.style.transform = 'scale(0.95)';
|
||||||
setTimeout(() => card.style.transform = '', 100);
|
setTimeout(() => card.style.transform = '', 100);
|
||||||
|
|
||||||
|
if (!prod) return showToast('Erro: Produto não encontrado.', 'error');
|
||||||
|
|
||||||
if (prod.cuisine) {
|
if (prod.cuisine) {
|
||||||
window.abrirModalObsCozinhaGlobal(prod.name, '', async (obs) => {
|
window.abrirModalObsCozinhaGlobal(prod.name, '', async (obs) => {
|
||||||
if (obs === null) return;
|
if (obs === null) return;
|
||||||
const r = await window.electronAPI.post('/items-comanda/', {
|
const loggedUser = await window.electronAPI.getUser();
|
||||||
|
const r = await window.electronAPI.post('/items-comanda', {
|
||||||
comanda: comanda.id,
|
comanda: comanda.id,
|
||||||
product: pId,
|
product: prod.id,
|
||||||
obs: obs
|
obs: obs,
|
||||||
|
applicant: loggedUser?.username || 'Sistema'
|
||||||
});
|
});
|
||||||
processarResultadoAdd(r);
|
processarResultadoAdd(r, prod, obs);
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
const r = await window.electronAPI.post('/items-comanda/', { comanda: comanda.id, product: pId });
|
const loggedUser = await window.electronAPI.getUser();
|
||||||
processarResultadoAdd(r);
|
const r = await window.electronAPI.post('/items-comanda', {
|
||||||
|
comanda: comanda.id,
|
||||||
|
product: prod.id,
|
||||||
|
applicant: loggedUser?.username || 'Sistema'
|
||||||
|
});
|
||||||
|
processarResultadoAdd(r, prod);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -451,7 +784,7 @@ async function abrirItensComanda(comandaIdOrObj) {
|
|||||||
|
|
||||||
const filtrados = todosProdutos.filter(p => !filtro || p.name.toLowerCase().includes(filtro.toLowerCase())).slice(0, 20);
|
const filtrados = todosProdutos.filter(p => !filtro || p.name.toLowerCase().includes(filtro.toLowerCase())).slice(0, 20);
|
||||||
|
|
||||||
// console.log('Produtos carregados no PDV:', todosProdutos);
|
// console.log('Produtos carregados no PDV:', todosProdutos);
|
||||||
container.innerHTML = filtrados.map(p => {
|
container.innerHTML = filtrados.map(p => {
|
||||||
const imgTarget = p.image ? `url('${p.image}')` : `url('https://wallpapers.com/images/featured/fundo-abstrato-escuro-27kvn4ewpldsngbu.jpg')`;
|
const imgTarget = p.image ? `url('${p.image}')` : `url('https://wallpapers.com/images/featured/fundo-abstrato-escuro-27kvn4ewpldsngbu.jpg')`;
|
||||||
return `
|
return `
|
||||||
@@ -479,67 +812,87 @@ async function abrirItensComanda(comandaIdOrObj) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// ─── Modal Nova Comanda ───────────────────────────────────────────────────────
|
// ─── Modal Comanda (Nova / Editar) ──────────────────────────────────────────
|
||||||
function abrirModalNovaComanda(mesas) {
|
function abrirModalNovaComanda(mesas, comandaExistente = null) {
|
||||||
|
const isEdit = !!comandaExistente;
|
||||||
|
|
||||||
openModal({
|
openModal({
|
||||||
title: 'Nova Comanda',
|
title: isEdit ? `Editar Comanda #${comandaExistente.id}` : 'Nova Comanda',
|
||||||
body: `
|
body: `
|
||||||
<form id="form-nova-comanda" class="form-grid">
|
<form id="form-comanda" class="form-grid">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>Nome do Cliente / Identificação</label>
|
<label>Nome do Cliente / Identificação</label>
|
||||||
<input type="text" id="comanda-nome" class="form-control" placeholder="Ex: João, Mesa do fundo..." autofocus required />
|
<input type="text" id="comanda-nome" class="form-control" placeholder="Ex: João, Mesa do fundo..." value="${isEdit ? (comandaExistente.name || '') : ''}" autofocus required />
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>Mesa</label>
|
<label>Mesa</label>
|
||||||
<select id="comanda-mesa" class="form-control">
|
<select id="comanda-mesa" class="form-control">
|
||||||
${mesas.map(m => `<option value="${m.id}">${m.nome || m.name || `Mesa ${m.numero || m.number || m.id}`}</option>`).join('')}
|
${mesas.map(m => `<option value="${m.id}" ${isEdit && String(comandaExistente.mesa) === String(m.id) ? 'selected' : ''}>${m.nome || m.name || `Mesa ${m.numero || m.number || m.id}`}</option>`).join('')}
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<button type="submit" style="display:none"></button> <!-- Invisível para permitir Enter -->
|
<div class="form-group">
|
||||||
|
<label>Relacionar Cliente (Opcional)</label>
|
||||||
|
<select id="comanda-cliente" class="form-control">
|
||||||
|
<option value="">Nenhum Cliente</option>
|
||||||
|
${_clients.filter(cl => cl.active !== false).map(cl => `
|
||||||
|
<option value="${cl.id}" ${isEdit && String(comandaExistente.client) === String(cl.id) ? 'selected' : ''}>
|
||||||
|
${cl.name}
|
||||||
|
</option>
|
||||||
|
`).join('')}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<button type="submit" style="display:none"></button>
|
||||||
</form>`,
|
</form>`,
|
||||||
footer: `
|
footer: `
|
||||||
<button class="btn btn-secondary btn-md" onclick="closeModal()">Cancelar</button>
|
<button class="btn btn-secondary btn-md" onclick="closeModal()">Cancelar</button>
|
||||||
<button class="btn btn-primary btn-md" id="btn-criar-comanda">Criar e Adicionar Itens</button>`,
|
<button class="btn btn-primary btn-md" id="btn-salvar-comanda">${isEdit ? 'Salvar Alterações' : 'Criar e Adicionar Itens'}</button>`,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Foco manual caso autofocus falhe em algum navegador
|
|
||||||
setTimeout(() => document.getElementById('comanda-nome')?.focus(), 100);
|
setTimeout(() => document.getElementById('comanda-nome')?.focus(), 100);
|
||||||
|
|
||||||
const submeter = async (e) => {
|
const submeter = async (e) => {
|
||||||
if (e) e.preventDefault();
|
if (e) e.preventDefault();
|
||||||
const mesaId = parseInt(document.getElementById('comanda-mesa').value);
|
const mesaId = parseInt(document.getElementById('comanda-mesa').value);
|
||||||
|
const clientId = document.getElementById('comanda-cliente').value ? parseInt(document.getElementById('comanda-cliente').value) : null;
|
||||||
const nome = document.getElementById('comanda-nome').value.trim();
|
const nome = document.getElementById('comanda-nome').value.trim();
|
||||||
|
|
||||||
if (!nome) return showToast('Informe o nome ou identificação.', 'error');
|
if (!nome) return showToast('Informe o nome ou identificação.', 'error');
|
||||||
|
|
||||||
|
const btn = document.getElementById('btn-salvar-comanda');
|
||||||
|
btn.disabled = true;
|
||||||
|
btn.textContent = isEdit ? 'Salvando...' : 'Criando...';
|
||||||
|
|
||||||
const loggedUser = await window.electronAPI.getUser();
|
const loggedUser = await window.electronAPI.getUser();
|
||||||
const payload = {
|
const payload = {
|
||||||
name: nome,
|
|
||||||
mesa: mesaId,
|
mesa: mesaId,
|
||||||
user: loggedUser?.id || 1,
|
user: loggedUser?.id || 1,
|
||||||
status: 'OPEN'
|
client: clientId,
|
||||||
|
name: nome,
|
||||||
};
|
};
|
||||||
|
|
||||||
const btn = document.getElementById('btn-criar-comanda');
|
let r;
|
||||||
btn.disabled = true;
|
if (isEdit) {
|
||||||
btn.textContent = 'Criando...';
|
r = await window.electronAPI.patch(`/comandas/${comandaExistente.id}`, payload);
|
||||||
|
} else {
|
||||||
|
r = await window.electronAPI.post('/comandas', payload);
|
||||||
|
}
|
||||||
|
|
||||||
const r = await window.electronAPI.post('/comandas/', payload);
|
|
||||||
if (r.ok) {
|
if (r.ok) {
|
||||||
showToast('Comanda criada!', 'success');
|
showToast(isEdit ? 'Comanda atualizada!' : 'Comanda criada!', 'success');
|
||||||
closeModal();
|
closeModal();
|
||||||
loadComandas(_mesasRef);
|
loadComandas(_mesasRef);
|
||||||
// Abre direto a modal de itens da comanda recém criada
|
if (!isEdit) {
|
||||||
setTimeout(() => abrirItensComanda(r.data), 300);
|
setTimeout(() => abrirItensComanda(r.data), 300);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
showToast(r.error, 'error');
|
showToast(r.error, 'error');
|
||||||
btn.disabled = false;
|
btn.disabled = false;
|
||||||
btn.textContent = 'Criar e Adicionar Itens';
|
btn.textContent = isEdit ? 'Salvar Alterações' : 'Criar e Adicionar Itens';
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
document.getElementById('form-nova-comanda').onsubmit = submeter;
|
document.getElementById('form-comanda').onsubmit = submeter;
|
||||||
document.getElementById('btn-criar-comanda').onclick = submeter;
|
document.getElementById('btn-salvar-comanda').onclick = submeter;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── Helpers ──────────────────────────────────────────────────────────────────
|
// ─── Helpers ──────────────────────────────────────────────────────────────────
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
export async function renderConfig(container) {
|
export async function renderConfig(container) {
|
||||||
const currentUrl = await window.electronAPI.getConfigUrl();
|
const currentUrl = await window.electronAPI.getConfigUrl();
|
||||||
|
const printSilent = await window.electronAPI.getPrintSilent();
|
||||||
|
|
||||||
container.innerHTML = `
|
container.innerHTML = `
|
||||||
<div class="page-header">
|
<div class="page-header">
|
||||||
@@ -26,6 +27,24 @@ export async function renderConfig(container) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="card" style="max-width: 600px; margin-top: 20px;">
|
||||||
|
<h3 style="margin-bottom: 20px; color: var(--text-primary);">🖨️ Impressão</h3>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label style="display: flex; align-items: center; gap: 10px; cursor: pointer;">
|
||||||
|
<input type="checkbox" id="config-print-silent" ${printSilent ? 'checked' : ''} style="width: 18px; height: 18px;" />
|
||||||
|
<span><strong>Impressão direta (sem diálogo)</strong></span>
|
||||||
|
</label>
|
||||||
|
<p style="font-size: 0.75rem; color: var(--text-muted); margin-top: 8px;">
|
||||||
|
Quando ativado, a impressão é enviada diretamente para a impressora padrão sem mostrar o diálogo do sistema.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="margin-top: 15px; display: flex; gap: 10px;">
|
||||||
|
<button class="btn btn-primary btn-md" id="btn-save-print-config">💾 Salvar</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="card" style="max-width: 600px; margin-top: 20px; border-left: 4px solid var(--primary);">
|
<div class="card" style="max-width: 600px; margin-top: 20px; border-left: 4px solid var(--primary);">
|
||||||
<h3 style="margin-bottom: 10px; color: var(--text-primary);">ℹ️ Sobre o Sistema</h3>
|
<h3 style="margin-bottom: 10px; color: var(--text-primary);">ℹ️ Sobre o Sistema</h3>
|
||||||
<p style="color: var(--text-secondary); font-size: 0.85rem;">
|
<p style="color: var(--text-secondary); font-size: 0.85rem;">
|
||||||
@@ -36,7 +55,7 @@ export async function renderConfig(container) {
|
|||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
// Salvar
|
// Salvar config de API
|
||||||
document.getElementById('btn-save-config').addEventListener('click', async () => {
|
document.getElementById('btn-save-config').addEventListener('click', async () => {
|
||||||
const newUrl = document.getElementById('config-api-url').value.trim();
|
const newUrl = document.getElementById('config-api-url').value.trim();
|
||||||
if (!newUrl.startsWith('http')) {
|
if (!newUrl.startsWith('http')) {
|
||||||
@@ -51,11 +70,22 @@ export async function renderConfig(container) {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Restaurar
|
// Restaurar URL
|
||||||
document.getElementById('btn-reset-url').addEventListener('click', async () => {
|
document.getElementById('btn-reset-url').addEventListener('click', async () => {
|
||||||
const defaultUrl = 'http://localhost:8000/api/v1';
|
const defaultUrl = 'http://localhost:8000/api/v1';
|
||||||
document.getElementById('config-api-url').value = defaultUrl;
|
document.getElementById('config-api-url').value = defaultUrl;
|
||||||
await window.electronAPI.setConfigUrl(defaultUrl);
|
await window.electronAPI.setConfigUrl(defaultUrl);
|
||||||
showToast('URL restaurada para o padrão localhost.', 'info');
|
showToast('URL restaurada para o padrão localhost.', 'info');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Salvar config de impressão
|
||||||
|
document.getElementById('btn-save-print-config').addEventListener('click', async () => {
|
||||||
|
const silent = document.getElementById('config-print-silent').checked;
|
||||||
|
const r = await window.electronAPI.setPrintSilent(silent);
|
||||||
|
if (r.ok) {
|
||||||
|
showToast(silent ? 'Impressão direta ativada!' : 'Impressão direta desativada.', 'success');
|
||||||
|
} else {
|
||||||
|
showToast('Erro ao salvar configuração.', 'error');
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,8 +28,8 @@ async function loadMesas() {
|
|||||||
|
|
||||||
// Carrega mesas e comandas em paralelo para determinar ocupação
|
// Carrega mesas e comandas em paralelo para determinar ocupação
|
||||||
const [mesasRes, comandasRes] = await Promise.all([
|
const [mesasRes, comandasRes] = await Promise.all([
|
||||||
window.electronAPI.get('/mesas/'),
|
window.electronAPI.get('/mesas'),
|
||||||
window.electronAPI.get('/comandas/'),
|
window.electronAPI.get('/comandas'),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
if (!mesasRes.ok) {
|
if (!mesasRes.ok) {
|
||||||
@@ -103,7 +103,7 @@ function abrirDetalheMesa(mesa, ocupada) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
document.getElementById('btn-del-mesa').addEventListener('click', async () => {
|
document.getElementById('btn-del-mesa').addEventListener('click', async () => {
|
||||||
const r = await window.electronAPI.delete(`/mesas/${mesa.id}/`);
|
const r = await window.electronAPI.delete(`/mesas/${mesa.id}`);
|
||||||
if (r.ok) { showToast('Mesa excluída!', 'success'); closeModal(); loadMesas(); }
|
if (r.ok) { showToast('Mesa excluída!', 'success'); closeModal(); loadMesas(); }
|
||||||
else showToast(r.error, 'error');
|
else showToast(r.error, 'error');
|
||||||
});
|
});
|
||||||
@@ -145,8 +145,8 @@ function abrirModalMesa(mesa = null) {
|
|||||||
if (!data.name) return showToast('Informe o nome da mesa.', 'error');
|
if (!data.name) return showToast('Informe o nome da mesa.', 'error');
|
||||||
|
|
||||||
const r = isEdit
|
const r = isEdit
|
||||||
? await window.electronAPI.put(`/mesas/${mesa.id}/`, data)
|
? await window.electronAPI.put(`/mesas/${mesa.id}`, data)
|
||||||
: await window.electronAPI.post('/mesas/', data);
|
: await window.electronAPI.post('/mesas', data);
|
||||||
|
|
||||||
if (r.ok) { showToast(isEdit ? 'Mesa atualizada!' : 'Mesa criada!', 'success'); closeModal(); loadMesas(); }
|
if (r.ok) { showToast(isEdit ? 'Mesa atualizada!' : 'Mesa criada!', 'success'); closeModal(); loadMesas(); }
|
||||||
else showToast(r.error, 'error');
|
else showToast(r.error, 'error');
|
||||||
|
|||||||
@@ -1,12 +1,11 @@
|
|||||||
export async function renderPagamentos(container) {
|
export async function renderPagamentos(container) {
|
||||||
// Carrega tipos de pagamento para o formulário de novo registro
|
// Carrega tipos de pagamento para o formulário de novo registro
|
||||||
let tiposPag = [], comandas = [];
|
|
||||||
const [tRes, cRes] = await Promise.all([
|
const [tRes, cRes] = await Promise.all([
|
||||||
window.electronAPI.get('/payment-types/'),
|
window.electronAPI.get('/payment-types'),
|
||||||
window.electronAPI.get('/comandas/'),
|
window.electronAPI.get('/comandas'),
|
||||||
]);
|
]);
|
||||||
if (tRes.ok) tiposPag = tRes.data;
|
const tiposPag = tRes.ok ? tRes.data : [];
|
||||||
if (cRes.ok) comandas = cRes.data;
|
const comandas = cRes.ok ? cRes.data : [];
|
||||||
|
|
||||||
container.innerHTML = `
|
container.innerHTML = `
|
||||||
<div class="page-header">
|
<div class="page-header">
|
||||||
@@ -42,13 +41,22 @@ async function loadPagamentos(tiposPag, comandas) {
|
|||||||
const wrap = document.getElementById('pagamentos-table');
|
const wrap = document.getElementById('pagamentos-table');
|
||||||
if (!wrap) return;
|
if (!wrap) return;
|
||||||
wrap.innerHTML = `<div class="loading-screen"><div class="spinner"></div></div>`;
|
wrap.innerHTML = `<div class="loading-screen"><div class="spinner"></div></div>`;
|
||||||
const res = await window.electronAPI.get('/payments/');
|
const res = await window.electronAPI.get('/payments');
|
||||||
if (!res.ok) { wrap.innerHTML = `<div class="table-empty">Erro ao carregar pagamentos.</div>`; return; }
|
if (!res.ok) { wrap.innerHTML = `<div class="table-empty">Erro ao carregar pagamentos.</div>`; return; }
|
||||||
_pagsData = res.data;
|
|
||||||
renderPagsTable(_pagsData);
|
// Ordena decrescente (mais novos primeiro)
|
||||||
|
_pagsData = (res.data || []).sort((a, b) => b.id - a.id);
|
||||||
|
|
||||||
|
// Cria mapa de comandas para consulta rápida
|
||||||
|
const cmdMap = (comandas || []).reduce((acc, c) => {
|
||||||
|
acc[String(c.id)] = `${c.name || 'Sem nome'} (${c.mesa_name || `Mesa ${c.mesa}`})`;
|
||||||
|
return acc;
|
||||||
|
}, {});
|
||||||
|
|
||||||
|
renderPagsTable(_pagsData, cmdMap);
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderPagsTable(data) {
|
function renderPagsTable(data, cmdMap = {}) {
|
||||||
const wrap = document.getElementById('pagamentos-table');
|
const wrap = document.getElementById('pagamentos-table');
|
||||||
if (!wrap) return;
|
if (!wrap) return;
|
||||||
|
|
||||||
@@ -73,25 +81,29 @@ function renderPagsTable(data) {
|
|||||||
<th>Ações</th>
|
<th>Ações</th>
|
||||||
</tr></thead>
|
</tr></thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
${data.map(p => `<tr>
|
${data.map(p => {
|
||||||
<td style="color:var(--text-muted)">#${p.id}</td>
|
const cDesc = cmdMap[String(p.comanda)] || p.comanda_name || '–';
|
||||||
<td>${p.client_name || '–'}</td>
|
return `
|
||||||
<td>
|
<tr>
|
||||||
${p.comanda ? `<span style="font-size:0.8rem">
|
<td style="color:var(--text-muted)">#${p.id}</td>
|
||||||
<span style="color:var(--text-muted)">#${p.comanda}</span>
|
<td>${p.client_name || '–'}</td>
|
||||||
${p.comanda_name ? `<span style="color:var(--text-secondary)"> ${p.comanda_name}</span>` : ''}
|
<td>
|
||||||
</span>` : '–'}
|
${p.comanda ? `<span style="font-size:0.8rem">
|
||||||
</td>
|
<span style="color:var(--text-muted)">#${p.comanda}</span>
|
||||||
<td><span class="badge badge-info">${p.type_pay_name || '–'}</span></td>
|
<span style="color:var(--text-secondary)"> ${cDesc}</span>
|
||||||
<td><strong style="color:var(--success)">R$ ${parseFloat(p.value || 0).toFixed(2)}</strong></td>
|
</span>` : '–'}
|
||||||
<td style="max-width:180px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--text-secondary);font-size:0.82rem">
|
</td>
|
||||||
${p.description || '–'}
|
<td><span class="badge badge-info">${p.type_pay_name || '–'}</span></td>
|
||||||
</td>
|
<td><strong style="color:var(--success)">R$ ${parseFloat(p.value || 0).toFixed(2)}</strong></td>
|
||||||
<td style="white-space:nowrap;font-size:0.82rem">${formatDate(p.datetime)}</td>
|
<td style="max-width:180px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--text-secondary);font-size:0.82rem">
|
||||||
<td>
|
${p.description || '–'}
|
||||||
<button class="btn btn-danger btn-sm btn-del-pag" data-id="${p.id}">Excluir</button>
|
</td>
|
||||||
</td>
|
<td style="white-space:nowrap;font-size:0.82rem">${formatDate(p.datetime)}</td>
|
||||||
</tr>`).join('')}
|
<td>
|
||||||
|
<button class="btn btn-danger btn-sm btn-del-pag" data-id="${p.id}">Excluir</button>
|
||||||
|
</td>
|
||||||
|
</tr>`;
|
||||||
|
}).join('')}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
<div style="padding:14px 20px;border-top:1px solid var(--border);display:flex;justify-content:flex-end;align-items:center;gap:8px">
|
<div style="padding:14px 20px;border-top:1px solid var(--border);display:flex;justify-content:flex-end;align-items:center;gap:8px">
|
||||||
@@ -101,7 +113,7 @@ function renderPagsTable(data) {
|
|||||||
|
|
||||||
wrap.querySelectorAll('.btn-del-pag').forEach(btn =>
|
wrap.querySelectorAll('.btn-del-pag').forEach(btn =>
|
||||||
btn.addEventListener('click', async () => {
|
btn.addEventListener('click', async () => {
|
||||||
const r = await window.electronAPI.delete(`/payments/${btn.dataset.id}/`);
|
const r = await window.electronAPI.delete(`/payments/${btn.dataset.id}`);
|
||||||
if (r.ok) { showToast('Pagamento excluído!', 'success'); loadPagamentos([], []); }
|
if (r.ok) { showToast('Pagamento excluído!', 'success'); loadPagamentos([], []); }
|
||||||
else showToast(r.error, 'error');
|
else showToast(r.error, 'error');
|
||||||
})
|
})
|
||||||
@@ -172,7 +184,7 @@ function abrirModalPagamento(tiposPag, comandas) {
|
|||||||
description: document.getElementById('pag-desc').value.trim(),
|
description: document.getElementById('pag-desc').value.trim(),
|
||||||
};
|
};
|
||||||
|
|
||||||
const r = await window.electronAPI.post('/payments/', data);
|
const r = await window.electronAPI.post('/payments', data);
|
||||||
if (r.ok) { showToast('Pagamento registrado!', 'success'); closeModal(); loadPagamentos(tiposPag, comandas); }
|
if (r.ok) { showToast('Pagamento registrado!', 'success'); closeModal(); loadPagamentos(tiposPag, comandas); }
|
||||||
else showToast(r.error, 'error');
|
else showToast(r.error, 'error');
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -2,13 +2,21 @@
|
|||||||
// Cada "order" representa um item individual na fila, com pipeline de status por timestamps
|
// Cada "order" representa um item individual na fila, com pipeline de status por timestamps
|
||||||
|
|
||||||
const STATUS_CONFIG = {
|
const STATUS_CONFIG = {
|
||||||
'Em espera': { badge: 'badge-warning', icon: '⏳', next: 'preparing', nextLabel: '▶ Preparando' },
|
'Na fila': { badge: 'badge-warning', icon: '⏳', next: 'preparing', nextLabel: '▶ Preparando' },
|
||||||
'Preparando': { badge: 'badge-info', icon: '🍳', next: 'finished', nextLabel: '✅ Pronto' },
|
'Preparando': { badge: 'badge-info', icon: '🍳', next: 'finished', nextLabel: '✅ Pronto' },
|
||||||
'Pronto': { badge: 'badge-success', icon: '✅', next: 'delivered', nextLabel: '🚀 Entregue' },
|
'Pronto': { badge: 'badge-success', icon: '✅', next: 'delivered', nextLabel: '🚀 Entregue' },
|
||||||
'Entregue': { badge: 'badge-muted', icon: '🚀', next: null, nextLabel: null },
|
'Entregue': { badge: 'badge-muted', icon: '🚀', next: null, nextLabel: null },
|
||||||
'Cancelado': { badge: 'badge-danger', icon: '❌', next: null, nextLabel: null },
|
'Cancelado': { badge: 'badge-danger', icon: '❌', next: null, nextLabel: null },
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function getOrderStatus(o) {
|
||||||
|
if (o.canceled && o.canceled !== '0001-01-01T00:00:00Z') return 'Cancelado';
|
||||||
|
if (o.delivered && o.delivered !== '0001-01-01T00:00:00Z') return 'Entregue';
|
||||||
|
if (o.finished && o.finished !== '0001-01-01T00:00:00Z') return 'Pronto';
|
||||||
|
if (o.preparing && o.preparing !== '0001-01-01T00:00:00Z') return 'Preparando';
|
||||||
|
return 'Na fila';
|
||||||
|
}
|
||||||
|
|
||||||
export async function renderPedidos(container) {
|
export async function renderPedidos(container) {
|
||||||
container.innerHTML = `
|
container.innerHTML = `
|
||||||
<div class="page-header">
|
<div class="page-header">
|
||||||
@@ -25,7 +33,7 @@ export async function renderPedidos(container) {
|
|||||||
<input type="text" class="search-input" id="search-order" placeholder="🔍 Buscar produto, comanda ou mesa..." />
|
<input type="text" class="search-input" id="search-order" placeholder="🔍 Buscar produto, comanda ou mesa..." />
|
||||||
<select id="filter-status-order" class="form-control" style="width:160px">
|
<select id="filter-status-order" class="form-control" style="width:160px">
|
||||||
<option value="">Todos os status</option>
|
<option value="">Todos os status</option>
|
||||||
<option value="Em espera">⏳ Em espera</option>
|
<option value="Na fila">⏳ Na fila</option>
|
||||||
<option value="Preparando">🍳 Preparando</option>
|
<option value="Preparando">🍳 Preparando</option>
|
||||||
<option value="Pronto">✅ Pronto</option>
|
<option value="Pronto">✅ Pronto</option>
|
||||||
<option value="Entregue">🚀 Entregue</option>
|
<option value="Entregue">🚀 Entregue</option>
|
||||||
@@ -43,19 +51,42 @@ export async function renderPedidos(container) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let _ordersData = [];
|
let _ordersData = [];
|
||||||
|
let _productsMap = {};
|
||||||
|
let _comandasMap = {};
|
||||||
|
let _mesasMap = {};
|
||||||
|
|
||||||
async function loadOrders() {
|
async function loadOrders() {
|
||||||
const wrap = document.getElementById('orders-table');
|
const wrap = document.getElementById('orders-table');
|
||||||
if (!wrap) return;
|
if (!wrap) return;
|
||||||
wrap.innerHTML = `<div class="loading-screen"><div class="spinner"></div></div>`;
|
wrap.innerHTML = `<div class="loading-screen"><div class="spinner"></div></div>`;
|
||||||
const res = await window.electronAPI.get('/orders/');
|
|
||||||
|
// Busca tudo em paralelo para resolver as referências (IDs)
|
||||||
|
const [res, pRes, cRes, mRes] = await Promise.all([
|
||||||
|
window.electronAPI.get('/orders'),
|
||||||
|
window.electronAPI.get('/products'),
|
||||||
|
window.electronAPI.get('/comandas'),
|
||||||
|
window.electronAPI.get('/mesas')
|
||||||
|
]);
|
||||||
|
|
||||||
if (!res.ok) { wrap.innerHTML = `<div class="table-empty">Erro ao carregar pedidos.</div>`; return; }
|
if (!res.ok) { wrap.innerHTML = `<div class="table-empty">Erro ao carregar pedidos.</div>`; return; }
|
||||||
|
|
||||||
|
// Constrói mapas para consulta rápida (IDs como string para segurança)
|
||||||
|
if (pRes.ok) _productsMap = pRes.data.reduce((acc, p) => { acc[String(p.id)] = p.name; return acc; }, {});
|
||||||
|
if (mRes.ok) _mesasMap = mRes.data.reduce((acc, m) => { acc[String(m.id)] = m.name; return acc; }, {});
|
||||||
|
if (cRes.ok) _comandasMap = cRes.data.reduce((acc, c) => {
|
||||||
|
acc[String(c.id)] = {
|
||||||
|
name: c.name || '–',
|
||||||
|
mesa: _mesasMap[String(c.mesa)] || `Mesa ${c.mesa}` || '–'
|
||||||
|
};
|
||||||
|
return acc;
|
||||||
|
}, {});
|
||||||
|
|
||||||
_ordersData = res.data;
|
_ordersData = res.data;
|
||||||
|
|
||||||
// Por padrão, filtra para mostrar só os não entregues/cancelados
|
// Por padrão, filtra para mostrar só os não entregues/cancelados
|
||||||
const filtroInicial = document.getElementById('filter-status-order');
|
const filtroInicial = document.getElementById('filter-status-order');
|
||||||
if (filtroInicial && !filtroInicial.value) {
|
if (filtroInicial && !filtroInicial.value) {
|
||||||
// Mantém filtro vazio mas é renderizado completo
|
// Mantém o filtro se necessário
|
||||||
}
|
}
|
||||||
renderOrdersTable(_ordersData);
|
renderOrdersTable(_ordersData);
|
||||||
}
|
}
|
||||||
@@ -79,13 +110,19 @@ function renderOrdersTable(data) {
|
|||||||
</tr></thead>
|
</tr></thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
${data.map(o => {
|
${data.map(o => {
|
||||||
const cfg = STATUS_CONFIG[o.status] || { badge: 'badge-muted', icon: '?', next: null };
|
const status = getOrderStatus(o);
|
||||||
|
const cfg = STATUS_CONFIG[status] || { badge: 'badge-muted', icon: '?', next: null };
|
||||||
|
|
||||||
|
// Resolve nomes com base nos IDs
|
||||||
|
const prodName = _productsMap[String(o.id_product)] || o.product_name || `ID #${o.id_product}`;
|
||||||
|
const comandaInfo = _comandasMap[String(o.id_comanda)] || { name: `Comanda #${o.id_comanda}`, mesa: '–' };
|
||||||
|
|
||||||
return `<tr>
|
return `<tr>
|
||||||
<td style="color:var(--text-muted)">#${o.id}</td>
|
<td style="color:var(--text-muted)">#${o.id}</td>
|
||||||
<td><strong>${o.product_name || '–'}</strong></td>
|
<td><strong>${prodName}</strong></td>
|
||||||
<td style="font-size:0.82rem;color:var(--text-secondary)">${o.comanda_name || '–'}</td>
|
<td style="font-size:0.82rem;color:var(--text-secondary)">${comandaInfo.name}</td>
|
||||||
<td style="font-size:0.82rem">${o.mesa_name || '–'}</td>
|
<td style="font-size:0.82rem">${comandaInfo.mesa}</td>
|
||||||
<td><span class="badge ${cfg.badge}">${cfg.icon} ${o.status}</span></td>
|
<td><span class="badge ${cfg.badge}">${cfg.icon} ${status}</span></td>
|
||||||
<td style="font-size:0.78rem;color:var(--text-muted);white-space:nowrap">${formatTime(o.queue)}</td>
|
<td style="font-size:0.78rem;color:var(--text-muted);white-space:nowrap">${formatTime(o.queue)}</td>
|
||||||
<td style="font-size:0.8rem;color:var(--text-secondary);max-width:140px;">
|
<td style="font-size:0.8rem;color:var(--text-secondary);max-width:140px;">
|
||||||
<div style="display:flex;align-items:center;gap:4px">
|
<div style="display:flex;align-items:center;gap:4px">
|
||||||
@@ -107,9 +144,9 @@ function renderOrdersTable(data) {
|
|||||||
</table>
|
</table>
|
||||||
<div style="padding:12px 20px;border-top:1px solid var(--border);font-size:0.8rem;color:var(--text-muted)">
|
<div style="padding:12px 20px;border-top:1px solid var(--border);font-size:0.8rem;color:var(--text-muted)">
|
||||||
${data.length} ${data.length === 1 ? 'pedido' : 'pedidos'} exibidos
|
${data.length} ${data.length === 1 ? 'pedido' : 'pedidos'} exibidos
|
||||||
· ⏳ ${data.filter(o => o.status === 'Em espera').length} em espera
|
· ⏳ ${data.filter(o => getOrderStatus(o) === 'Na fila').length} na fila
|
||||||
· 🍳 ${data.filter(o => o.status === 'Preparando').length} preparando
|
· 🍳 ${data.filter(o => getOrderStatus(o) === 'Preparando').length} preparando
|
||||||
· ✅ ${data.filter(o => o.status === 'Pronto').length} prontos
|
· ✅ ${data.filter(o => getOrderStatus(o) === 'Pronto').length} prontos
|
||||||
</div>`;
|
</div>`;
|
||||||
|
|
||||||
// Avançar status para próxima etapa
|
// Avançar status para próxima etapa
|
||||||
@@ -117,7 +154,7 @@ function renderOrdersTable(data) {
|
|||||||
btn.addEventListener('click', async () => {
|
btn.addEventListener('click', async () => {
|
||||||
const nowISO = new Date().toISOString();
|
const nowISO = new Date().toISOString();
|
||||||
const patch = { [btn.dataset.next]: nowISO };
|
const patch = { [btn.dataset.next]: nowISO };
|
||||||
const r = await window.electronAPI.patch(`/orders/${btn.dataset.id}/`, patch);
|
const r = await window.electronAPI.patch(`/orders/${btn.dataset.id}`, patch);
|
||||||
if (r.ok) { showToast('Status atualizado!', 'success'); loadOrders(); }
|
if (r.ok) { showToast('Status atualizado!', 'success'); loadOrders(); }
|
||||||
else showToast(r.error, 'error');
|
else showToast(r.error, 'error');
|
||||||
})
|
})
|
||||||
@@ -126,7 +163,7 @@ function renderOrdersTable(data) {
|
|||||||
// Cancelar pedido
|
// Cancelar pedido
|
||||||
wrap.querySelectorAll('.btn-cancela').forEach(btn =>
|
wrap.querySelectorAll('.btn-cancela').forEach(btn =>
|
||||||
btn.addEventListener('click', async () => {
|
btn.addEventListener('click', async () => {
|
||||||
const r = await window.electronAPI.patch(`/orders/${btn.dataset.id}/`, { canceled: new Date().toISOString() });
|
const r = await window.electronAPI.patch(`/orders/${btn.dataset.id}`, { canceled: new Date().toISOString() });
|
||||||
if (r.ok) { showToast('Pedido cancelado.', 'info'); loadOrders(); }
|
if (r.ok) { showToast('Pedido cancelado.', 'info'); loadOrders(); }
|
||||||
else showToast(r.error, 'error');
|
else showToast(r.error, 'error');
|
||||||
})
|
})
|
||||||
@@ -144,7 +181,7 @@ function renderOrdersTable(data) {
|
|||||||
window.abrirModalObsCozinhaGlobal(productName, currentObs, async (novaObs) => {
|
window.abrirModalObsCozinhaGlobal(productName, currentObs, async (novaObs) => {
|
||||||
if (novaObs === null || novaObs === currentObs) return;
|
if (novaObs === null || novaObs === currentObs) return;
|
||||||
|
|
||||||
const r = await window.electronAPI.patch(`/orders/${orderId}/`, { obs: novaObs });
|
const r = await window.electronAPI.patch(`/orders/${orderId}`, { obs: novaObs });
|
||||||
if (r.ok) {
|
if (r.ok) {
|
||||||
showToast('Observação atualizada!', 'success');
|
showToast('Observação atualizada!', 'success');
|
||||||
loadOrders();
|
loadOrders();
|
||||||
@@ -167,7 +204,7 @@ function filtrarOrders() {
|
|||||||
(o.comanda_name || '').toLowerCase().includes(q) ||
|
(o.comanda_name || '').toLowerCase().includes(q) ||
|
||||||
(o.mesa_name || '').toLowerCase().includes(q) ||
|
(o.mesa_name || '').toLowerCase().includes(q) ||
|
||||||
(o.obs || '').toLowerCase().includes(q);
|
(o.obs || '').toLowerCase().includes(q);
|
||||||
const matchStatus = !status || o.status === status;
|
const matchStatus = !status || getOrderStatus(o) === status;
|
||||||
return matchQ && matchStatus;
|
return matchQ && matchStatus;
|
||||||
});
|
});
|
||||||
renderOrdersTable(filtered);
|
renderOrdersTable(filtered);
|
||||||
|
|||||||
@@ -1,7 +1,12 @@
|
|||||||
export async function renderProdutos(container) {
|
export async function renderProdutos(container) {
|
||||||
let categorias = [];
|
let categorias = [];
|
||||||
const catRes = await window.electronAPI.get('/categories/');
|
let unidades = [];
|
||||||
|
const [catRes, unRes] = await Promise.all([
|
||||||
|
window.electronAPI.get('/categories'),
|
||||||
|
window.electronAPI.get('/unit-of-measurements')
|
||||||
|
]);
|
||||||
if (catRes.ok) categorias = catRes.data;
|
if (catRes.ok) categorias = catRes.data;
|
||||||
|
if (unRes.ok) unidades = unRes.data;
|
||||||
|
|
||||||
container.innerHTML = `
|
container.innerHTML = `
|
||||||
<div class="page-header">
|
<div class="page-header">
|
||||||
@@ -10,7 +15,7 @@ export async function renderProdutos(container) {
|
|||||||
<div class="page-subtitle">Cardápio e categorias</div>
|
<div class="page-subtitle">Cardápio e categorias</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="page-actions">
|
<div class="page-actions">
|
||||||
<button class="btn btn-secondary btn-md" id="btn-nova-cat">+ Categoria</button>
|
<button class="btn btn-secondary btn-md" id="btn-gerenciar-cat">📁 Categorias</button>
|
||||||
<button class="btn btn-primary btn-md" id="btn-novo-prod">+ Produto</button>
|
<button class="btn btn-primary btn-md" id="btn-novo-prod">+ Produto</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -30,28 +35,28 @@ export async function renderProdutos(container) {
|
|||||||
<div id="produtos-table"></div>
|
<div id="produtos-table"></div>
|
||||||
</div>`;
|
</div>`;
|
||||||
|
|
||||||
await loadProdutos(categorias);
|
await loadProdutos(categorias, unidades);
|
||||||
|
|
||||||
document.getElementById('btn-novo-prod').addEventListener('click', () => abrirModalProduto(null, categorias));
|
document.getElementById('btn-novo-prod').addEventListener('click', () => abrirModalProduto(null, categorias, unidades));
|
||||||
document.getElementById('btn-nova-cat').addEventListener('click', () => abrirModalCategoria());
|
document.getElementById('btn-gerenciar-cat').addEventListener('click', () => abrirModalGerenciarCategorias(categorias));
|
||||||
document.getElementById('search-produto').addEventListener('input', () => filtrarProdutos(categorias));
|
document.getElementById('search-produto').addEventListener('input', () => filtrarProdutos(categorias, unidades));
|
||||||
document.getElementById('filter-cat').addEventListener('change', () => filtrarProdutos(categorias));
|
document.getElementById('filter-cat').addEventListener('change', () => filtrarProdutos(categorias, unidades));
|
||||||
document.getElementById('filter-ativo').addEventListener('change', () => filtrarProdutos(categorias));
|
document.getElementById('filter-ativo').addEventListener('change', () => filtrarProdutos(categorias, unidades));
|
||||||
}
|
}
|
||||||
|
|
||||||
let _produtosData = [];
|
let _produtosData = [];
|
||||||
|
|
||||||
async function loadProdutos(categorias) {
|
async function loadProdutos(categorias, unidades) {
|
||||||
const wrap = document.getElementById('produtos-table');
|
const wrap = document.getElementById('produtos-table');
|
||||||
if (!wrap) return;
|
if (!wrap) return;
|
||||||
wrap.innerHTML = `<div class="loading-screen"><div class="spinner"></div></div>`;
|
wrap.innerHTML = `<div class="loading-screen"><div class="spinner"></div></div>`;
|
||||||
const res = await window.electronAPI.get('/products/');
|
const res = await window.electronAPI.get('/products');
|
||||||
if (!res.ok) { wrap.innerHTML = `<div class="table-empty">Erro ao carregar produtos.</div>`; return; }
|
if (!res.ok) { wrap.innerHTML = `<div class="table-empty">Erro ao carregar produtos.</div>`; return; }
|
||||||
_produtosData = res.data;
|
_produtosData = res.data;
|
||||||
renderProdutosTable(_produtosData, categorias);
|
renderProdutosTable(_produtosData, categorias, unidades);
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderProdutosTable(data, categorias) {
|
function renderProdutosTable(data, categorias, unidades) {
|
||||||
const wrap = document.getElementById('produtos-table');
|
const wrap = document.getElementById('produtos-table');
|
||||||
if (!wrap) return;
|
if (!wrap) return;
|
||||||
if (!data.length) { wrap.innerHTML = `<div class="table-empty">Nenhum produto encontrado.</div>`; return; }
|
if (!data.length) { wrap.innerHTML = `<div class="table-empty">Nenhum produto encontrado.</div>`; return; }
|
||||||
@@ -87,14 +92,16 @@ function renderProdutosTable(data, categorias) {
|
|||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<span class="badge ${p.active ? 'badge-success' : 'badge-danger'}">
|
<button class="badge ${p.active ? 'badge-success' : 'badge-danger'} btn-status-prod"
|
||||||
|
data-id="${p.id}" data-active="${p.active}"
|
||||||
|
style="cursor:pointer; border:none; outline:none; transition: transform 0.1s active"
|
||||||
|
title="Clique para alterar status">
|
||||||
${p.active ? 'Ativo' : 'Inativo'}
|
${p.active ? 'Ativo' : 'Inativo'}
|
||||||
</span>
|
</button>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<div style="display:flex;gap:6px">
|
<div style="display:flex;gap:6px">
|
||||||
<button class="btn btn-secondary btn-sm btn-edit-prod" data-id="${p.id}">Editar</button>
|
<button class="btn btn-secondary btn-sm btn-edit-prod" data-id="${p.id}">Editar</button>
|
||||||
<button class="btn btn-danger btn-sm btn-del-prod" data-id="${p.id}">Excluir</button>
|
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>`).join('')}
|
</tr>`).join('')}
|
||||||
@@ -103,21 +110,27 @@ function renderProdutosTable(data, categorias) {
|
|||||||
|
|
||||||
wrap.querySelectorAll('.btn-edit-prod').forEach(btn => {
|
wrap.querySelectorAll('.btn-edit-prod').forEach(btn => {
|
||||||
btn.addEventListener('click', () => {
|
btn.addEventListener('click', () => {
|
||||||
const p = _produtosData.find(x => x.id === parseInt(btn.dataset.id));
|
const p = _produtosData.find(x => String(x.id) === String(btn.dataset.id));
|
||||||
if (p) abrirModalProduto(p, categorias);
|
if (p) abrirModalProduto(p, categorias, unidades);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
wrap.querySelectorAll('.btn-del-prod').forEach(btn =>
|
wrap.querySelectorAll('.btn-status-prod').forEach(btn => {
|
||||||
btn.addEventListener('click', async () => {
|
btn.addEventListener('click', async () => {
|
||||||
const r = await window.electronAPI.delete(`/products/${btn.dataset.id}/`);
|
const id = btn.dataset.id;
|
||||||
if (r.ok) { showToast('Produto excluído!', 'success'); loadProdutos(categorias); }
|
const currentActive = btn.dataset.active === 'true';
|
||||||
else showToast(r.error, 'error');
|
const r = await window.electronAPI.patch(`/products/${id}`, { active: !currentActive });
|
||||||
})
|
if (r.ok) {
|
||||||
);
|
showToast(`Produto ${!currentActive ? 'ativado' : 'inativado'}!`, 'success');
|
||||||
|
loadProdutos(categorias, unidades);
|
||||||
|
} else {
|
||||||
|
showToast(r.error, 'error');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function filtrarProdutos(categorias) {
|
function filtrarProdutos(categorias, unidades) {
|
||||||
const q = document.getElementById('search-produto')?.value.toLowerCase() || '';
|
const q = document.getElementById('search-produto')?.value.toLowerCase() || '';
|
||||||
const catId = parseInt(document.getElementById('filter-cat')?.value) || null;
|
const catId = parseInt(document.getElementById('filter-cat')?.value) || null;
|
||||||
const ativo = document.getElementById('filter-ativo')?.value;
|
const ativo = document.getElementById('filter-ativo')?.value;
|
||||||
@@ -128,10 +141,10 @@ function filtrarProdutos(categorias) {
|
|||||||
const matchAtiv = !ativo || String(p.active) === ativo;
|
const matchAtiv = !ativo || String(p.active) === ativo;
|
||||||
return matchQ && matchCat && matchAtiv;
|
return matchQ && matchCat && matchAtiv;
|
||||||
});
|
});
|
||||||
renderProdutosTable(filtered, categorias);
|
renderProdutosTable(filtered, categorias, unidades);
|
||||||
}
|
}
|
||||||
|
|
||||||
function abrirModalProduto(produto, categorias) {
|
function abrirModalProduto(produto, categorias, unidades) {
|
||||||
const isEdit = !!produto;
|
const isEdit = !!produto;
|
||||||
openModal({
|
openModal({
|
||||||
title: isEdit ? `Editar: ${produto.name}` : 'Novo Produto',
|
title: isEdit ? `Editar: ${produto.name}` : 'Novo Produto',
|
||||||
@@ -156,6 +169,13 @@ function abrirModalProduto(produto, categorias) {
|
|||||||
${categorias.map(c => `<option value="${c.id}" ${produto?.category === c.id ? 'selected' : ''}>${c.nome || c.name}</option>`).join('')}
|
${categorias.map(c => `<option value="${c.id}" ${produto?.category === c.id ? 'selected' : ''}>${c.nome || c.name}</option>`).join('')}
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Unidade de Medida</label>
|
||||||
|
<select id="prod-unit" class="form-control">
|
||||||
|
<option value="">– Selecione –</option>
|
||||||
|
${unidades.map(u => `<option value="${u.id}" ${produto?.unit_of_measure === u.id ? 'selected' : ''}>${u.acronym || u.name}</option>`).join('')}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>Ativo</label>
|
<label>Ativo</label>
|
||||||
<select id="prod-ativo" class="form-control">
|
<select id="prod-ativo" class="form-control">
|
||||||
@@ -175,52 +195,168 @@ function abrirModalProduto(produto, categorias) {
|
|||||||
<input type="text" id="prod-desc" class="form-control" value="${produto?.description || ''}" placeholder="Descrição opcional" />
|
<input type="text" id="prod-desc" class="form-control" value="${produto?.description || ''}" placeholder="Descrição opcional" />
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group" style="grid-column:1/-1">
|
<div class="form-group" style="grid-column:1/-1">
|
||||||
<label>Imagem</label>
|
<label>URL da Imagem</label>
|
||||||
<input type="file" id="prod-img" accept="image/*" class="form-control" />
|
<input type="text" id="prod-img" class="form-control" value="${produto?.image || ''}" placeholder="http://..." />
|
||||||
</div>
|
</div>
|
||||||
</div>`,
|
</div>`,
|
||||||
footer: `
|
footer: `
|
||||||
<button class="btn btn-secondary btn-md" onclick="closeModal()">Cancelar</button>
|
<button class="btn btn-secondary btn-md" onclick="closeModal()">Cancelar</button>
|
||||||
<button class="btn btn-primary btn-md" id="btn-salvar-prod">${isEdit ? 'Salvar' : 'Criar'}</button>`,
|
<button class="btn btn-primary btn-md" id="btn-salvar-prod">${isEdit ? 'Salvar Alterações' : 'Criar Produto'}</button>`,
|
||||||
});
|
});
|
||||||
|
|
||||||
document.getElementById('btn-salvar-prod').addEventListener('click', async () => {
|
document.getElementById('btn-salvar-prod').addEventListener('click', async () => {
|
||||||
const catVal = parseInt(document.getElementById('prod-cat').value) || null;
|
const btn = document.getElementById('btn-salvar-prod');
|
||||||
|
btn.disabled = true;
|
||||||
|
btn.textContent = 'Processando...';
|
||||||
|
|
||||||
|
const catVal = parseInt(document.getElementById('prod-cat').value);
|
||||||
|
const unitVal = parseInt(document.getElementById('prod-unit').value);
|
||||||
|
|
||||||
|
// Constrói o payload enviando apenas o que é necessário/preenchido
|
||||||
const data = {
|
const data = {
|
||||||
name: document.getElementById('prod-nome').value,
|
name: document.getElementById('prod-nome').value.trim(),
|
||||||
description: document.getElementById('prod-desc').value,
|
description: document.getElementById('prod-desc').value.trim(),
|
||||||
//image: document.getElementById('prod-img').value,
|
|
||||||
price: parseFloat(document.getElementById('prod-preco').value) || 0,
|
price: parseFloat(document.getElementById('prod-preco').value) || 0,
|
||||||
quantity: parseInt(document.getElementById('prod-qty').value) || 0,
|
quantity: parseInt(document.getElementById('prod-qty').value) || 0,
|
||||||
category: catVal,
|
|
||||||
active: document.getElementById('prod-ativo').value === 'true',
|
active: document.getElementById('prod-ativo').value === 'true',
|
||||||
cuisine: document.getElementById('prod-cuisine').value === 'true',
|
cuisine: document.getElementById('prod-cuisine').value === 'true',
|
||||||
|
image: document.getElementById('prod-img').value.trim(),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (catVal) data.category = catVal;
|
||||||
|
if (unitVal) data.unit_of_measure = unitVal;
|
||||||
|
|
||||||
|
if (!data.name) {
|
||||||
|
btn.disabled = false;
|
||||||
|
btn.textContent = isEdit ? 'Salvar Alterações' : 'Criar Produto';
|
||||||
|
return showToast('O nome do produto é obrigatório.', 'warning');
|
||||||
|
}
|
||||||
|
|
||||||
const r = isEdit
|
const r = isEdit
|
||||||
? await window.electronAPI.put(`/products/${produto.id}/`, data)
|
? await window.electronAPI.patch(`/products/${produto.id}`, data)
|
||||||
: await window.electronAPI.post('/products/', data);
|
: await window.electronAPI.post('/products', data);
|
||||||
if (r.ok) { showToast(isEdit ? 'Produto atualizado!' : 'Produto criado!', 'success'); closeModal(); loadProdutos(categorias); }
|
|
||||||
else showToast(r.error, 'error');
|
if (r.ok) {
|
||||||
|
showToast(isEdit ? 'Produto atualizado!' : 'Produto criado!', 'success');
|
||||||
|
closeModal();
|
||||||
|
loadProdutos(categorias, unidades);
|
||||||
|
} else {
|
||||||
|
showToast(r.error, 'error');
|
||||||
|
btn.disabled = false;
|
||||||
|
btn.textContent = isEdit ? 'Salvar Alterações' : 'Criar Produto';
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function abrirModalCategoria() {
|
// ─── Modal Gerenciar Categorias ──────────────────────────────────────────────
|
||||||
|
async function abrirModalGerenciarCategorias(categoriasArr) {
|
||||||
|
let categorias = Array.isArray(categoriasArr) ? categoriasArr : [];
|
||||||
|
|
||||||
|
const updateTable = () => {
|
||||||
|
const listWrap = document.getElementById('cat-list-items');
|
||||||
|
if (!listWrap) return;
|
||||||
|
listWrap.innerHTML = categorias.map(c => `
|
||||||
|
<div style="display:flex; justify-content:space-between; align-items:center; padding:10px; border-bottom:1px solid var(--border)">
|
||||||
|
<span style="${!c.active ? 'text-decoration:line-through; color:var(--text-muted)' : ''}">
|
||||||
|
${c.nome || c.name} ${!c.active ? '(Inativa)' : ''}
|
||||||
|
</span>
|
||||||
|
<button class="btn btn-ghost btn-sm btn-edit-cat" data-id="${c.id}" title="Editar Categoria">✏️</button>
|
||||||
|
</div>`).join('');
|
||||||
|
|
||||||
|
listWrap.querySelectorAll('.btn-edit-cat').forEach(btn => {
|
||||||
|
btn.addEventListener('click', () => {
|
||||||
|
const cat = categorias.find(x => String(x.id) === String(btn.dataset.id));
|
||||||
|
if (cat) abrirFormularioCategoria(cat, updateTable, async (novasCats) => {
|
||||||
|
categorias = novasCats;
|
||||||
|
updateTable();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
openModal({
|
openModal({
|
||||||
title: 'Nova Categoria',
|
title: '📁 Gerenciar Categorias',
|
||||||
body: `
|
body: `
|
||||||
<div class="form-group">
|
<div style="margin-bottom:20px">
|
||||||
<label>Nome</label>
|
<button class="btn btn-primary btn-sm" id="btn-cat-add-new">+ Adicionar Nova</button>
|
||||||
<input type="text" id="cat-nome" class="form-control" placeholder="Ex: Bebidas, Lanches..." />
|
</div>
|
||||||
|
<div id="cat-list-items" style="max-height:400px; overflow-y:auto; border:1px solid var(--border); border-radius:var(--radius-sm)">
|
||||||
|
<div class="table-empty">Limpando...</div>
|
||||||
</div>`,
|
</div>`,
|
||||||
footer: `
|
footer: `<button class="btn btn-secondary btn-md" onclick="closeModal()">Fechar</button>`,
|
||||||
<button class="btn btn-secondary btn-md" onclick="closeModal()">Cancelar</button>
|
|
||||||
<button class="btn btn-primary btn-md" id="btn-criar-cat">Criar</button>`,
|
|
||||||
});
|
});
|
||||||
document.getElementById('btn-criar-cat').addEventListener('click', async () => {
|
|
||||||
const nome = document.getElementById('cat-nome').value.trim();
|
updateTable();
|
||||||
if (!nome) return showToast('Informe um nome.', 'error');
|
|
||||||
const r = await window.electronAPI.post('/categories/', { nome, name: nome });
|
document.getElementById('btn-cat-add-new').addEventListener('click', () => {
|
||||||
if (r.ok) { showToast('Categoria criada!', 'success'); closeModal(); }
|
abrirFormularioCategoria(null, updateTable, async (novasCats) => {
|
||||||
else showToast(r.error, 'error');
|
categorias = novasCats;
|
||||||
|
updateTable();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Abre formulário pequeno para criar ou editar uma única categoria
|
||||||
|
function abrirFormularioCategoria(cat, onSuccess, onListUpdate) {
|
||||||
|
const isEdit = !!cat;
|
||||||
|
|
||||||
|
// Criamos uma mini modal ou sobrepomos a atual com uma de confirmação simples de formulário
|
||||||
|
// Para simplicidade, vamos usar o openModal mesmo (ele sobrepõe)
|
||||||
|
openModal({
|
||||||
|
title: isEdit ? `Editar Categoria: ${cat.nome || cat.name}` : 'Nova Categoria',
|
||||||
|
body: `
|
||||||
|
<div class="form-grid">
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Nome da Categoria</label>
|
||||||
|
<input type="text" id="cat-field-name" class="form-control" value="${isEdit ? (cat.nome || cat.name) : ''}" placeholder="Ex: Bebidas, Sobremesas..." />
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Status</label>
|
||||||
|
<select id="cat-field-active" class="form-control">
|
||||||
|
<option value="true" ${cat?.active !== false ? 'selected' : ''}>Ativa</option>
|
||||||
|
<option value="false" ${cat?.active === false ? 'selected' : ''}>Inativa</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>`,
|
||||||
|
footer: `
|
||||||
|
<button class="btn btn-secondary btn-md" id="btn-cat-form-cancel">Voltar</button>
|
||||||
|
<button class="btn btn-primary btn-md" id="btn-cat-form-save">${isEdit ? 'Salvar Alterações' : 'Criar Categoria'}</button>`,
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById('btn-cat-form-cancel').onclick = () => {
|
||||||
|
closeModal();
|
||||||
|
// Reabre o gerenciador
|
||||||
|
setTimeout(() => {
|
||||||
|
// Recarregar categorias para garantir sync
|
||||||
|
window.electronAPI.get('/categories').then(res => {
|
||||||
|
if (res.ok) onListUpdate(res.data);
|
||||||
|
});
|
||||||
|
}, 300);
|
||||||
|
};
|
||||||
|
|
||||||
|
document.getElementById('btn-cat-form-save').addEventListener('click', async () => {
|
||||||
|
const nome = document.getElementById('cat-field-name').value.trim();
|
||||||
|
const active = document.getElementById('cat-field-active').value === 'true';
|
||||||
|
|
||||||
|
if (!nome) return showToast('O nome é obrigatório.', 'warning');
|
||||||
|
|
||||||
|
const data = { name: nome, active: active };
|
||||||
|
const r = isEdit
|
||||||
|
? await window.electronAPI.patch(`/categories/${cat.id}`, { name: nome }) // Use PATCH as requested
|
||||||
|
: await window.electronAPI.post('/categories', data);
|
||||||
|
|
||||||
|
if (r.ok) {
|
||||||
|
showToast(isEdit ? 'Categoria atualizada!' : 'Categoria criada!', 'success');
|
||||||
|
const res = await window.electronAPI.get('/categories');
|
||||||
|
if (res.ok) {
|
||||||
|
onListUpdate(res.data);
|
||||||
|
closeModal();
|
||||||
|
// Não reabre o gerenciador imediatamente para dar tempo do toast sumir,
|
||||||
|
// mas aqui vamos reabrir para manter o fluxo
|
||||||
|
setTimeout(() => abrirModalGerenciarCategorias(res.data), 300);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
showToast(r.error, 'error');
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -979,3 +979,80 @@ select.form-control option {
|
|||||||
color: var(--primary);
|
color: var(--primary);
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ─── Print Styles ────────────────────────────────────────────────────────── */
|
||||||
|
@media print {
|
||||||
|
@page {
|
||||||
|
size: 80mm auto;
|
||||||
|
margin: 5mm;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
background: white !important;
|
||||||
|
color: black !important;
|
||||||
|
font-size: 12px;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
body > *:not(.print-comanda) {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.print-comanda {
|
||||||
|
display: block !important;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 80mm;
|
||||||
|
background: white;
|
||||||
|
color: black;
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
font-size: 11px;
|
||||||
|
line-height: 1.3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.print-comanda * {
|
||||||
|
color: black !important;
|
||||||
|
background: transparent !important;
|
||||||
|
border: none !important;
|
||||||
|
box-shadow: none !important;
|
||||||
|
text-shadow: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.print-header {
|
||||||
|
text-align: center;
|
||||||
|
border-bottom: 1px dashed #000;
|
||||||
|
padding-bottom: 8px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.print-title {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.print-info {
|
||||||
|
margin: 4px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.print-items {
|
||||||
|
border-bottom: 1px dashed #000;
|
||||||
|
padding-bottom: 8px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.print-item {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 2px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.print-totals {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.print-footer {
|
||||||
|
text-align: center;
|
||||||
|
margin-top: 10px;
|
||||||
|
font-size: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user