Compare commits

...

1 Commits

Author SHA1 Message Date
eb62b890cf db produção 2026-04-30 15:34:54 -03:00
9 changed files with 300 additions and 168 deletions

View File

@@ -0,0 +1,62 @@
/**
* API helpers for handling paginated responses from Go server.
*
* Server now returns: { data: [...], total, page, limit, total_pages }
* Max limit per page: 200
*/
const MAX_PAGE_SIZE = 200;
export function buildQuery(params = {}) {
const parts = [];
for (const [key, value] of Object.entries(params)) {
if (value !== null && value !== undefined && value !== '') {
parts.push(`${encodeURIComponent(key)}=${encodeURIComponent(value)}`);
}
}
return parts.length ? `?${parts.join('&')}` : '';
}
export function extractData(res) {
if (!res.ok) return [];
const body = res.data;
if (Array.isArray(body)) return body;
if (body && Array.isArray(body.data)) return body.data;
return [];
}
export function extractPagination(res) {
if (!res.ok || !res.data) return { total: 0, page: 1, limit: 50, total_pages: 1 };
const body = res.data;
if (body && typeof body === 'object' && 'total' in body) {
return {
total: body.total || 0,
page: body.page || 1,
limit: body.limit || 50,
total_pages: body.total_pages || 1
};
}
const arr = Array.isArray(body) ? body : (body.data || []);
return { total: arr.length, page: 1, limit: arr.length, total_pages: 1 };
}
export async function fetchAllProducts() {
let allData = [];
let page = 1;
let totalPages = 1;
while (page <= totalPages) {
const params = { page, limit: MAX_PAGE_SIZE };
const res = await window.electronAPI.get(`/products${buildQuery(params)}`);
if (!res.ok) break;
allData = allData.concat(extractData(res));
const meta = extractPagination(res);
totalPages = meta.total_pages;
page++;
if (allData.length >= meta.total) break;
}
return allData;
}

View File

@@ -1,3 +1,5 @@
import { extractData, fetchAllProducts } from '../api-helpers.js';
export async function renderClientes(container) { export async function renderClientes(container) {
container.innerHTML = ` container.innerHTML = `
<div class="page-header"> <div class="page-header">
@@ -41,31 +43,29 @@ async function loadClientes() {
if (!wrap) return; if (!wrap) return;
wrap.innerHTML = `<div class="loading-screen"><div class="spinner"></div></div>`; wrap.innerHTML = `<div class="loading-screen"><div class="spinner"></div></div>`;
const [res, pRes, cRes, ptRes, pagsRes] = await Promise.all([ const [res, products, cRes, ptRes, pagsRes] = await Promise.all([
window.electronAPI.get('/clients'), window.electronAPI.get('/clients?page=1&limit=200'),
window.electronAPI.get('/products'), fetchAllProducts(),
window.electronAPI.get('/comandas'), window.electronAPI.get('/comandas?page=1&limit=200'),
window.electronAPI.get('/payment-types'), window.electronAPI.get('/payment-types?page=1&limit=200'),
window.electronAPI.get('/payments') window.electronAPI.get('/payments?page=1&limit=200')
]); ]);
if (ptRes.ok) _paymentTypes = ptRes.data; if (ptRes.ok) _paymentTypes = extractData(ptRes);
if (pRes.ok) { _productsMap = (products || []).reduce((acc, p) => {
_productsMap = pRes.data.reduce((acc, p) => {
acc[String(p.id)] = { acc[String(p.id)] = {
name: p.name, name: p.name,
price: parseFloat(p.price || 0) price: parseFloat(p.price || 0)
}; };
return acc; return acc;
}, {}); }, {});
}
_comandasData = cRes.ok ? (cRes.data || []) : []; _comandasData = cRes.ok ? extractData(cRes) : [];
_paymentsMap = {}; _paymentsMap = {};
if (pagsRes.ok) { if (pagsRes.ok) {
(pagsRes.data || []).forEach(p => { (extractData(pagsRes) || []).forEach(p => {
if (p.comanda) { if (p.comanda) {
if (!_paymentsMap[p.comanda]) _paymentsMap[p.comanda] = []; if (!_paymentsMap[p.comanda]) _paymentsMap[p.comanda] = [];
_paymentsMap[p.comanda].push(p); _paymentsMap[p.comanda].push(p);
@@ -75,7 +75,7 @@ async function loadClientes() {
if (!res.ok) { wrap.innerHTML = `<div class="table-empty">Erro ao carregar clientes.</div>`; return; } if (!res.ok) { wrap.innerHTML = `<div class="table-empty">Erro ao carregar clientes.</div>`; return; }
_clientesData = res.data || []; _clientesData = extractData(res);
_clientesData.forEach(c => { _clientesData.forEach(c => {
const fiados = _comandasData.filter(com => { const fiados = _comandasData.filter(com => {

View File

@@ -1,3 +1,5 @@
import { extractData, extractPagination, buildQuery, fetchAllProducts } from '../api-helpers.js';
function imprimirComanda(comanda, pagamentosComanda, totalComanda) { function imprimirComanda(comanda, pagamentosComanda, totalComanda) {
const itens = comanda.items || []; const itens = comanda.items || [];
const totalPago = (pagamentosComanda || []).reduce((acc, p) => acc + parseFloat(p.value || 0), 0); const totalPago = (pagamentosComanda || []).reduce((acc, p) => acc + parseFloat(p.value || 0), 0);
@@ -241,13 +243,18 @@ export async function renderComandas(container) {
</div>`; </div>`;
let mesas = []; let mesas = [];
const mesasRes = await window.electronAPI.get('/mesas'); const mesasRes = await window.electronAPI.get('/mesas?page=1&limit=200');
if (mesasRes.ok) mesas = mesasRes.data; if (mesasRes.ok) mesas = extractData(mesasRes);
await loadComandas(mesas); await loadComandas(mesas);
document.getElementById('btn-nova-comanda').addEventListener('click', () => abrirModalNovaComanda(mesas)); document.getElementById('btn-nova-comanda').addEventListener('click', () => abrirModalNovaComanda(mesas));
document.getElementById('search-comanda').addEventListener('input', () => filtrarComandas());
let searchTimeout;
document.getElementById('search-comanda').addEventListener('input', () => {
clearTimeout(searchTimeout);
searchTimeout = setTimeout(() => filtrarComandas(), 300);
});
document.getElementById('filter-status').addEventListener('change', () => filtrarComandas()); document.getElementById('filter-status').addEventListener('change', () => filtrarComandas());
} }
@@ -258,38 +265,48 @@ let _productsNames = {}; // Cache de nomes {id: name}
let _paymentTypes = []; let _paymentTypes = [];
let _clients = []; let _clients = [];
let _paymentsMap = {}; // Cache de pagamentos por comanda {comandaId: [pagamentos]} let _paymentsMap = {}; // Cache de pagamentos por comanda {comandaId: [pagamentos]}
let _comandaPage = 1;
let _comandaTotal = 0;
let _comandaTotalPages = 1;
const COMANDA_PAGE_SIZE = 50;
async function loadComandas(mesas) { async function loadComandas(mesas, page = 1) {
_mesasRef = mesas; _mesasRef = mesas;
_comandaPage = page;
const wrap = document.getElementById('comandas-table'); const wrap = document.getElementById('comandas-table');
if (!wrap) return; if (!wrap) return;
wrap.innerHTML = `<div class="loading-screen"><div class="spinner"></div></div>`; wrap.innerHTML = `<div class="loading-screen"><div class="spinner"></div></div>`;
const [res, pRes, ptRes, cRes, pagsRes] = await Promise.all([ const q = document.getElementById('search-comanda')?.value.toLowerCase() || '';
window.electronAPI.get('/comandas'), const status = document.getElementById('filter-status')?.value || '';
window.electronAPI.get('/products'),
window.electronAPI.get('/payment-types'), const queryParams = { page, limit: COMANDA_PAGE_SIZE };
window.electronAPI.get('/clients'), if (q) queryParams.name = q;
window.electronAPI.get('/payments') if (status) queryParams.status = status === 'ACTIVE' ? 'OPEN' : status;
const [res, ptRes, cRes, pagsRes, products] = await Promise.all([
window.electronAPI.get(`/comandas${buildQuery(queryParams)}`),
window.electronAPI.get('/payment-types?page=1&limit=200'),
window.electronAPI.get('/clients?page=1&limit=200'),
window.electronAPI.get('/payments?page=1&limit=200'),
fetchAllProducts()
]); ]);
if (ptRes.ok) _paymentTypes = ptRes.data; if (ptRes.ok) _paymentTypes = extractData(ptRes);
if (cRes.ok) _clients = cRes.data; if (cRes.ok) _clients = extractData(cRes);
if (pRes.ok) { _productsMap = (products || []).reduce((acc, p) => {
_productsMap = pRes.data.reduce((acc, p) => {
acc[String(p.id)] = parseFloat(p.price || 0); acc[String(p.id)] = parseFloat(p.price || 0);
return acc; return acc;
}, {}); }, {});
_productsNames = pRes.data.reduce((acc, p) => { _productsNames = (products || []).reduce((acc, p) => {
acc[String(p.id)] = p.name || `Produto #${p.id}`; acc[String(p.id)] = p.name || `Produto #${p.id}`;
return acc; return acc;
}, {}); }, {});
}
_paymentsMap = {}; _paymentsMap = {};
if (pagsRes.ok) { if (pagsRes.ok) {
(pagsRes.data || []).forEach(p => { (extractData(pagsRes) || []).forEach(p => {
if (p.comanda) { if (p.comanda) {
if (!_paymentsMap[p.comanda]) _paymentsMap[p.comanda] = []; if (!_paymentsMap[p.comanda]) _paymentsMap[p.comanda] = [];
_paymentsMap[p.comanda].push(p); _paymentsMap[p.comanda].push(p);
@@ -298,20 +315,22 @@ async function loadComandas(mesas) {
} }
if (!res.ok) { wrap.innerHTML = `<div class="table-empty">Erro ao carregar comandas.</div>`; return; } if (!res.ok) { wrap.innerHTML = `<div class="table-empty">Erro ao carregar comandas.</div>`; return; }
_comandasData = res.ok ? res.data : []; _comandasData = extractData(res);
_comandasData.reverse(); const meta = extractPagination(res);
_comandaTotal = meta.total;
_comandaTotalPages = meta.total_pages;
console.log('[PDV] Comandas carregadas do servidor:', _comandasData); console.log('[PDV] Comandas carregadas do servidor:', _comandasData);
filtrarComandas(); renderComandasTable(_comandasData);
} }
function renderComandasTable(data) { function renderComandasTable(data) {
const wrap = document.getElementById('comandas-table'); const wrap = document.getElementById('comandas-table');
if (!wrap) return; if (!wrap) return;
if (!data.length) { wrap.innerHTML = `<div class="table-empty">Nenhuma comanda encontrada.</div>`; return; } if (!data.length) {
wrap.innerHTML = `<div class="table-empty">Nenhuma comanda encontrada.</div>${renderPaginationControls()}`;
// Limita a exibição às primeiras 100 comandas return;
const limitedData = data.slice(0, 100); }
wrap.innerHTML = ` wrap.innerHTML = `
<table> <table>
@@ -365,10 +384,54 @@ function renderComandasTable(data) {
}).join('')} }).join('')}
</tbody> </tbody>
</table> </table>
${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>` : ''} ${renderPaginationControls()}
`; `;
// Listener para linha toda bindComandaTableListeners();
}
function renderPaginationControls() {
if (_comandaTotalPages <= 1) return '';
const startItem = (_comandaPage - 1) * COMANDA_PAGE_SIZE + 1;
const endItem = Math.min(_comandaPage * COMANDA_PAGE_SIZE, _comandaTotal);
let pagesHtml = '';
const maxVisible = 5;
let startPage = Math.max(1, _comandaPage - Math.floor(maxVisible / 2));
let endPage = Math.min(_comandaTotalPages, startPage + maxVisible - 1);
if (endPage - startPage < maxVisible - 1) startPage = Math.max(1, endPage - maxVisible + 1);
if (startPage > 1) {
pagesHtml += `<button class="btn btn-ghost btn-sm btn-pag-page" data-page="1">1</button>`;
if (startPage > 2) pagesHtml += `<span style="color:var(--text-muted)">...</span>`;
}
for (let i = startPage; i <= endPage; i++) {
const active = i === _comandaPage ? 'btn-primary btn-sm' : 'btn-ghost btn-sm';
pagesHtml += `<button class="${active} btn-pag-page" data-page="${i}">${i}</button>`;
}
if (endPage < _comandaTotalPages) {
if (endPage < _comandaTotalPages - 1) pagesHtml += `<span style="color:var(--text-muted)">...</span>`;
pagesHtml += `<button class="btn btn-ghost btn-sm btn-pag-page" data-page="${_comandaTotalPages}">${_comandaTotalPages}</button>`;
}
return `
<div class="pagination-bar" style="display:flex;justify-content:space-between;align-items:center;padding:12px 16px;border-top:1px solid var(--border);margin-top:4px">
<span style="font-size:0.82rem;color:var(--text-muted)">Mostrando ${startItem}${endItem} de ${_comandaTotal}</span>
<div style="display:flex;gap:4px;align-items:center">
<button class="btn btn-ghost btn-sm btn-pag-page" data-page="${_comandaPage - 1}" ${_comandaPage <= 1 ? 'disabled' : ''}>◀</button>
${pagesHtml}
<button class="btn btn-ghost btn-sm btn-pag-page" data-page="${_comandaPage + 1}" ${_comandaPage >= _comandaTotalPages ? 'disabled' : ''}>▶</button>
</div>
</div>`;
}
function bindComandaTableListeners() {
const wrap = document.getElementById('comandas-table');
if (!wrap) return;
wrap.querySelectorAll('.comanda-row').forEach(row => { wrap.querySelectorAll('.comanda-row').forEach(row => {
row.addEventListener('click', () => { row.addEventListener('click', () => {
const comanda = _comandasData.find(c => c.id === parseInt(row.dataset.id)); const comanda = _comandasData.find(c => c.id === parseInt(row.dataset.id));
@@ -376,7 +439,6 @@ function renderComandasTable(data) {
}); });
}); });
// Listener para Receber
wrap.querySelectorAll('.btn-receber').forEach(btn => { wrap.querySelectorAll('.btn-receber').forEach(btn => {
btn.addEventListener('click', (e) => { btn.addEventListener('click', (e) => {
e.stopPropagation(); e.stopPropagation();
@@ -385,7 +447,6 @@ function renderComandasTable(data) {
}); });
}); });
// Listener para Editar
wrap.querySelectorAll('.btn-editar').forEach(btn => { wrap.querySelectorAll('.btn-editar').forEach(btn => {
btn.addEventListener('click', (e) => { btn.addEventListener('click', (e) => {
e.stopPropagation(); e.stopPropagation();
@@ -394,25 +455,32 @@ function renderComandasTable(data) {
}); });
}); });
// Listener para botão "Pagar" (muda p/ PAYING)
wrap.querySelectorAll('.btn-pagar').forEach(btn => { wrap.querySelectorAll('.btn-pagar').forEach(btn => {
btn.addEventListener('click', async (e) => { btn.addEventListener('click', async (e) => {
e.stopPropagation(); e.stopPropagation();
const r = await window.electronAPI.patch(`/comandas/${btn.dataset.id}`, { status: 'PAYING' }); const r = await window.electronAPI.patch(`/comandas/${btn.dataset.id}`, { status: 'PAYING' });
if (r.ok) { showToast('Comanda em fase de pagamento!', 'info'); loadComandas(_mesasRef); } if (r.ok) { showToast('Comanda em fase de pagamento!', 'info'); loadComandas(_mesasRef, _comandaPage); }
else showToast(r.error, 'error'); else showToast(r.error, 'error');
}); });
}); });
// Listener para botão "Reabrir" (muda p/ OPEN)
wrap.querySelectorAll('.btn-reopen').forEach(btn => { wrap.querySelectorAll('.btn-reopen').forEach(btn => {
btn.addEventListener('click', async (e) => { btn.addEventListener('click', async (e) => {
e.stopPropagation(); e.stopPropagation();
const r = await window.electronAPI.patch(`/comandas/${btn.dataset.id}`, { status: 'OPEN' }); const r = await window.electronAPI.patch(`/comandas/${btn.dataset.id}`, { status: 'OPEN' });
if (r.ok) { showToast('Comanda reaberta!', 'info'); loadComandas(_mesasRef); } if (r.ok) { showToast('Comanda reaberta!', 'info'); loadComandas(_mesasRef, _comandaPage); }
else showToast(r.error, 'error'); else showToast(r.error, 'error');
}); });
}); });
wrap.querySelectorAll('.btn-pag-page').forEach(btn => {
btn.addEventListener('click', () => {
const page = parseInt(btn.dataset.page);
if (page >= 1 && page <= _comandaTotalPages && page !== _comandaPage) {
loadComandas(_mesasRef, page);
}
});
});
} }
async function abrirModalPagamento(comanda, onPaymentComplete) { async function abrirModalPagamento(comanda, onPaymentComplete) {
try { try {
@@ -525,7 +593,7 @@ async function abrirModalPagamento(comanda, onPaymentComplete) {
showToast(isVale ? 'Venda registrada como FIADO!' : 'Pagamento realizado!', 'success'); showToast(isVale ? 'Venda registrada como FIADO!' : 'Pagamento realizado!', 'success');
} }
closeModal(); closeModal();
loadComandas(_mesasRef); loadComandas(_mesasRef, _comandaPage);
if (onPaymentComplete) onPaymentComplete(); if (onPaymentComplete) onPaymentComplete();
} catch (err) { } catch (err) {
showToast(err.message, 'error'); showToast(err.message, 'error');
@@ -542,18 +610,7 @@ async function abrirModalPagamento(comanda, onPaymentComplete) {
} }
function filtrarComandas() { function filtrarComandas() {
const q = document.getElementById('search-comanda')?.value.toLowerCase() || ''; loadComandas(_mesasRef, 1);
const status = document.getElementById('filter-status')?.value || '';
const filtered = _comandasData.filter(c => {
const matchQ = !q ||
(c.name || '').toLowerCase().includes(q) ||
(c.mesa_name || '').toLowerCase().includes(q) ||
String(c.id).includes(q);
const matchStatus = !status ||
(status === 'ACTIVE' ? (c.status === 'OPEN' || c.status === 'PAYING') : (c.status === status));
return matchQ && matchStatus;
});
renderComandasTable(filtered);
} }
// ─── Modal de Itens (Novo Layout PDV Split) ─────────────────────────────────── // ─── Modal de Itens (Novo Layout PDV Split) ───────────────────────────────────
@@ -569,11 +626,11 @@ async function abrirItensComanda(comandaIdOrObj) {
const podeAdd = comanda.status === 'OPEN'; const podeAdd = comanda.status === 'OPEN';
// Carrega produtos (ativos) e usuário logado // Carrega produtos (ativos) e usuário logado
const [pRes, loggedUser] = await Promise.all([ const [allProducts, loggedUser] = await Promise.all([
window.electronAPI.get('/products'), fetchAllProducts(),
window.electronAPI.getUser() window.electronAPI.getUser()
]); ]);
let todosProdutos = pRes.ok ? pRes.data.filter(p => p.active) : []; let todosProdutos = (allProducts || []).filter(p => p.active);
openModal({ openModal({
full: true, full: true,
@@ -604,9 +661,9 @@ async function abrirItensComanda(comandaIdOrObj) {
// Carrega pagamentos específicos desta comanda // Carrega pagamentos específicos desta comanda
let pagamentosComanda = _paymentsMap[comanda.id] || []; let pagamentosComanda = _paymentsMap[comanda.id] || [];
if (!pagamentosComanda.length) { if (!pagamentosComanda.length) {
const pagsRes = await window.electronAPI.get('/payments'); const pagsRes = await window.electronAPI.get(`/payments?page=1&limit=200&comanda_id=${comanda.id}`);
if (pagsRes.ok) { if (pagsRes.ok) {
pagamentosComanda = (pagsRes.data || []).filter(p => String(p.comanda) === String(comanda.id)); pagamentosComanda = (extractData(pagsRes) || []).filter(p => String(p.comanda) === String(comanda.id));
} }
} }
@@ -731,7 +788,7 @@ async function abrirItensComanda(comandaIdOrObj) {
showToast('Item excluído!', 'success'); showToast('Item excluído!', 'success');
comanda.items = comanda.items.filter(it => it.id !== parseInt(itemId)); comanda.items = comanda.items.filter(it => it.id !== parseInt(itemId));
renderLeft(); renderLeft();
loadComandas(_mesasRef); loadComandas(_mesasRef, _comandaPage);
} else { } else {
showToast(r.error, 'error'); showToast(r.error, 'error');
} }
@@ -764,9 +821,9 @@ async function abrirItensComanda(comandaIdOrObj) {
if (novaObs === null || novaObs === item.obs) return; if (novaObs === null || novaObs === item.obs) return;
// Busca os pedidos para encontrar o ID da order vinculada // Busca os pedidos para encontrar o ID da order vinculada
const ordersRes = await window.electronAPI.get('/orders'); const ordersRes = await window.electronAPI.get(`/orders?page=1&limit=200&comanda_id=${comanda.id}`);
if (ordersRes.ok) { if (ordersRes.ok) {
const linkedOrder = ordersRes.data.find(o => String(o.productComanda) === itemId); const linkedOrder = extractData(ordersRes).find(o => String(o.productComanda) === itemId);
if (linkedOrder) { if (linkedOrder) {
// PATCH na Order (Cozinha) // PATCH na Order (Cozinha)
@@ -815,7 +872,7 @@ async function abrirItensComanda(comandaIdOrObj) {
if (r.ok) { if (r.ok) {
showToast('Comanda excluída!', 'success'); showToast('Comanda excluída!', 'success');
closeModal(); closeModal();
loadComandas(_mesasRef); loadComandas(_mesasRef, _comandaPage);
} else { } else {
showToast(r.error, 'error'); showToast(r.error, 'error');
} }
@@ -833,7 +890,7 @@ async function abrirItensComanda(comandaIdOrObj) {
comanda.items.push(novoItem); comanda.items.push(novoItem);
renderLeft(); renderLeft();
loadComandas(_mesasRef); loadComandas(_mesasRef, _comandaPage);
if (prod && prod.cuisine) { if (prod && prod.cuisine) {
const orderPayload = { const orderPayload = {
@@ -999,7 +1056,7 @@ function abrirModalNovaComanda(mesas, comandaExistente = null) {
if (r.ok) { if (r.ok) {
showToast(isEdit ? 'Comanda atualizada!' : 'Comanda criada!', 'success'); showToast(isEdit ? 'Comanda atualizada!' : 'Comanda criada!', 'success');
closeModal(); closeModal();
loadComandas(_mesasRef); loadComandas(_mesasRef, 1);
if (!isEdit) { if (!isEdit) {
setTimeout(() => abrirItensComanda(r.data), 300); setTimeout(() => abrirItensComanda(r.data), 300);
} }

View File

@@ -1,3 +1,5 @@
import { extractData } from '../api-helpers.js';
export async function renderDashboard(container) { export async function renderDashboard(container) {
container.innerHTML = ` container.innerHTML = `
<div class="page-header"> <div class="page-header">
@@ -37,26 +39,29 @@ export async function renderDashboard(container) {
// Carrega dados em paralelo // Carrega dados em paralelo
const [mesas, comandas, pedidos, clientes] = await Promise.all([ const [mesas, comandas, pedidos, clientes] = await Promise.all([
window.electronAPI.get('/mesas/'), window.electronAPI.get('/mesas?page=1&limit=200'),
window.electronAPI.get('/comandas/'), window.electronAPI.get('/comandas?page=1&limit=200'),
window.electronAPI.get('/orders/'), window.electronAPI.get('/orders?page=1&limit=200'),
window.electronAPI.get('/clients/'), window.electronAPI.get('/clients?page=1&limit=200'),
]); ]);
const mesasData = mesas.ok ? extractData(mesas) : [];
const comandasData = comandas.ok ? extractData(comandas) : [];
const pedidosData = pedidos.ok ? extractData(pedidos) : [];
const clientesData = clientes.ok ? extractData(clientes) : [];
if (mesas.ok) { if (mesas.ok) {
// Cruza com comandas para verificar ocupação // Cruza com comandas para verificar ocupação
const mesasOcupadas = new Set(); const mesasOcupadas = new Set();
if (comandas.ok) { comandasData.forEach(c => {
comandas.data.forEach(c => {
if ((c.status === 'OPEN' || c.status === 'PAYING') && c.mesa) mesasOcupadas.add(c.mesa); if ((c.status === 'OPEN' || c.status === 'PAYING') && c.mesa) mesasOcupadas.add(c.mesa);
}); });
}
const abertas = mesas.data.filter(m => m.active && mesasOcupadas.has(m.id)).length; const abertas = mesasData.filter(m => m.active && mesasOcupadas.has(m.id)).length;
document.getElementById('stat-mesas').textContent = abertas; document.getElementById('stat-mesas').textContent = abertas;
const preview = document.getElementById('dash-mesas-preview'); const preview = document.getElementById('dash-mesas-preview');
preview.innerHTML = mesas.data.slice(0, 12).map(m => { preview.innerHTML = mesasData.slice(0, 12).map(m => {
const ocupada = mesasOcupadas.has(m.id); const ocupada = mesasOcupadas.has(m.id);
return `<span style="padding:4px 10px;border-radius:6px;font-size:0.78rem;font-weight:600; return `<span style="padding:4px 10px;border-radius:6px;font-size:0.78rem;font-weight:600;
background:${ocupada ? 'rgba(239,68,68,0.15)' : 'rgba(34,197,94,0.12)'}; background:${ocupada ? 'rgba(239,68,68,0.15)' : 'rgba(34,197,94,0.12)'};
@@ -67,12 +72,12 @@ export async function renderDashboard(container) {
} }
if (comandas.ok) { if (comandas.ok) {
document.getElementById('stat-comandas').textContent = comandas.data.filter(c => c.status === 'OPEN' || c.status === 'PAYING').length; document.getElementById('stat-comandas').textContent = comandasData.filter(c => c.status === 'OPEN' || c.status === 'PAYING').length;
const preview = document.getElementById('dash-comandas-preview'); const preview = document.getElementById('dash-comandas-preview');
preview.innerHTML = `<table style="width:100%;font-size:0.85rem"> preview.innerHTML = `<table style="width:100%;font-size:0.85rem">
<thead><tr style="color:var(--text-muted)"><th style="text-align:left;padding:4px 0">ID</th><th style="text-align:left">Nome/Mesa</th><th style="text-align:left">Status</th></tr></thead> <thead><tr style="color:var(--text-muted)"><th style="text-align:left;padding:4px 0">ID</th><th style="text-align:left">Nome/Mesa</th><th style="text-align:left">Status</th></tr></thead>
<tbody> <tbody>
${comandas.data.slice(0, 5).map(c => { ${comandasData.slice(0, 5).map(c => {
const statusLabel = c.status === 'OPEN' ? 'Aberta' : (c.status === 'PAYING' ? 'Pagando' : 'Fechada'); const statusLabel = c.status === 'OPEN' ? 'Aberta' : (c.status === 'PAYING' ? 'Pagando' : 'Fechada');
const badgeClass = c.status === 'OPEN' ? 'badge-success' : (c.status === 'PAYING' ? 'badge-warning' : 'badge-muted'); const badgeClass = c.status === 'OPEN' ? 'badge-success' : (c.status === 'PAYING' ? 'badge-warning' : 'badge-muted');
return ` return `
@@ -88,11 +93,11 @@ export async function renderDashboard(container) {
if (pedidos.ok) { if (pedidos.ok) {
const hoje = new Date().toISOString().slice(0, 10); const hoje = new Date().toISOString().slice(0, 10);
const pedidosHoje = pedidos.data.filter(p => (p.created_at || p.data || '').startsWith(hoje)).length; const pedidosHoje = pedidosData.filter(p => (p.created_at || p.data || '').startsWith(hoje)).length;
document.getElementById('stat-pedidos').textContent = pedidosHoje || pedidos.data.length; document.getElementById('stat-pedidos').textContent = pedidosHoje || pedidosData.length;
} }
if (clientes.ok) { if (clientes.ok) {
document.getElementById('stat-clientes').textContent = clientes.data.length; document.getElementById('stat-clientes').textContent = clientesData.length;
} }
} }

View File

@@ -1,3 +1,5 @@
import { extractData } from '../api-helpers.js';
export async function renderMesas(container) { export async function renderMesas(container) {
container.innerHTML = ` container.innerHTML = `
<div class="page-header"> <div class="page-header">
@@ -28,8 +30,8 @@ async function loadMesas() {
// Carrega mesas e comandas em paralelo para determinar ocupação // Carrega mesas e comandas em paralelo para determinar ocupação
const [mesasRes, comandasRes] = await Promise.all([ const [mesasRes, comandasRes] = await Promise.all([
window.electronAPI.get('/mesas'), window.electronAPI.get('/mesas?page=1&limit=200'),
window.electronAPI.get('/comandas'), window.electronAPI.get('/comandas?page=1&limit=200'),
]); ]);
if (!mesasRes.ok) { if (!mesasRes.ok) {
@@ -37,12 +39,12 @@ async function loadMesas() {
return; return;
} }
const mesas = mesasRes.data; const mesas = extractData(mesasRes);
// IDs de mesas com pelo menos uma comanda ativa (OPEN ou PAYING) // IDs de mesas com pelo menos uma comanda ativa (OPEN ou PAYING)
const mesasOcupadas = new Set(); const mesasOcupadas = new Set();
if (comandasRes.ok) { if (comandasRes.ok) {
comandasRes.data.forEach(c => { extractData(comandasRes).forEach(c => {
if ((c.status === 'OPEN' || c.status === 'PAYING') && c.mesa) mesasOcupadas.add(c.mesa); if ((c.status === 'OPEN' || c.status === 'PAYING') && c.mesa) mesasOcupadas.add(c.mesa);
}); });
} }

View File

@@ -1,12 +1,14 @@
import { extractData } from '../api-helpers.js';
export async function renderPagamentos(container) { export async function renderPagamentos(container) {
const [tRes, cRes, cliRes] = await Promise.all([ const [tRes, cRes, cliRes] = await Promise.all([
window.electronAPI.get('/payment-types'), window.electronAPI.get('/payment-types?page=1&limit=800'),
window.electronAPI.get('/comandas'), window.electronAPI.get('/comandas?page=1&limit=800'),
window.electronAPI.get('/clients'), window.electronAPI.get('/clients?page=1&limit=200'),
]); ]);
const tiposPag = tRes.ok ? tRes.data : []; const tiposPag = tRes.ok ? extractData(tRes) : [];
const comandas = cRes.ok ? cRes.data : []; const comandas = cRes.ok ? extractData(cRes) : [];
const clientes = cliRes.ok ? cliRes.data : []; const clientes = cliRes.ok ? extractData(cliRes) : [];
container.innerHTML = ` container.innerHTML = `
<div class="page-header"> <div class="page-header">
@@ -68,10 +70,10 @@ async function loadPagamentos(tiposPag, comandas, clientes) {
_tiposPag = tiposPag; _tiposPag = tiposPag;
const res = await window.electronAPI.get('/payments'); const res = await window.electronAPI.get('/payments?page=1&limit=800');
if (!res.ok) { wrap.innerHTML = `<div class="table-empty">Erro ao carregar pagamentos.</div>`; return; } if (!res.ok) { wrap.innerHTML = `<div class="table-empty">Erro ao carregar pagamentos.</div>`; return; }
_pagsData = (res.data || []).sort((a, b) => b.id - a.id); _pagsData = (extractData(res) || []).sort((a, b) => b.id - a.id);
_cmdMap = (comandas || []).reduce((acc, c) => { _cmdMap = (comandas || []).reduce((acc, c) => {
acc[String(c.id)] = { acc[String(c.id)] = {

View File

@@ -1,3 +1,5 @@
import { extractData, fetchAllProducts } from '../api-helpers.js';
let _productsMap = {}; let _productsMap = {};
let _productsNames = {}; let _productsNames = {};
let _paymentTypes = []; let _paymentTypes = [];
@@ -85,8 +87,8 @@ export async function renderPdv(container) {
if (searchInput) { if (searchInput) {
searchInput.oninput = async (e) => { searchInput.oninput = async (e) => {
const pRes = await window.electronAPI.get('/products'); const allProducts = await fetchAllProducts();
const todosProdutos = pRes.ok ? pRes.data.filter(p => p.active) : []; const todosProdutos = (allProducts || []).filter(p => p.active);
renderRight(todosProdutos, e.target.value); renderRight(todosProdutos, e.target.value);
}; };
} }
@@ -94,14 +96,14 @@ export async function renderPdv(container) {
} }
async function criarComandaPdv() { async function criarComandaPdv() {
const mesasRes = await window.electronAPI.get('/mesas'); const mesasRes = await window.electronAPI.get('/mesas?page=1&limit=200');
if (mesasRes.ok) { if (mesasRes.ok) {
_mesas = mesasRes.data; _mesas = extractData(mesasRes);
} }
const comandasRes = await window.electronAPI.get('/comandas'); const comandasRes = await window.electronAPI.get('/comandas?page=1&limit=200');
if (comandasRes.ok) { if (comandasRes.ok) {
const existing = comandasRes.data.find(c => c.name === 'PDV-BALCAO' && c.status === 'OPEN'); const existing = extractData(comandasRes).find(c => c.name === 'PDV-BALCAO' && c.status === 'OPEN');
if (existing) { if (existing) {
_pdvComanda = existing; _pdvComanda = existing;
console.log('[PDV] ComandaPDV encontrada:', _pdvComanda); console.log('[PDV] ComandaPDV encontrada:', _pdvComanda);
@@ -135,27 +137,25 @@ async function criarComandaPdv() {
} }
async function loadPdvData() { async function loadPdvData() {
const [pRes, ptRes, cRes] = await Promise.all([ const [products, ptRes, cRes] = await Promise.all([
window.electronAPI.get('/products'), fetchAllProducts(),
window.electronAPI.get('/payment-types'), window.electronAPI.get('/payment-types?page=1&limit=200'),
window.electronAPI.get('/clients') window.electronAPI.get('/clients?page=1&limit=200')
]); ]);
if (pRes.ok) { _productsMap = (products || []).reduce((acc, p) => {
_productsMap = pRes.data.reduce((acc, p) => {
acc[String(p.id)] = parseFloat(p.price || 0); acc[String(p.id)] = parseFloat(p.price || 0);
return acc; return acc;
}, {}); }, {});
_productsNames = pRes.data.reduce((acc, p) => { _productsNames = (products || []).reduce((acc, p) => {
acc[String(p.id)] = p.name || `Produto #${p.id}`; acc[String(p.id)] = p.name || `Produto #${p.id}`;
return acc; return acc;
}, {}); }, {});
}
if (ptRes.ok) _paymentTypes = ptRes.data; if (ptRes.ok) _paymentTypes = extractData(ptRes);
if (cRes.ok) _clients = cRes.data; if (cRes.ok) _clients = extractData(cRes);
const todosProdutos = pRes.ok ? pRes.data.filter(p => p.active) : []; const todosProdutos = (products || []).filter(p => p.active);
renderRight(todosProdutos); renderRight(todosProdutos);
renderLeft(); renderLeft();
} }
@@ -257,7 +257,7 @@ async function recarregarComanda() {
if (!_pdvComanda) return; if (!_pdvComanda) return;
const r = await window.electronAPI.get(`/comandas/${_pdvComanda.id}`); const r = await window.electronAPI.get(`/comandas/${_pdvComanda.id}`);
if (r.ok) { if (r.ok) {
_pdvComanda = r.data; _pdvComanda = r.data.data || r.data;
renderLeft(); renderLeft();
} }
} }

View File

@@ -1,6 +1,8 @@
// Pedidos = Fila de Cozinha (KDS - Kitchen Display System) // Pedidos = Fila de Cozinha (KDS - Kitchen Display System)
// Cada "order" representa um item individual na fila, com pipeline de status por timestamps // Cada "order" representa um item individual na fila, com pipeline de status por timestamps
import { extractData, fetchAllProducts } from '../api-helpers.js';
const STATUS_CONFIG = { const STATUS_CONFIG = {
'Na fila': { badge: 'badge-warning', icon: '⏳', next: 'preparing', nextLabel: '▶ Preparando' }, 'Na fila': { badge: 'badge-warning', icon: '⏳', next: 'preparing', nextLabel: '▶ Preparando' },
'Preparando': { badge: 'badge-info', icon: '🍳', next: 'finished', nextLabel: '✅ Pronto' }, 'Preparando': { badge: 'badge-info', icon: '🍳', next: 'finished', nextLabel: '✅ Pronto' },
@@ -62,21 +64,24 @@ async function loadOrders() {
wrap.innerHTML = `<div class="loading-screen"><div class="spinner"></div></div>`; wrap.innerHTML = `<div class="loading-screen"><div class="spinner"></div></div>`;
// Busca tudo em paralelo para resolver as referências (IDs) // Busca tudo em paralelo para resolver as referências (IDs)
const [res, pRes, cRes, mRes] = await Promise.all([ const [res, products, cRes, mRes] = await Promise.all([
window.electronAPI.get('/orders'), window.electronAPI.get('/orders?page=1&limit=200'),
window.electronAPI.get('/products'), fetchAllProducts(),
window.electronAPI.get('/comandas'), window.electronAPI.get('/comandas?page=1&limit=200'),
window.electronAPI.get('/mesas') window.electronAPI.get('/mesas?page=1&limit=200')
]); ]);
if (!res.ok) { wrap.innerHTML = `<div class="table-empty">Erro ao carregar pedidos.</div>`; return; } if (!res.ok) { wrap.innerHTML = `<div class="table-empty">Erro ao carregar pedidos.</div>`; return; }
if (pRes.ok) { _productsMap = (products || []).reduce((acc, p) => { acc[String(p.id)] = p.name; return acc; }, {});
_productsMap = pRes.data.reduce((acc, p) => { acc[String(p.id)] = p.name; return acc; }, {}); _productsFullMap = (products || []).reduce((acc, p) => { acc[String(p.id)] = p; return acc; }, {});
_productsFullMap = pRes.data.reduce((acc, p) => { acc[String(p.id)] = p; return acc; }, {}); if (mRes.ok) {
const mesas = extractData(mRes);
_mesasMap = mesas.reduce((acc, m) => { acc[String(m.id)] = m.name; return acc; }, {});
} }
if (mRes.ok) _mesasMap = mRes.data.reduce((acc, m) => { acc[String(m.id)] = m.name; return acc; }, {}); if (cRes.ok) {
if (cRes.ok) _comandasMap = cRes.data.reduce((acc, c) => { const comandas = extractData(cRes);
_comandasMap = comandas.reduce((acc, c) => {
acc[String(c.id)] = { acc[String(c.id)] = {
id: c.id, id: c.id,
name: c.name || '', name: c.name || '',
@@ -85,8 +90,9 @@ async function loadOrders() {
}; };
return acc; return acc;
}, {}); }, {});
}
_ordersData = res.data; _ordersData = extractData(res);
// Por padrão, filtra para mostrar só os não entregues/cancelados // Por padrão, filtra para mostrar só os não entregues/cancelados
const filtroInicial = document.getElementById('filter-status-order'); const filtroInicial = document.getElementById('filter-status-order');

View File

@@ -1,12 +1,14 @@
import { extractData, fetchAllProducts } from '../api-helpers.js';
export async function renderProdutos(container) { export async function renderProdutos(container) {
let categorias = []; let categorias = [];
let unidades = []; let unidades = [];
const [catRes, unRes] = await Promise.all([ const [catRes, unRes] = await Promise.all([
window.electronAPI.get('/categories'), window.electronAPI.get('/categories?page=1&limit=200'),
window.electronAPI.get('/unit-of-measurements') window.electronAPI.get('/unit-of-measurements?page=1&limit=200')
]); ]);
if (catRes.ok) categorias = catRes.data; if (catRes.ok) categorias = extractData(catRes);
if (unRes.ok) unidades = unRes.data; if (unRes.ok) unidades = extractData(unRes);
container.innerHTML = ` container.innerHTML = `
<div class="page-header"> <div class="page-header">
@@ -50,9 +52,8 @@ async function loadProdutos(categorias, unidades) {
const wrap = document.getElementById('produtos-table'); const wrap = document.getElementById('produtos-table');
if (!wrap) return; if (!wrap) return;
wrap.innerHTML = `<div class="loading-screen"><div class="spinner"></div></div>`; wrap.innerHTML = `<div class="loading-screen"><div class="spinner"></div></div>`;
const res = await window.electronAPI.get('/products'); const products = await fetchAllProducts();
if (!res.ok) { wrap.innerHTML = `<div class="table-empty">Erro ao carregar produtos.</div>`; return; } _produtosData = products || [];
_produtosData = res.data;
renderProdutosTable(_produtosData, categorias, unidades); renderProdutosTable(_produtosData, categorias, unidades);
} }
@@ -364,11 +365,9 @@ function abrirFormularioCategoria(cat, onSuccess, onListUpdate) {
document.getElementById('btn-cat-form-cancel').onclick = () => { document.getElementById('btn-cat-form-cancel').onclick = () => {
closeModal(); closeModal();
// Reabre o gerenciador
setTimeout(() => { setTimeout(() => {
// Recarregar categorias para garantir sync window.electronAPI.get('/categories?page=1&limit=200').then(res => {
window.electronAPI.get('/categories').then(res => { if (res.ok) onListUpdate(extractData(res));
if (res.ok) onListUpdate(res.data);
}); });
}, 300); }, 300);
}; };
@@ -386,13 +385,12 @@ function abrirFormularioCategoria(cat, onSuccess, onListUpdate) {
if (r.ok) { if (r.ok) {
showToast(isEdit ? 'Categoria atualizada!' : 'Categoria criada!', 'success'); showToast(isEdit ? 'Categoria atualizada!' : 'Categoria criada!', 'success');
const res = await window.electronAPI.get('/categories'); const res = await window.electronAPI.get('/categories?page=1&limit=200');
if (res.ok) { if (res.ok) {
onListUpdate(res.data); const cats = extractData(res);
onListUpdate(cats);
closeModal(); closeModal();
// Não reabre o gerenciador imediatamente para dar tempo do toast sumir, setTimeout(() => abrirModalGerenciarCategorias(cats), 300);
// mas aqui vamos reabrir para manter o fluxo
setTimeout(() => abrirModalGerenciarCategorias(res.data), 300);
} }
} else { } else {
showToast(r.error, 'error'); showToast(r.error, 'error');