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();
|
||||
|
||||
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 ────────────────────────────────────────────────────────
|
||||
@@ -31,89 +38,45 @@ function createWindow() {
|
||||
});
|
||||
|
||||
mainWindow.loadFile(path.join(__dirname, '../renderer/index.html'));
|
||||
mainWindow.webContents.openDevTools();
|
||||
// mainWindow.webContents.openDevTools();
|
||||
}
|
||||
|
||||
// ─── Helpers de Token ────────────────────────────────────────────────────────
|
||||
let isRefreshing = false;
|
||||
let refreshPromise = null;
|
||||
// ─── Helpers de Autenticação (Middleware Go) ──────────────────────────────────
|
||||
|
||||
function getHeaders() {
|
||||
const token = store.get('access_token');
|
||||
// console.log('[MAIN] getHeaders - Token exists:', !!token);
|
||||
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;
|
||||
const user = store.get('user');
|
||||
return user && user.id ? { 'X-User-ID': String(user.id) } : {};
|
||||
}
|
||||
|
||||
async function requestWithRetry(method, endpoint, data) {
|
||||
const url = `${getBaseUrl()}${endpoint}`;
|
||||
console.log(`[API] ${method.toUpperCase()} ${url}`);
|
||||
const cleanEndpoint = endpoint.endsWith('/') ? endpoint.slice(0, -1) : endpoint;
|
||||
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 {
|
||||
const res = await axios({ method, url, data, headers: getHeaders() });
|
||||
const res = await axios({
|
||||
method,
|
||||
url,
|
||||
data,
|
||||
headers
|
||||
});
|
||||
return { ok: true, data: res.data };
|
||||
} catch (err) {
|
||||
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;
|
||||
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 };
|
||||
}
|
||||
}
|
||||
@@ -121,29 +84,55 @@ async function requestWithRetry(method, endpoint, data) {
|
||||
// ─── IPC Handlers (Registrar IMEDIATAMENTE) ──────────────────────────────────
|
||||
ipcMain.handle('auth:login', async (_, { username, password }) => {
|
||||
try {
|
||||
const res = await axios.post(`${getBaseUrl()}/token/`, { username, password });
|
||||
console.log('[MAIN] Login Successful. User:', res.data.user?.username);
|
||||
store.set('access_token', res.data.access);
|
||||
store.set('refresh_token', res.data.refresh);
|
||||
store.set('user', res.data.user);
|
||||
const url = `${getBaseUrl()}/login`;
|
||||
console.log(`[DEBUG_LOGIN] >>> Tentando login em: ${url}`);
|
||||
|
||||
const res = await axios.post(url, { username, password }, {
|
||||
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 };
|
||||
} catch (err) {
|
||||
console.error('[MAIN] Login Failed:', err.response?.data || err.message);
|
||||
return { ok: false, error: 'Credenciais inválidas.' };
|
||||
console.error('[DEBUG_LOGIN] >>> Falha no Login:', err.response?.data || err.message);
|
||||
return { ok: false, error: 'Erro de autenticação no servidor local.' };
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.handle('auth:logout', () => {
|
||||
store.delete('user');
|
||||
store.delete('access_token');
|
||||
store.delete('refresh_token');
|
||||
store.delete('user');
|
||||
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', () => {
|
||||
console.log('[MAIN] IPC auth:user requested.');
|
||||
return store.get('user');
|
||||
});
|
||||
|
||||
@@ -160,6 +149,38 @@ ipcMain.handle('config:set-url', (_, url) => {
|
||||
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 ──────────────────────────────────────────────────────────────
|
||||
app.whenReady().then(() => {
|
||||
createWindow();
|
||||
|
||||
@@ -10,6 +10,11 @@ contextBridge.exposeInMainWorld('electronAPI', {
|
||||
// Config
|
||||
getConfigUrl: () => ipcRenderer.invoke('config:get-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
|
||||
get: (endpoint) => ipcRenderer.invoke('api:get', endpoint),
|
||||
|
||||
@@ -31,28 +31,63 @@ export async function renderClientes(container) {
|
||||
}
|
||||
|
||||
let _clientesData = [];
|
||||
let _comandasData = [];
|
||||
let _productsMap = {};
|
||||
let _paymentTypes = [];
|
||||
|
||||
async function loadClientes() {
|
||||
const wrap = document.getElementById('clientes-table');
|
||||
if (!wrap) return;
|
||||
wrap.innerHTML = `<div class="loading-screen"><div class="spinner"></div></div>`;
|
||||
|
||||
// Carrega clientes e produtos em paralelo para ter os preços
|
||||
const [res, pRes] = await Promise.all([
|
||||
window.electronAPI.get('/clients/'),
|
||||
window.electronAPI.get('/products/')
|
||||
// Carrega clientes, produtos, comandas e tipos de pagamento em paralelo
|
||||
const [res, pRes, cRes, ptRes] = await Promise.all([
|
||||
window.electronAPI.get('/clients'),
|
||||
window.electronAPI.get('/products'),
|
||||
window.electronAPI.get('/comandas'),
|
||||
window.electronAPI.get('/payment-types')
|
||||
]);
|
||||
|
||||
if (ptRes.ok) _paymentTypes = ptRes.data;
|
||||
|
||||
if (pRes.ok) {
|
||||
_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;
|
||||
}, {});
|
||||
}
|
||||
|
||||
if (cRes.ok) _comandasData = cRes.data;
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -61,8 +96,8 @@ function renderClientesTable(data) {
|
||||
if (!wrap) return;
|
||||
if (!data.length) { wrap.innerHTML = `<div class="table-empty">Nenhum cliente encontrado.</div>`; return; }
|
||||
|
||||
// Ordena por maior débito por padrão
|
||||
const sorted = [...data].sort((a, b) => parseFloat(b.debt || 0) - parseFloat(a.debt || 0));
|
||||
// Ordena por maior débito por padrão usando o cálculo dinâmico
|
||||
const sorted = [...data].sort((a, b) => (b.real_debt || 0) - (a.real_debt || 0));
|
||||
|
||||
wrap.innerHTML = `
|
||||
<table>
|
||||
@@ -70,20 +105,20 @@ function renderClientesTable(data) {
|
||||
<th>#</th>
|
||||
<th>Nome</th>
|
||||
<th>Contato</th>
|
||||
<th>Débito</th>
|
||||
<th style="color:var(--primary)">Débito Dinâmico</th>
|
||||
<th>Status</th>
|
||||
<th>Cadastrado em</th>
|
||||
<th>Ações</th>
|
||||
</tr></thead>
|
||||
<tbody>
|
||||
${sorted.map(c => {
|
||||
const debt = parseFloat(c.debt || 0);
|
||||
const debt = c.real_debt || 0;
|
||||
return `<tr>
|
||||
<td style="color:var(--text-muted)">#${c.id}</td>
|
||||
<td><strong>${c.name}</strong></td>
|
||||
<td>${c.contact || '–'}</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)}
|
||||
</span>
|
||||
</td>
|
||||
@@ -95,7 +130,7 @@ function renderClientesTable(data) {
|
||||
<td style="font-size:0.82rem;color:var(--text-muted)">${formatDate(c.created_at)}</td>
|
||||
<td>
|
||||
<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>
|
||||
</div>
|
||||
</td>
|
||||
@@ -131,7 +166,7 @@ function filtrarClientes() {
|
||||
(c.contact || '').toLowerCase().includes(q) ||
|
||||
String(c.id).includes(q);
|
||||
|
||||
const debtValue = parseFloat(c.debt || 0);
|
||||
const debtValue = c.real_debt || 0;
|
||||
const matchDebt = !debtFltr ||
|
||||
(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');
|
||||
|
||||
const r = isEdit
|
||||
? await window.electronAPI.put(`/clients/${cliente.id}/`, data)
|
||||
: await window.electronAPI.post('/clients/', data);
|
||||
? await window.electronAPI.put(`/clients/${cliente.id}`, data)
|
||||
: await window.electronAPI.post('/clients', data);
|
||||
|
||||
if (r.ok) { showToast(isEdit ? 'Cliente atualizado!' : 'Cliente criado!', 'success'); closeModal(); loadClientes(); }
|
||||
else showToast(r.error, 'error');
|
||||
@@ -200,42 +235,64 @@ async function abrirHistoricoFiados(cliente) {
|
||||
title: `📜 Fiados: ${cliente.name}`,
|
||||
body: `
|
||||
<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>
|
||||
<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 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 style="display:flex; justify-content:space-between; align-items:center">
|
||||
<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>
|
||||
<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>`,
|
||||
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 summary = document.getElementById('fiados-summary');
|
||||
if (!listContainer) return;
|
||||
|
||||
if (!res.ok) {
|
||||
listContainer.innerHTML = `<div class="table-empty">Erro ao carregar fiados: ${res.error}</div>`;
|
||||
return;
|
||||
}
|
||||
console.log(`[DEBUG_FIADOS] Procurando fiados para cliente ${cliente.id} (${cliente.name})...`);
|
||||
console.log(`[DEBUG_FIADOS] Total de comandas na memória: ${_comandasData.length}`);
|
||||
|
||||
// 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) {
|
||||
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;
|
||||
}
|
||||
|
||||
listContainer.classList.remove('loading-screen');
|
||||
listContainer.style.maxHeight = '400px';
|
||||
listContainer.style.overflowY = 'auto';
|
||||
summary.classList.remove('hidden');
|
||||
|
||||
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 `
|
||||
<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>
|
||||
<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>
|
||||
<div style="text-align:right">
|
||||
@@ -254,25 +311,26 @@ async function abrirHistoricoFiados(cliente) {
|
||||
</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;">
|
||||
<details>
|
||||
<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})
|
||||
</summary>
|
||||
<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)">
|
||||
<span>• ${it.product_name}</span>
|
||||
<span>• ${prodName}</span>
|
||||
<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>
|
||||
</li>
|
||||
`).join('')}
|
||||
`}).join('')}
|
||||
</ul>
|
||||
</details>
|
||||
</div>
|
||||
@@ -298,24 +356,45 @@ async function abrirHistoricoFiados(cliente) {
|
||||
});
|
||||
|
||||
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;
|
||||
|
||||
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');
|
||||
btn.disabled = true;
|
||||
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) {
|
||||
showToast('Pagamento realizado com sucesso!', 'success');
|
||||
const payload = {
|
||||
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();
|
||||
loadClientes(); // Recarrega a lista para atualizar os débitos
|
||||
loadClientes();
|
||||
} else {
|
||||
showToast(r.error || 'Erro ao processar pagamento.', 'error');
|
||||
btn.disabled = false;
|
||||
btn.textContent = '💳 Pagar Selecionados';
|
||||
showToast(`Concluído com ${erros} erro(s). Verifique os recibos.`, 'warning');
|
||||
loadClientes();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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) {
|
||||
container.innerHTML = `
|
||||
<div class="page-header">
|
||||
@@ -24,7 +144,7 @@ export async function renderComandas(container) {
|
||||
</div>`;
|
||||
|
||||
let mesas = [];
|
||||
const mesasRes = await window.electronAPI.get('/mesas/');
|
||||
const mesasRes = await window.electronAPI.get('/mesas');
|
||||
if (mesasRes.ok) mesas = mesasRes.data;
|
||||
|
||||
await loadComandas(mesas);
|
||||
@@ -37,8 +157,10 @@ export async function renderComandas(container) {
|
||||
let _comandasData = [];
|
||||
let _mesasRef = [];
|
||||
let _productsMap = {}; // Cache de preços {id: price}
|
||||
let _productsNames = {}; // Cache de nomes {id: name}
|
||||
let _paymentTypes = [];
|
||||
let _clients = [];
|
||||
let _paymentsMap = {}; // Cache de pagamentos por comanda {comandaId: [pagamentos]}
|
||||
|
||||
async function loadComandas(mesas) {
|
||||
_mesasRef = mesas;
|
||||
@@ -46,12 +168,12 @@ async function loadComandas(mesas) {
|
||||
if (!wrap) return;
|
||||
wrap.innerHTML = `<div class="loading-screen"><div class="spinner"></div></div>`;
|
||||
|
||||
// Carrega dados necessários em paralelo
|
||||
const [res, pRes, ptRes, cRes] = await Promise.all([
|
||||
window.electronAPI.get('/comandas/'),
|
||||
window.electronAPI.get('/products/'),
|
||||
window.electronAPI.get('/payment-types/'),
|
||||
window.electronAPI.get('/clients/')
|
||||
const [res, pRes, ptRes, cRes, pagsRes] = await Promise.all([
|
||||
window.electronAPI.get('/comandas'),
|
||||
window.electronAPI.get('/products'),
|
||||
window.electronAPI.get('/payment-types'),
|
||||
window.electronAPI.get('/clients'),
|
||||
window.electronAPI.get('/payments')
|
||||
]);
|
||||
|
||||
if (ptRes.ok) _paymentTypes = ptRes.data;
|
||||
@@ -59,15 +181,30 @@ async function loadComandas(mesas) {
|
||||
|
||||
if (pRes.ok) {
|
||||
_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;
|
||||
}, {});
|
||||
}
|
||||
|
||||
if (!res.ok) { wrap.innerHTML = `<div class="table-empty">Erro ao carregar comandas.</div>`; return; }
|
||||
_comandasData = res.data;
|
||||
_paymentsMap = {};
|
||||
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();
|
||||
}
|
||||
|
||||
@@ -88,7 +225,6 @@ function renderComandasTable(data) {
|
||||
<th>Status</th>
|
||||
<th>Total</th>
|
||||
<th>Aberta em</th>
|
||||
<th>Itens</th>
|
||||
<th>Ações</th>
|
||||
</tr></thead>
|
||||
<tbody>
|
||||
@@ -100,29 +236,33 @@ function renderComandasTable(data) {
|
||||
};
|
||||
const cfg = statusCfg[c.status] || { label: c.status, badge: 'badge-muted' };
|
||||
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>${c.name || '–'}</td>
|
||||
<td>${c.mesa_name || `Mesa ${c.mesa}` || '–'}</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>
|
||||
<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>
|
||||
<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 && 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>` : ''}
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>`;
|
||||
}).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>` : ''}
|
||||
`;
|
||||
|
||||
// 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
|
||||
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));
|
||||
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)
|
||||
wrap.querySelectorAll('.btn-pagar').forEach(btn => {
|
||||
btn.addEventListener('click', async () => {
|
||||
const r = await window.electronAPI.patch(`/comandas/${btn.dataset.id}/`, { status: 'PAYING' });
|
||||
btn.addEventListener('click', async (e) => {
|
||||
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); }
|
||||
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 => {
|
||||
btn.addEventListener('click', async () => {
|
||||
const r = await window.electronAPI.patch(`/comandas/${btn.dataset.id}/`, { status: 'OPEN' });
|
||||
btn.addEventListener('click', async (e) => {
|
||||
e.stopPropagation();
|
||||
const r = await window.electronAPI.patch(`/comandas/${btn.dataset.id}`, { status: 'OPEN' });
|
||||
if (r.ok) { showToast('Comanda reaberta!', 'info'); loadComandas(_mesasRef); }
|
||||
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
|
||||
wrap.querySelectorAll('.btn-itens').forEach(btn => {
|
||||
btn.addEventListener('click', () => {
|
||||
const comanda = _comandasData.find(c => c.id === parseInt(btn.dataset.id));
|
||||
if (comanda) abrirItensComanda(comanda);
|
||||
const pagamentosAtuais = _paymentsMap[comanda.id] || [];
|
||||
const totalPago = pagamentosAtuais.reduce((acc, p) => acc + parseFloat(p.value || 0), 0);
|
||||
const valorRestante = Math.max(0, totalBruto - totalPago);
|
||||
|
||||
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
|
||||
wrap.querySelectorAll('.badge[data-id]').forEach(badge => {
|
||||
badge.addEventListener('click', () => {
|
||||
const comanda = _comandasData.find(c => c.id === parseInt(badge.dataset.id));
|
||||
if (comanda) abrirItensComanda(comanda);
|
||||
const btnConfirmar = document.getElementById('btn-confirmar-pagamento');
|
||||
|
||||
document.getElementById('pay-value').addEventListener('input', () => {
|
||||
const valorInformado = parseFloat(document.getElementById('pay-value').value) || 0;
|
||||
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)
|
||||
wrap.querySelectorAll('.btn-excluir').forEach(btn => {
|
||||
btn.addEventListener('click', async () => {
|
||||
if (confirm('Deseja realmente EXCLUIR/APAGAR esta comanda?')) {
|
||||
const r = await window.electronAPI.post(`/comandas/${btn.dataset.id}/apagar/`, {});
|
||||
if (r.ok) {
|
||||
showToast('Comanda excluída!', 'success');
|
||||
loadComandas(_mesasRef);
|
||||
btnConfirmar.addEventListener('click', async () => {
|
||||
const payTypeId = parseInt(document.getElementById('pay-type').value);
|
||||
const clientId = document.getElementById('pay-client').value || null;
|
||||
const totalOriginal = parseFloat(document.getElementById('pay-value').value);
|
||||
|
||||
const tipoPgto = document.getElementById('pay-type').options[document.getElementById('pay-type').selectedIndex].text;
|
||||
const isVale = tipoPgto.toLowerCase().includes('vale');
|
||||
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 {
|
||||
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';
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
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');
|
||||
}
|
||||
});
|
||||
} catch (err) {
|
||||
console.error('[PDV] Erro ao abrir modal de pagamento:', err);
|
||||
showToast('Erro ao abrir tela de pagamento.', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
function filtrarComandas() {
|
||||
@@ -263,13 +461,19 @@ function filtrarComandas() {
|
||||
|
||||
// ─── Modal de Itens (Novo Layout PDV Split) ───────────────────────────────────
|
||||
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 podeAdd = comanda.status === 'OPEN'; // Só permite add se ainda não estiver pagando?
|
||||
const podeAdd = comanda.status === 'OPEN';
|
||||
|
||||
|
||||
// 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) : [];
|
||||
|
||||
openModal({
|
||||
@@ -292,16 +496,32 @@ async function abrirItensComanda(comandaIdOrObj) {
|
||||
</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 container = document.getElementById('pdv-items-list');
|
||||
if (!container) return;
|
||||
|
||||
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 = `
|
||||
<div style="flex:1; overflow-y: auto;">
|
||||
@@ -315,17 +535,17 @@ async function abrirItensComanda(comandaIdOrObj) {
|
||||
</tr></thead>
|
||||
<tbody>
|
||||
${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 tooltip = it.obs ? `title="${it.obs}"` : '';
|
||||
|
||||
return `
|
||||
<tr data-item-id="${it.id}">
|
||||
<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 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>
|
||||
${podeAdd ? `
|
||||
<td style="padding:10px 0;text-align:center;border-bottom:1px solid var(--border)">
|
||||
@@ -344,16 +564,50 @@ async function abrirItensComanda(comandaIdOrObj) {
|
||||
}).join('')}
|
||||
</tbody>
|
||||
</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 style="padding-top:20px;margin-top:auto;border-top:2px solid var(--border)">
|
||||
<div style="display:flex;justify-content:space-between;margin-bottom:6px">
|
||||
<span style="color:var(--text-secondary)">Total de Itens:</span>
|
||||
<strong>${itens.length}</strong>
|
||||
</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">
|
||||
<span style="color:var(--text-secondary);font-size:1.1rem">Total da Conta:</span>
|
||||
<strong style="color:var(--success);font-size:1.3rem">R$ ${totalComanda.toFixed(2)}</strong>
|
||||
<span style="color:var(--text-secondary);font-size:1.1rem">${temPagamentos ? 'Restante:' : 'Total:'}</span>
|
||||
<strong style="color:${valorRestante <= 0 ? 'var(--success)' : 'var(--warning)'};font-size:1.3rem">R$ ${Math.max(0, valorRestante).toFixed(2)}</strong>
|
||||
</div>
|
||||
${ativa ? `
|
||||
<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 () => {
|
||||
const itemId = btn.dataset.id;
|
||||
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) {
|
||||
showToast('Item excluído!', 'success');
|
||||
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
|
||||
container.querySelectorAll('.btn-edit-obs').forEach(btn => {
|
||||
btn.addEventListener('click', async () => {
|
||||
const itemId = parseInt(btn.dataset.id);
|
||||
const item = comanda.items.find(it => it.id === itemId);
|
||||
const prod = todosProdutos.find(p => p.id === item.product);
|
||||
const itemId = String(btn.dataset.id);
|
||||
const item = comanda.items.find(it => String(it.id) === itemId);
|
||||
const prod = todosProdutos.find(p => String(p.id) === String(item?.product));
|
||||
|
||||
if (item && prod) {
|
||||
window.abrirModalObsCozinhaGlobal(prod.name, item.obs, async (novaObs) => {
|
||||
if (novaObs === null) return;
|
||||
const r = await window.electronAPI.patch(`/items-comanda/${itemId}/`, { obs: novaObs });
|
||||
if (r.ok) {
|
||||
showToast('Observação atualizada!', 'success');
|
||||
item.obs = novaObs;
|
||||
renderLeft();
|
||||
loadComandas(_mesasRef);
|
||||
} else {
|
||||
showToast(r.error, 'error');
|
||||
window.abrirModalObsCozinhaGlobal(prod.name, item.obs || '', async (novaObs) => {
|
||||
if (novaObs === null || novaObs === item.obs) return;
|
||||
|
||||
// Busca os pedidos para encontrar o ID da order vinculada
|
||||
const ordersRes = await window.electronAPI.get('/orders');
|
||||
if (ordersRes.ok) {
|
||||
const linkedOrder = ordersRes.data.find(o => String(o.productComanda) === itemId);
|
||||
|
||||
if (linkedOrder) {
|
||||
// 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 (!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();
|
||||
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 {
|
||||
showToast(r.error, 'error');
|
||||
}
|
||||
@@ -421,25 +745,34 @@ async function abrirItensComanda(comandaIdOrObj) {
|
||||
container.querySelectorAll('.pdv-product-card').forEach(card => {
|
||||
card.addEventListener('click', async () => {
|
||||
if (!podeAdd) return showToast('Comanda em fechamento ou fechada.', 'warning');
|
||||
const pId = parseInt(card.dataset.id);
|
||||
const prod = todosProdutos.find(x => x.id === pId);
|
||||
const pId = String(card.dataset.id);
|
||||
const prod = todosProdutos.find(x => String(x.id) === pId);
|
||||
|
||||
card.style.transform = 'scale(0.95)';
|
||||
setTimeout(() => card.style.transform = '', 100);
|
||||
|
||||
if (!prod) return showToast('Erro: Produto não encontrado.', 'error');
|
||||
|
||||
if (prod.cuisine) {
|
||||
window.abrirModalObsCozinhaGlobal(prod.name, '', async (obs) => {
|
||||
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,
|
||||
product: pId,
|
||||
obs: obs
|
||||
product: prod.id,
|
||||
obs: obs,
|
||||
applicant: loggedUser?.username || 'Sistema'
|
||||
});
|
||||
processarResultadoAdd(r);
|
||||
processarResultadoAdd(r, prod, obs);
|
||||
});
|
||||
} else {
|
||||
const r = await window.electronAPI.post('/items-comanda/', { comanda: comanda.id, product: pId });
|
||||
processarResultadoAdd(r);
|
||||
const loggedUser = await window.electronAPI.getUser();
|
||||
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);
|
||||
|
||||
// console.log('Produtos carregados no PDV:', todosProdutos);
|
||||
// console.log('Produtos carregados no PDV:', todosProdutos);
|
||||
container.innerHTML = filtrados.map(p => {
|
||||
const imgTarget = p.image ? `url('${p.image}')` : `url('https://wallpapers.com/images/featured/fundo-abstrato-escuro-27kvn4ewpldsngbu.jpg')`;
|
||||
return `
|
||||
@@ -479,67 +812,87 @@ async function abrirItensComanda(comandaIdOrObj) {
|
||||
}
|
||||
|
||||
|
||||
// ─── Modal Nova Comanda ───────────────────────────────────────────────────────
|
||||
function abrirModalNovaComanda(mesas) {
|
||||
// ─── Modal Comanda (Nova / Editar) ──────────────────────────────────────────
|
||||
function abrirModalNovaComanda(mesas, comandaExistente = null) {
|
||||
const isEdit = !!comandaExistente;
|
||||
|
||||
openModal({
|
||||
title: 'Nova Comanda',
|
||||
title: isEdit ? `Editar Comanda #${comandaExistente.id}` : 'Nova Comanda',
|
||||
body: `
|
||||
<form id="form-nova-comanda" class="form-grid">
|
||||
<form id="form-comanda" class="form-grid">
|
||||
<div class="form-group">
|
||||
<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 class="form-group">
|
||||
<label>Mesa</label>
|
||||
<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>
|
||||
</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>`,
|
||||
footer: `
|
||||
<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);
|
||||
|
||||
const submeter = async (e) => {
|
||||
if (e) e.preventDefault();
|
||||
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();
|
||||
|
||||
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 payload = {
|
||||
name: nome,
|
||||
mesa: mesaId,
|
||||
user: loggedUser?.id || 1,
|
||||
status: 'OPEN'
|
||||
client: clientId,
|
||||
name: nome,
|
||||
};
|
||||
|
||||
const btn = document.getElementById('btn-criar-comanda');
|
||||
btn.disabled = true;
|
||||
btn.textContent = 'Criando...';
|
||||
let r;
|
||||
if (isEdit) {
|
||||
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) {
|
||||
showToast('Comanda criada!', 'success');
|
||||
showToast(isEdit ? 'Comanda atualizada!' : 'Comanda criada!', 'success');
|
||||
closeModal();
|
||||
loadComandas(_mesasRef);
|
||||
// Abre direto a modal de itens da comanda recém criada
|
||||
setTimeout(() => abrirItensComanda(r.data), 300);
|
||||
if (!isEdit) {
|
||||
setTimeout(() => abrirItensComanda(r.data), 300);
|
||||
}
|
||||
} else {
|
||||
showToast(r.error, 'error');
|
||||
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('btn-criar-comanda').onclick = submeter;
|
||||
document.getElementById('form-comanda').onsubmit = submeter;
|
||||
document.getElementById('btn-salvar-comanda').onclick = submeter;
|
||||
}
|
||||
|
||||
// ─── Helpers ──────────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
export async function renderConfig(container) {
|
||||
const currentUrl = await window.electronAPI.getConfigUrl();
|
||||
const printSilent = await window.electronAPI.getPrintSilent();
|
||||
|
||||
container.innerHTML = `
|
||||
<div class="page-header">
|
||||
@@ -26,6 +27,24 @@ export async function renderConfig(container) {
|
||||
</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);">
|
||||
<h3 style="margin-bottom: 10px; color: var(--text-primary);">ℹ️ Sobre o Sistema</h3>
|
||||
<p style="color: var(--text-secondary); font-size: 0.85rem;">
|
||||
@@ -36,7 +55,7 @@ export async function renderConfig(container) {
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Salvar
|
||||
// Salvar config de API
|
||||
document.getElementById('btn-save-config').addEventListener('click', async () => {
|
||||
const newUrl = document.getElementById('config-api-url').value.trim();
|
||||
if (!newUrl.startsWith('http')) {
|
||||
@@ -51,11 +70,22 @@ export async function renderConfig(container) {
|
||||
}
|
||||
});
|
||||
|
||||
// Restaurar
|
||||
// Restaurar URL
|
||||
document.getElementById('btn-reset-url').addEventListener('click', async () => {
|
||||
const defaultUrl = 'http://localhost:8000/api/v1';
|
||||
document.getElementById('config-api-url').value = defaultUrl;
|
||||
await window.electronAPI.setConfigUrl(defaultUrl);
|
||||
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
|
||||
const [mesasRes, comandasRes] = await Promise.all([
|
||||
window.electronAPI.get('/mesas/'),
|
||||
window.electronAPI.get('/comandas/'),
|
||||
window.electronAPI.get('/mesas'),
|
||||
window.electronAPI.get('/comandas'),
|
||||
]);
|
||||
|
||||
if (!mesasRes.ok) {
|
||||
@@ -103,7 +103,7 @@ function abrirDetalheMesa(mesa, ocupada) {
|
||||
});
|
||||
|
||||
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(); }
|
||||
else showToast(r.error, 'error');
|
||||
});
|
||||
@@ -145,8 +145,8 @@ function abrirModalMesa(mesa = null) {
|
||||
if (!data.name) return showToast('Informe o nome da mesa.', 'error');
|
||||
|
||||
const r = isEdit
|
||||
? await window.electronAPI.put(`/mesas/${mesa.id}/`, data)
|
||||
: await window.electronAPI.post('/mesas/', data);
|
||||
? await window.electronAPI.put(`/mesas/${mesa.id}`, data)
|
||||
: await window.electronAPI.post('/mesas', data);
|
||||
|
||||
if (r.ok) { showToast(isEdit ? 'Mesa atualizada!' : 'Mesa criada!', 'success'); closeModal(); loadMesas(); }
|
||||
else showToast(r.error, 'error');
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
export async function renderPagamentos(container) {
|
||||
// Carrega tipos de pagamento para o formulário de novo registro
|
||||
let tiposPag = [], comandas = [];
|
||||
const [tRes, cRes] = await Promise.all([
|
||||
window.electronAPI.get('/payment-types/'),
|
||||
window.electronAPI.get('/comandas/'),
|
||||
window.electronAPI.get('/payment-types'),
|
||||
window.electronAPI.get('/comandas'),
|
||||
]);
|
||||
if (tRes.ok) tiposPag = tRes.data;
|
||||
if (cRes.ok) comandas = cRes.data;
|
||||
const tiposPag = tRes.ok ? tRes.data : [];
|
||||
const comandas = cRes.ok ? cRes.data : [];
|
||||
|
||||
container.innerHTML = `
|
||||
<div class="page-header">
|
||||
@@ -42,13 +41,22 @@ async function loadPagamentos(tiposPag, comandas) {
|
||||
const wrap = document.getElementById('pagamentos-table');
|
||||
if (!wrap) return;
|
||||
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; }
|
||||
_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');
|
||||
if (!wrap) return;
|
||||
|
||||
@@ -73,25 +81,29 @@ function renderPagsTable(data) {
|
||||
<th>Ações</th>
|
||||
</tr></thead>
|
||||
<tbody>
|
||||
${data.map(p => `<tr>
|
||||
<td style="color:var(--text-muted)">#${p.id}</td>
|
||||
<td>${p.client_name || '–'}</td>
|
||||
<td>
|
||||
${p.comanda ? `<span style="font-size:0.8rem">
|
||||
<span style="color:var(--text-muted)">#${p.comanda}</span>
|
||||
${p.comanda_name ? `<span style="color:var(--text-secondary)"> ${p.comanda_name}</span>` : ''}
|
||||
</span>` : '–'}
|
||||
</td>
|
||||
<td><span class="badge badge-info">${p.type_pay_name || '–'}</span></td>
|
||||
<td><strong style="color:var(--success)">R$ ${parseFloat(p.value || 0).toFixed(2)}</strong></td>
|
||||
<td style="max-width:180px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--text-secondary);font-size:0.82rem">
|
||||
${p.description || '–'}
|
||||
</td>
|
||||
<td style="white-space:nowrap;font-size:0.82rem">${formatDate(p.datetime)}</td>
|
||||
<td>
|
||||
<button class="btn btn-danger btn-sm btn-del-pag" data-id="${p.id}">Excluir</button>
|
||||
</td>
|
||||
</tr>`).join('')}
|
||||
${data.map(p => {
|
||||
const cDesc = cmdMap[String(p.comanda)] || p.comanda_name || '–';
|
||||
return `
|
||||
<tr>
|
||||
<td style="color:var(--text-muted)">#${p.id}</td>
|
||||
<td>${p.client_name || '–'}</td>
|
||||
<td>
|
||||
${p.comanda ? `<span style="font-size:0.8rem">
|
||||
<span style="color:var(--text-muted)">#${p.comanda}</span>
|
||||
<span style="color:var(--text-secondary)"> ${cDesc}</span>
|
||||
</span>` : '–'}
|
||||
</td>
|
||||
<td><span class="badge badge-info">${p.type_pay_name || '–'}</span></td>
|
||||
<td><strong style="color:var(--success)">R$ ${parseFloat(p.value || 0).toFixed(2)}</strong></td>
|
||||
<td style="max-width:180px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--text-secondary);font-size:0.82rem">
|
||||
${p.description || '–'}
|
||||
</td>
|
||||
<td style="white-space:nowrap;font-size:0.82rem">${formatDate(p.datetime)}</td>
|
||||
<td>
|
||||
<button class="btn btn-danger btn-sm btn-del-pag" data-id="${p.id}">Excluir</button>
|
||||
</td>
|
||||
</tr>`;
|
||||
}).join('')}
|
||||
</tbody>
|
||||
</table>
|
||||
<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 =>
|
||||
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([], []); }
|
||||
else showToast(r.error, 'error');
|
||||
})
|
||||
@@ -172,7 +184,7 @@ function abrirModalPagamento(tiposPag, comandas) {
|
||||
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); }
|
||||
else showToast(r.error, 'error');
|
||||
});
|
||||
|
||||
@@ -2,13 +2,21 @@
|
||||
// Cada "order" representa um item individual na fila, com pipeline de status por timestamps
|
||||
|
||||
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' },
|
||||
'Pronto': { badge: 'badge-success', icon: '✅', next: 'delivered', nextLabel: '🚀 Entregue' },
|
||||
'Entregue': { badge: 'badge-muted', 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) {
|
||||
container.innerHTML = `
|
||||
<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..." />
|
||||
<select id="filter-status-order" class="form-control" style="width:160px">
|
||||
<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="Pronto">✅ Pronto</option>
|
||||
<option value="Entregue">🚀 Entregue</option>
|
||||
@@ -43,19 +51,42 @@ export async function renderPedidos(container) {
|
||||
}
|
||||
|
||||
let _ordersData = [];
|
||||
let _productsMap = {};
|
||||
let _comandasMap = {};
|
||||
let _mesasMap = {};
|
||||
|
||||
async function loadOrders() {
|
||||
const wrap = document.getElementById('orders-table');
|
||||
if (!wrap) return;
|
||||
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; }
|
||||
|
||||
// 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;
|
||||
|
||||
// Por padrão, filtra para mostrar só os não entregues/cancelados
|
||||
const filtroInicial = document.getElementById('filter-status-order');
|
||||
if (filtroInicial && !filtroInicial.value) {
|
||||
// Mantém filtro vazio mas é renderizado completo
|
||||
// Mantém o filtro se necessário
|
||||
}
|
||||
renderOrdersTable(_ordersData);
|
||||
}
|
||||
@@ -79,13 +110,19 @@ function renderOrdersTable(data) {
|
||||
</tr></thead>
|
||||
<tbody>
|
||||
${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>
|
||||
<td style="color:var(--text-muted)">#${o.id}</td>
|
||||
<td><strong>${o.product_name || '–'}</strong></td>
|
||||
<td style="font-size:0.82rem;color:var(--text-secondary)">${o.comanda_name || '–'}</td>
|
||||
<td style="font-size:0.82rem">${o.mesa_name || '–'}</td>
|
||||
<td><span class="badge ${cfg.badge}">${cfg.icon} ${o.status}</span></td>
|
||||
<td><strong>${prodName}</strong></td>
|
||||
<td style="font-size:0.82rem;color:var(--text-secondary)">${comandaInfo.name}</td>
|
||||
<td style="font-size:0.82rem">${comandaInfo.mesa}</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.8rem;color:var(--text-secondary);max-width:140px;">
|
||||
<div style="display:flex;align-items:center;gap:4px">
|
||||
@@ -107,9 +144,9 @@ function renderOrdersTable(data) {
|
||||
</table>
|
||||
<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.filter(o => o.status === 'Em espera').length} em espera
|
||||
· 🍳 ${data.filter(o => o.status === 'Preparando').length} preparando
|
||||
· ✅ ${data.filter(o => o.status === 'Pronto').length} prontos
|
||||
· ⏳ ${data.filter(o => getOrderStatus(o) === 'Na fila').length} na fila
|
||||
· 🍳 ${data.filter(o => getOrderStatus(o) === 'Preparando').length} preparando
|
||||
· ✅ ${data.filter(o => getOrderStatus(o) === 'Pronto').length} prontos
|
||||
</div>`;
|
||||
|
||||
// Avançar status para próxima etapa
|
||||
@@ -117,7 +154,7 @@ function renderOrdersTable(data) {
|
||||
btn.addEventListener('click', async () => {
|
||||
const nowISO = new Date().toISOString();
|
||||
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(); }
|
||||
else showToast(r.error, 'error');
|
||||
})
|
||||
@@ -126,7 +163,7 @@ function renderOrdersTable(data) {
|
||||
// Cancelar pedido
|
||||
wrap.querySelectorAll('.btn-cancela').forEach(btn =>
|
||||
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(); }
|
||||
else showToast(r.error, 'error');
|
||||
})
|
||||
@@ -144,7 +181,7 @@ function renderOrdersTable(data) {
|
||||
window.abrirModalObsCozinhaGlobal(productName, currentObs, async (novaObs) => {
|
||||
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) {
|
||||
showToast('Observação atualizada!', 'success');
|
||||
loadOrders();
|
||||
@@ -167,7 +204,7 @@ function filtrarOrders() {
|
||||
(o.comanda_name || '').toLowerCase().includes(q) ||
|
||||
(o.mesa_name || '').toLowerCase().includes(q) ||
|
||||
(o.obs || '').toLowerCase().includes(q);
|
||||
const matchStatus = !status || o.status === status;
|
||||
const matchStatus = !status || getOrderStatus(o) === status;
|
||||
return matchQ && matchStatus;
|
||||
});
|
||||
renderOrdersTable(filtered);
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
export async function renderProdutos(container) {
|
||||
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 (unRes.ok) unidades = unRes.data;
|
||||
|
||||
container.innerHTML = `
|
||||
<div class="page-header">
|
||||
@@ -10,7 +15,7 @@ export async function renderProdutos(container) {
|
||||
<div class="page-subtitle">Cardápio e categorias</div>
|
||||
</div>
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
@@ -30,28 +35,28 @@ export async function renderProdutos(container) {
|
||||
<div id="produtos-table"></div>
|
||||
</div>`;
|
||||
|
||||
await loadProdutos(categorias);
|
||||
await loadProdutos(categorias, unidades);
|
||||
|
||||
document.getElementById('btn-novo-prod').addEventListener('click', () => abrirModalProduto(null, categorias));
|
||||
document.getElementById('btn-nova-cat').addEventListener('click', () => abrirModalCategoria());
|
||||
document.getElementById('search-produto').addEventListener('input', () => filtrarProdutos(categorias));
|
||||
document.getElementById('filter-cat').addEventListener('change', () => filtrarProdutos(categorias));
|
||||
document.getElementById('filter-ativo').addEventListener('change', () => filtrarProdutos(categorias));
|
||||
document.getElementById('btn-novo-prod').addEventListener('click', () => abrirModalProduto(null, categorias, unidades));
|
||||
document.getElementById('btn-gerenciar-cat').addEventListener('click', () => abrirModalGerenciarCategorias(categorias));
|
||||
document.getElementById('search-produto').addEventListener('input', () => filtrarProdutos(categorias, unidades));
|
||||
document.getElementById('filter-cat').addEventListener('change', () => filtrarProdutos(categorias, unidades));
|
||||
document.getElementById('filter-ativo').addEventListener('change', () => filtrarProdutos(categorias, unidades));
|
||||
}
|
||||
|
||||
let _produtosData = [];
|
||||
|
||||
async function loadProdutos(categorias) {
|
||||
async function loadProdutos(categorias, unidades) {
|
||||
const wrap = document.getElementById('produtos-table');
|
||||
if (!wrap) return;
|
||||
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; }
|
||||
_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');
|
||||
if (!wrap) return;
|
||||
if (!data.length) { wrap.innerHTML = `<div class="table-empty">Nenhum produto encontrado.</div>`; return; }
|
||||
@@ -87,14 +92,16 @@ function renderProdutosTable(data, categorias) {
|
||||
</span>
|
||||
</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'}
|
||||
</span>
|
||||
</button>
|
||||
</td>
|
||||
<td>
|
||||
<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-danger btn-sm btn-del-prod" data-id="${p.id}">Excluir</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>`).join('')}
|
||||
@@ -103,21 +110,27 @@ function renderProdutosTable(data, categorias) {
|
||||
|
||||
wrap.querySelectorAll('.btn-edit-prod').forEach(btn => {
|
||||
btn.addEventListener('click', () => {
|
||||
const p = _produtosData.find(x => x.id === parseInt(btn.dataset.id));
|
||||
if (p) abrirModalProduto(p, categorias);
|
||||
const p = _produtosData.find(x => String(x.id) === String(btn.dataset.id));
|
||||
if (p) abrirModalProduto(p, categorias, unidades);
|
||||
});
|
||||
});
|
||||
|
||||
wrap.querySelectorAll('.btn-del-prod').forEach(btn =>
|
||||
wrap.querySelectorAll('.btn-status-prod').forEach(btn => {
|
||||
btn.addEventListener('click', async () => {
|
||||
const r = await window.electronAPI.delete(`/products/${btn.dataset.id}/`);
|
||||
if (r.ok) { showToast('Produto excluído!', 'success'); loadProdutos(categorias); }
|
||||
else showToast(r.error, 'error');
|
||||
})
|
||||
);
|
||||
const id = btn.dataset.id;
|
||||
const currentActive = btn.dataset.active === 'true';
|
||||
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 catId = parseInt(document.getElementById('filter-cat')?.value) || null;
|
||||
const ativo = document.getElementById('filter-ativo')?.value;
|
||||
@@ -128,10 +141,10 @@ function filtrarProdutos(categorias) {
|
||||
const matchAtiv = !ativo || String(p.active) === ativo;
|
||||
return matchQ && matchCat && matchAtiv;
|
||||
});
|
||||
renderProdutosTable(filtered, categorias);
|
||||
renderProdutosTable(filtered, categorias, unidades);
|
||||
}
|
||||
|
||||
function abrirModalProduto(produto, categorias) {
|
||||
function abrirModalProduto(produto, categorias, unidades) {
|
||||
const isEdit = !!produto;
|
||||
openModal({
|
||||
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('')}
|
||||
</select>
|
||||
</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">
|
||||
<label>Ativo</label>
|
||||
<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" />
|
||||
</div>
|
||||
<div class="form-group" style="grid-column:1/-1">
|
||||
<label>Imagem</label>
|
||||
<input type="file" id="prod-img" accept="image/*" class="form-control" />
|
||||
<label>URL da Imagem</label>
|
||||
<input type="text" id="prod-img" class="form-control" value="${produto?.image || ''}" placeholder="http://..." />
|
||||
</div>
|
||||
</div>`,
|
||||
footer: `
|
||||
<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 () => {
|
||||
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 = {
|
||||
name: document.getElementById('prod-nome').value,
|
||||
description: document.getElementById('prod-desc').value,
|
||||
//image: document.getElementById('prod-img').value,
|
||||
name: document.getElementById('prod-nome').value.trim(),
|
||||
description: document.getElementById('prod-desc').value.trim(),
|
||||
price: parseFloat(document.getElementById('prod-preco').value) || 0,
|
||||
quantity: parseInt(document.getElementById('prod-qty').value) || 0,
|
||||
category: catVal,
|
||||
active: document.getElementById('prod-ativo').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
|
||||
? await window.electronAPI.put(`/products/${produto.id}/`, 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');
|
||||
? await window.electronAPI.patch(`/products/${produto.id}`, data)
|
||||
: await window.electronAPI.post('/products', data);
|
||||
|
||||
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({
|
||||
title: 'Nova Categoria',
|
||||
title: '📁 Gerenciar Categorias',
|
||||
body: `
|
||||
<div class="form-group">
|
||||
<label>Nome</label>
|
||||
<input type="text" id="cat-nome" class="form-control" placeholder="Ex: Bebidas, Lanches..." />
|
||||
<div style="margin-bottom:20px">
|
||||
<button class="btn btn-primary btn-sm" id="btn-cat-add-new">+ Adicionar Nova</button>
|
||||
</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>`,
|
||||
footer: `
|
||||
<button class="btn btn-secondary btn-md" onclick="closeModal()">Cancelar</button>
|
||||
<button class="btn btn-primary btn-md" id="btn-criar-cat">Criar</button>`,
|
||||
footer: `<button class="btn btn-secondary btn-md" onclick="closeModal()">Fechar</button>`,
|
||||
});
|
||||
document.getElementById('btn-criar-cat').addEventListener('click', async () => {
|
||||
const nome = document.getElementById('cat-nome').value.trim();
|
||||
if (!nome) return showToast('Informe um nome.', 'error');
|
||||
const r = await window.electronAPI.post('/categories/', { nome, name: nome });
|
||||
if (r.ok) { showToast('Categoria criada!', 'success'); closeModal(); }
|
||||
else showToast(r.error, 'error');
|
||||
|
||||
updateTable();
|
||||
|
||||
document.getElementById('btn-cat-add-new').addEventListener('click', () => {
|
||||
abrirFormularioCategoria(null, updateTable, async (novasCats) => {
|
||||
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);
|
||||
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