Compare commits

...

3 Commits

Author SHA1 Message Date
eb62b890cf db produção 2026-04-30 15:34:54 -03:00
Welton Silva
b710603876 feat: relatorio de pagamento impresso 2026-04-28 20:10:49 -03:00
Welton Silva
1c8568927c feat: add date filter and report printing to payments page 2026-04-28 19:36:37 -03:00
9 changed files with 456 additions and 319 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) {
container.innerHTML = `
<div class="page-header">
@@ -41,31 +43,29 @@ async function loadClientes() {
if (!wrap) return;
wrap.innerHTML = `<div class="loading-screen"><div class="spinner"></div></div>`;
const [res, pRes, cRes, ptRes, pagsRes] = await Promise.all([
window.electronAPI.get('/clients'),
window.electronAPI.get('/products'),
window.electronAPI.get('/comandas'),
window.electronAPI.get('/payment-types'),
window.electronAPI.get('/payments')
const [res, products, cRes, ptRes, pagsRes] = await Promise.all([
window.electronAPI.get('/clients?page=1&limit=200'),
fetchAllProducts(),
window.electronAPI.get('/comandas?page=1&limit=200'),
window.electronAPI.get('/payment-types?page=1&limit=200'),
window.electronAPI.get('/payments?page=1&limit=200')
]);
if (ptRes.ok) _paymentTypes = ptRes.data;
if (ptRes.ok) _paymentTypes = extractData(ptRes);
if (pRes.ok) {
_productsMap = pRes.data.reduce((acc, p) => {
_productsMap = (products || []).reduce((acc, p) => {
acc[String(p.id)] = {
name: p.name,
price: parseFloat(p.price || 0)
};
return acc;
}, {});
}
_comandasData = cRes.ok ? (cRes.data || []) : [];
_comandasData = cRes.ok ? extractData(cRes) : [];
_paymentsMap = {};
if (pagsRes.ok) {
(pagsRes.data || []).forEach(p => {
(extractData(pagsRes) || []).forEach(p => {
if (p.comanda) {
if (!_paymentsMap[p.comanda]) _paymentsMap[p.comanda] = [];
_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; }
_clientesData = res.data || [];
_clientesData = extractData(res);
_clientesData.forEach(c => {
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) {
const itens = comanda.items || [];
const totalPago = (pagamentosComanda || []).reduce((acc, p) => acc + parseFloat(p.value || 0), 0);
@@ -241,13 +243,18 @@ export async function renderComandas(container) {
</div>`;
let mesas = [];
const mesasRes = await window.electronAPI.get('/mesas');
if (mesasRes.ok) mesas = mesasRes.data;
const mesasRes = await window.electronAPI.get('/mesas?page=1&limit=200');
if (mesasRes.ok) mesas = extractData(mesasRes);
await loadComandas(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());
}
@@ -258,38 +265,48 @@ let _productsNames = {}; // Cache de nomes {id: name}
let _paymentTypes = [];
let _clients = [];
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;
_comandaPage = page;
const wrap = document.getElementById('comandas-table');
if (!wrap) return;
wrap.innerHTML = `<div class="loading-screen"><div class="spinner"></div></div>`;
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')
const q = document.getElementById('search-comanda')?.value.toLowerCase() || '';
const status = document.getElementById('filter-status')?.value || '';
const queryParams = { page, limit: COMANDA_PAGE_SIZE };
if (q) queryParams.name = q;
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 (cRes.ok) _clients = cRes.data;
if (ptRes.ok) _paymentTypes = extractData(ptRes);
if (cRes.ok) _clients = extractData(cRes);
if (pRes.ok) {
_productsMap = pRes.data.reduce((acc, p) => {
_productsMap = (products || []).reduce((acc, p) => {
acc[String(p.id)] = parseFloat(p.price || 0);
return acc;
}, {});
_productsNames = pRes.data.reduce((acc, p) => {
_productsNames = (products || []).reduce((acc, p) => {
acc[String(p.id)] = p.name || `Produto #${p.id}`;
return acc;
}, {});
}
_paymentsMap = {};
if (pagsRes.ok) {
(pagsRes.data || []).forEach(p => {
(extractData(pagsRes) || []).forEach(p => {
if (p.comanda) {
if (!_paymentsMap[p.comanda]) _paymentsMap[p.comanda] = [];
_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; }
_comandasData = res.ok ? res.data : [];
_comandasData.reverse();
_comandasData = extractData(res);
const meta = extractPagination(res);
_comandaTotal = meta.total;
_comandaTotalPages = meta.total_pages;
console.log('[PDV] Comandas carregadas do servidor:', _comandasData);
filtrarComandas();
renderComandasTable(_comandasData);
}
function renderComandasTable(data) {
const wrap = document.getElementById('comandas-table');
if (!wrap) return;
if (!data.length) { wrap.innerHTML = `<div class="table-empty">Nenhuma comanda encontrada.</div>`; return; }
// Limita a exibição às primeiras 100 comandas
const limitedData = data.slice(0, 100);
if (!data.length) {
wrap.innerHTML = `<div class="table-empty">Nenhuma comanda encontrada.</div>${renderPaginationControls()}`;
return;
}
wrap.innerHTML = `
<table>
@@ -365,10 +384,54 @@ function renderComandasTable(data) {
}).join('')}
</tbody>
</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 => {
row.addEventListener('click', () => {
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 => {
btn.addEventListener('click', (e) => {
e.stopPropagation();
@@ -385,7 +447,6 @@ function renderComandasTable(data) {
});
});
// Listener para Editar
wrap.querySelectorAll('.btn-editar').forEach(btn => {
btn.addEventListener('click', (e) => {
e.stopPropagation();
@@ -394,25 +455,32 @@ function renderComandasTable(data) {
});
});
// Listener para botão "Pagar" (muda p/ PAYING)
wrap.querySelectorAll('.btn-pagar').forEach(btn => {
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); }
if (r.ok) { showToast('Comanda em fase de pagamento!', 'info'); loadComandas(_mesasRef, _comandaPage); }
else showToast(r.error, 'error');
});
});
// Listener para botão "Reabrir" (muda p/ OPEN)
wrap.querySelectorAll('.btn-reopen').forEach(btn => {
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); }
if (r.ok) { showToast('Comanda reaberta!', 'info'); loadComandas(_mesasRef, _comandaPage); }
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) {
try {
@@ -525,7 +593,7 @@ async function abrirModalPagamento(comanda, onPaymentComplete) {
showToast(isVale ? 'Venda registrada como FIADO!' : 'Pagamento realizado!', 'success');
}
closeModal();
loadComandas(_mesasRef);
loadComandas(_mesasRef, _comandaPage);
if (onPaymentComplete) onPaymentComplete();
} catch (err) {
showToast(err.message, 'error');
@@ -542,18 +610,7 @@ async function abrirModalPagamento(comanda, onPaymentComplete) {
}
function filtrarComandas() {
const q = document.getElementById('search-comanda')?.value.toLowerCase() || '';
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);
loadComandas(_mesasRef, 1);
}
// ─── Modal de Itens (Novo Layout PDV Split) ───────────────────────────────────
@@ -569,11 +626,11 @@ async function abrirItensComanda(comandaIdOrObj) {
const podeAdd = comanda.status === 'OPEN';
// Carrega produtos (ativos) e usuário logado
const [pRes, loggedUser] = await Promise.all([
window.electronAPI.get('/products'),
const [allProducts, loggedUser] = await Promise.all([
fetchAllProducts(),
window.electronAPI.getUser()
]);
let todosProdutos = pRes.ok ? pRes.data.filter(p => p.active) : [];
let todosProdutos = (allProducts || []).filter(p => p.active);
openModal({
full: true,
@@ -604,9 +661,9 @@ async function abrirItensComanda(comandaIdOrObj) {
// Carrega pagamentos específicos desta comanda
let pagamentosComanda = _paymentsMap[comanda.id] || [];
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) {
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');
comanda.items = comanda.items.filter(it => it.id !== parseInt(itemId));
renderLeft();
loadComandas(_mesasRef);
loadComandas(_mesasRef, _comandaPage);
} else {
showToast(r.error, 'error');
}
@@ -764,9 +821,9 @@ async function abrirItensComanda(comandaIdOrObj) {
if (novaObs === null || novaObs === item.obs) return;
// 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) {
const linkedOrder = ordersRes.data.find(o => String(o.productComanda) === itemId);
const linkedOrder = extractData(ordersRes).find(o => String(o.productComanda) === itemId);
if (linkedOrder) {
// PATCH na Order (Cozinha)
@@ -815,7 +872,7 @@ async function abrirItensComanda(comandaIdOrObj) {
if (r.ok) {
showToast('Comanda excluída!', 'success');
closeModal();
loadComandas(_mesasRef);
loadComandas(_mesasRef, _comandaPage);
} else {
showToast(r.error, 'error');
}
@@ -833,7 +890,7 @@ async function abrirItensComanda(comandaIdOrObj) {
comanda.items.push(novoItem);
renderLeft();
loadComandas(_mesasRef);
loadComandas(_mesasRef, _comandaPage);
if (prod && prod.cuisine) {
const orderPayload = {
@@ -999,7 +1056,7 @@ function abrirModalNovaComanda(mesas, comandaExistente = null) {
if (r.ok) {
showToast(isEdit ? 'Comanda atualizada!' : 'Comanda criada!', 'success');
closeModal();
loadComandas(_mesasRef);
loadComandas(_mesasRef, 1);
if (!isEdit) {
setTimeout(() => abrirItensComanda(r.data), 300);
}

View File

@@ -1,3 +1,5 @@
import { extractData } from '../api-helpers.js';
export async function renderDashboard(container) {
container.innerHTML = `
<div class="page-header">
@@ -37,26 +39,29 @@ export async function renderDashboard(container) {
// Carrega dados em paralelo
const [mesas, comandas, pedidos, clientes] = await Promise.all([
window.electronAPI.get('/mesas/'),
window.electronAPI.get('/comandas/'),
window.electronAPI.get('/orders/'),
window.electronAPI.get('/clients/'),
window.electronAPI.get('/mesas?page=1&limit=200'),
window.electronAPI.get('/comandas?page=1&limit=200'),
window.electronAPI.get('/orders?page=1&limit=200'),
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) {
// Cruza com comandas para verificar ocupação
const mesasOcupadas = new Set();
if (comandas.ok) {
comandas.data.forEach(c => {
comandasData.forEach(c => {
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;
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);
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)'};
@@ -67,12 +72,12 @@ export async function renderDashboard(container) {
}
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');
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>
<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 badgeClass = c.status === 'OPEN' ? 'badge-success' : (c.status === 'PAYING' ? 'badge-warning' : 'badge-muted');
return `
@@ -88,11 +93,11 @@ export async function renderDashboard(container) {
if (pedidos.ok) {
const hoje = new Date().toISOString().slice(0, 10);
const pedidosHoje = pedidos.data.filter(p => (p.created_at || p.data || '').startsWith(hoje)).length;
document.getElementById('stat-pedidos').textContent = pedidosHoje || pedidos.data.length;
const pedidosHoje = pedidosData.filter(p => (p.created_at || p.data || '').startsWith(hoje)).length;
document.getElementById('stat-pedidos').textContent = pedidosHoje || pedidosData.length;
}
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) {
container.innerHTML = `
<div class="page-header">
@@ -28,8 +30,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?page=1&limit=200'),
window.electronAPI.get('/comandas?page=1&limit=200'),
]);
if (!mesasRes.ok) {
@@ -37,12 +39,12 @@ async function loadMesas() {
return;
}
const mesas = mesasRes.data;
const mesas = extractData(mesasRes);
// IDs de mesas com pelo menos uma comanda ativa (OPEN ou PAYING)
const mesasOcupadas = new Set();
if (comandasRes.ok) {
comandasRes.data.forEach(c => {
extractData(comandasRes).forEach(c => {
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) {
const [tRes, cRes, cliRes] = await Promise.all([
window.electronAPI.get('/payment-types'),
window.electronAPI.get('/comandas'),
window.electronAPI.get('/clients'),
window.electronAPI.get('/payment-types?page=1&limit=800'),
window.electronAPI.get('/comandas?page=1&limit=800'),
window.electronAPI.get('/clients?page=1&limit=200'),
]);
const tiposPag = tRes.ok ? tRes.data : [];
const comandas = cRes.ok ? cRes.data : [];
const clientes = cliRes.ok ? cliRes.data : [];
const tiposPag = tRes.ok ? extractData(tRes) : [];
const comandas = cRes.ok ? extractData(cRes) : [];
const clientes = cliRes.ok ? extractData(cliRes) : [];
container.innerHTML = `
<div class="page-header">
@@ -14,9 +16,6 @@ export async function renderPagamentos(container) {
<div class="page-title">💳 Pagamentos</div>
<div class="page-subtitle">Histórico financeiro do estabelecimento</div>
</div>
<div class="page-actions">
<button class="btn btn-primary btn-md" id="btn-novo-pag">+ Registrar Pagamento</button>
</div>
</div>
<div class="table-wrap">
<div class="table-toolbar">
@@ -26,14 +25,37 @@ export async function renderPagamentos(container) {
${tiposPag.map(t => `<option value="${t.id}">${t.nome || t.name}</option>`).join('')}
</select>
</div>
<div class="table-toolbar" style="border-top:1px solid var(--border);margin-top:0;padding-top:12px">
<div style="display:flex;gap:12px;align-items:center;flex-wrap:wrap">
<div style="display:flex;gap:8px;align-items:center">
<label style="font-size:0.8rem;color:var(--text-muted)">De:</label>
<input type="datetime-local" id="filter-dataini" class="form-control" style="width:180px" />
</div>
<div style="display:flex;gap:8px;align-items:center">
<label style="font-size:0.8rem;color:var(--text-muted)">Até:</label>
<input type="datetime-local" id="filter-datafim" class="form-control" style="width:180px" />
</div>
<button class="btn btn-secondary btn-sm" id="btn-limpar-filtro-data">🗑️ Limpar</button>
</div>
<div style="display:flex;gap:8px">
<button class="btn btn-warning btn-md" id="btn-imprimir-relatorio">🖨️ Imprimir Cupom</button>
</div>
</div>
<div id="pagamentos-table"></div>
</div>`;
await loadPagamentos(tiposPag, comandas, clientes);
document.getElementById('btn-novo-pag').addEventListener('click', () => abrirModalPagamento(tiposPag, comandas));
document.getElementById('search-pag').addEventListener('input', () => filtrarPagamentos());
document.getElementById('filter-tipo').addEventListener('change', () => filtrarPagamentos());
document.getElementById('filter-dataini').addEventListener('change', () => filtrarPagamentos());
document.getElementById('filter-datafim').addEventListener('change', () => filtrarPagamentos());
document.getElementById('btn-limpar-filtro-data').addEventListener('click', () => {
document.getElementById('filter-dataini').value = '';
document.getElementById('filter-datafim').value = '';
filtrarPagamentos();
});
document.getElementById('btn-imprimir-relatorio').addEventListener('click', () => imprimirRelatorio());
}
let _pagsData = [];
@@ -48,10 +70,10 @@ async function loadPagamentos(tiposPag, comandas, clientes) {
_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; }
_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) => {
acc[String(c.id)] = {
@@ -60,7 +82,8 @@ async function loadPagamentos(tiposPag, comandas, clientes) {
mesa_name: c.mesa_name || '',
status: c.status,
dt_open: c.dt_open,
client: c.client
client: c.client,
user: c.user
};
return acc;
}, {});
@@ -72,159 +95,47 @@ async function loadPagamentos(tiposPag, comandas, clientes) {
}, {});
}
renderPagsTable();
}
function renderPagsTable() {
const wrap = document.getElementById('pagamentos-table');
if (!wrap) return;
if (!_pagsData.length) {
wrap.innerHTML = `<div class="table-empty">Nenhum pagamento registrado.</div>`;
return;
}
const total = _pagsData.reduce((acc, p) => acc + parseFloat(p.value || 0), 0);
wrap.innerHTML = `
<table>
<thead><tr>
<th>#</th>
<th>Cliente</th>
<th>Comanda</th>
<th>Tipo</th>
<th>Valor</th>
<th>Descrição</th>
<th>Data</th>
<th>Ações</th>
</tr></thead>
<tbody>
${_pagsData.map(p => {
const cInfo = _cmdMap[String(p.comanda)];
const clienteNome = _clientesMap[String(p.client)] || p.client_name || '';
const cDesc = cInfo ? `${cInfo.name} (${cInfo.mesa})` : (p.comanda_name || '');
return `
<tr>
<td style="color:var(--text-muted)">#${p.id}</td>
<td>${clienteNome}</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-secondary btn-sm btn-view-pag" data-id="${p.id}">🔍 Ver</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">
<span style="font-size:0.82rem;color:var(--text-secondary)">Total exibido:</span>
<strong style="color:var(--success);font-size:1rem">R$ ${total.toFixed(2)}</strong>
</div>`;
wrap.querySelectorAll('.btn-view-pag').forEach(btn =>
btn.addEventListener('click', () => {
const pag = _pagsData.find(p => String(p.id) === String(btn.dataset.id));
if (pag) abrirDetalhesPagamento(pag);
})
);
}
function abrirDetalhesPagamento(pagamento) {
const cInfo = _cmdMap[String(pagamento.comanda)];
const tipoNome = _tiposPag.find(t => String(t.id) === String(pagamento.type_pay))?.name ||
_tiposPag.find(t => String(t.id) === String(pagamento.type_pay))?.nome ||
pagamento.type_pay_name || '';
const clienteNome = _clientesMap[String(pagamento.client)] || pagamento.client_name || '';
openModal({
title: `💳 Detalhes do Pagamento #${pagamento.id}`,
body: `
<div style="display:flex;flex-direction:column;gap:16px">
<div class="card" style="padding:16px;background:var(--bg-elevated)">
<div style="display:grid;grid-template-columns:1fr 1fr;gap:16px">
<div>
<div style="font-size:0.75rem;color:var(--text-muted);text-transform:uppercase;margin-bottom:4px">Valor</div>
<div style="font-size:1.5rem;font-weight:700;color:var(--success)">R$ ${parseFloat(pagamento.value || 0).toFixed(2)}</div>
</div>
<div>
<div style="font-size:0.75rem;color:var(--text-muted);text-transform:uppercase;margin-bottom:4px">Forma de Pagamento</div>
<div style="font-size:1rem;font-weight:600">${tipoNome}</div>
</div>
</div>
</div>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:12px">
<div>
<div style="font-size:0.75rem;color:var(--text-muted);text-transform:uppercase;margin-bottom:4px">Cliente</div>
<div style="font-weight:500">${clienteNome}</div>
</div>
<div>
<div style="font-size:0.75rem;color:var(--text-muted);text-transform:uppercase;margin-bottom:4px">Data/Hora</div>
<div style="font-weight:500">${formatDate(pagamento.datetime)}</div>
</div>
</div>
${pagamento.comanda ? `
<div>
<div style="font-size:0.75rem;color:var(--text-muted);text-transform:uppercase;margin-bottom:8px">Comanda</div>
<div class="card" style="padding:12px;background:var(--bg-elevated)">
<div style="display:flex;justify-content:space-between;margin-bottom:8px">
<div>
<div style="font-weight:600">#${pagamento.comanda}${cInfo?.name || ''}</div>
<div style="font-size:0.8rem;color:var(--text-secondary)">${cInfo?.mesa || ''}</div>
</div>
<span class="badge badge-${cInfo?.status === 'FIADO' ? 'warning' : cInfo?.status === 'CLOSED' ? 'success' : 'info'}">
${cInfo?.status || ''}
</span>
</div>
${cInfo?.dt_open ? `
<div style="font-size:0.8rem;color:var(--text-muted)">
Abertura: ${formatDate(cInfo.dt_open)}
</div>
` : ''}
</div>
</div>
` : ''}
${pagamento.description ? `
<div>
<div style="font-size:0.75rem;color:var(--text-muted);text-transform:uppercase;margin-bottom:4px">Descrição</div>
<div style="padding:10px;background:var(--bg-elevated);border-radius:var(--radius-sm)">${pagamento.description}</div>
</div>
` : ''}
</div>
`,
footer: `<button class="btn btn-secondary btn-md" onclick="closeModal()">Fechar</button>`
});
window._pagsFiltered = _pagsData;
renderPagsTableFiltered(_pagsData);
}
function filtrarPagamentos() {
const q = document.getElementById('search-pag')?.value.toLowerCase() || '';
const tipo = parseInt(document.getElementById('filter-tipo')?.value) || null;
const search = (document.getElementById('search-pag')?.value || '').toLowerCase();
const tipo = document.getElementById('filter-tipo')?.value || '';
const dataIni = document.getElementById('filter-dataini')?.value;
const dataFim = document.getElementById('filter-datafim')?.value;
const filtered = _pagsData.filter(p => {
const clienteNome = _clientesMap[String(p.client)] || p.client_name || '';
if (tipo && String(p.type_pay) !== tipo) return false;
if (dataIni) {
const pDate = new Date(p.datetime);
const iniDate = new Date(dataIni);
if (pDate < iniDate) return false;
}
if (dataFim) {
const pDate = new Date(p.datetime);
const fimDate = new Date(dataFim);
fimDate.setHours(23, 59, 59, 999);
if (pDate > fimDate) return false;
}
if (search) {
const cInfo = _cmdMap[String(p.comanda)];
const comandaNome = cInfo?.name || p.comanda_name || '';
const matchQ = !q ||
clienteNome.toLowerCase().includes(q) ||
comandaNome.toLowerCase().includes(q) ||
(p.description || '').toLowerCase().includes(q) ||
String(p.id).includes(q);
const matchTipo = !tipo || p.type_pay === tipo;
return matchQ && matchTipo;
const clienteNome = _clientesMap[String(p.client)] || p.client_name || '';
const cDesc = cInfo ? `${cInfo.name} ${cInfo.mesa}` : (p.comanda_name || '');
const searchStr = `${p.id} ${clienteNome} ${p.comanda} ${cDesc} ${p.description} ${p.value}`.toLowerCase();
if (!searchStr.includes(search)) return false;
}
return true;
});
window._pagsFiltered = filtered;
renderPagsTableFiltered(filtered);
}
function renderPagsTableFiltered(filtered) {
const wrap = document.getElementById('pagamentos-table');
if (!wrap) return;
@@ -288,6 +199,102 @@ function filtrarPagamentos() {
);
}
function imprimirRelatorio() {
const filtered = window._pagsFiltered || _pagsData;
if (!filtered.length) return showToast('Nenhum pagamento para imprimir.', 'warning');
const dataIni = document.getElementById('filter-dataini')?.value;
const dataFim = document.getElementById('filter-datafim')?.value;
const dataIniStr = dataIni ? new Date(dataIni).toLocaleString('pt-BR') : 'Início';
const dataFimStr = dataFim ? new Date(dataFim).toLocaleString('pt-BR') : 'Agora';
const porTipo = {};
filtered.forEach(p => {
const tipoNome = p.type_pay_name || _tiposPag.find(t => String(t.id) === String(p.type_pay))?.nome || _tiposPag.find(t => String(t.id) === String(p.type_pay))?.name || 'Outro';
if (!porTipo[tipoNome]) porTipo[tipoNome] = 0;
porTipo[tipoNome] += parseFloat(p.value || 0);
});
const totalGeral = filtered.reduce((acc, p) => acc + parseFloat(p.value || 0), 0);
const htmlRelatorio = `
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Relatório de Pagamentos</title>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body { font-family: 'Courier New', monospace; font-size: 12px; padding: 10px; width: 80mm; }
.rel { display: block; }
.rel * { color: black !important; background: transparent !important; }
.header { text-align: center; border-bottom: 2px solid #000; padding-bottom: 8px; margin-bottom: 12px; }
.title { font-size: 16px; font-weight: bold; text-transform: uppercase; }
.periodo { font-size: 11px; margin-top: 4px; }
.section { margin-top: 16px; }
.section-title { font-size: 13px; font-weight: bold; border-bottom: 1px solid #000; padding-bottom: 4px; margin-bottom: 8px; }
.row { display: flex; justify-content: space-between; padding: 3px 0; }
.total { font-weight: bold; margin-top: 8px; border-top: 2px solid #000; padding-top: 8px; }
.footer { text-align: center; margin-top: 16px; font-size: 10px; }
@media print {
@page { size: 80mm auto; margin: 0; }
}
</style>
</head>
<body>
<div class="rel">
<div class="header">
<div class="title">Raul Rock Bar & Café</div>
<div class="periodo">Relatório de Pagamentos</div>
<div class="periodo">${dataIniStr} - ${dataFimStr}</div>
</div>
<div class="section">
<div class="section-title">RESUMO POR TIPO</div>
${Object.entries(porTipo).map(([tipo, valor]) => `
<div class="row">
<span>${tipo}</span>
<span>R$ ${valor.toFixed(2)}</span>
</div>
`).join('')}
</div>
<div class="total">
<div class="row">
<span>TOTAL GERAL:</span>
<span>R$ ${totalGeral.toFixed(2)}</span>
</div>
</div>
<div class="footer">
<div>------------------------</div>
<div>${filtered.length} pagamento(s)</div>
<div>Gerado em: ${new Date().toLocaleString('pt-BR')}</div>
</div>
</div>
</body>
</html>
`;
window.electronAPI.printDirect(htmlRelatorio).then(r => {
if (r.ok) {
showToast('Relatório enviado para impressão!', 'success');
} else if (r.error === 'NO_PRINTER') {
showToast('Nenhuma impressora configurada.', 'warning', 5000);
const printWindow = window.open('', '', 'width=300,height=400');
printWindow.document.write(htmlRelatorio);
printWindow.document.close();
setTimeout(() => printWindow.print(), 300);
} else {
const printWindow = window.open('', '', 'width=300,height=400');
printWindow.document.write(htmlRelatorio);
printWindow.document.close();
setTimeout(() => printWindow.print(), 300);
}
});
}
function abrirModalPagamento(tiposPag, comandas) {
const comandasAbertas = comandas.filter(c => c.status === 'OPEN' || c.status === 'PAYING');

View File

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

View File

@@ -1,6 +1,8 @@
// Pedidos = Fila de Cozinha (KDS - Kitchen Display System)
// Cada "order" representa um item individual na fila, com pipeline de status por timestamps
import { extractData, fetchAllProducts } from '../api-helpers.js';
const STATUS_CONFIG = {
'Na fila': { badge: 'badge-warning', icon: '⏳', next: 'preparing', nextLabel: '▶ Preparando' },
'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>`;
// 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')
const [res, products, cRes, mRes] = await Promise.all([
window.electronAPI.get('/orders?page=1&limit=200'),
fetchAllProducts(),
window.electronAPI.get('/comandas?page=1&limit=200'),
window.electronAPI.get('/mesas?page=1&limit=200')
]);
if (!res.ok) { wrap.innerHTML = `<div class="table-empty">Erro ao carregar pedidos.</div>`; return; }
if (pRes.ok) {
_productsMap = pRes.data.reduce((acc, p) => { acc[String(p.id)] = p.name; return acc; }, {});
_productsFullMap = pRes.data.reduce((acc, p) => { acc[String(p.id)] = p; return acc; }, {});
_productsMap = (products || []).reduce((acc, p) => { acc[String(p.id)] = p.name; return acc; }, {});
_productsFullMap = (products || []).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) _comandasMap = cRes.data.reduce((acc, c) => {
if (cRes.ok) {
const comandas = extractData(cRes);
_comandasMap = comandas.reduce((acc, c) => {
acc[String(c.id)] = {
id: c.id,
name: c.name || '',
@@ -85,8 +90,9 @@ async function loadOrders() {
};
return acc;
}, {});
}
_ordersData = res.data;
_ordersData = extractData(res);
// Por padrão, filtra para mostrar só os não entregues/cancelados
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) {
let categorias = [];
let unidades = [];
const [catRes, unRes] = await Promise.all([
window.electronAPI.get('/categories'),
window.electronAPI.get('/unit-of-measurements')
window.electronAPI.get('/categories?page=1&limit=200'),
window.electronAPI.get('/unit-of-measurements?page=1&limit=200')
]);
if (catRes.ok) categorias = catRes.data;
if (unRes.ok) unidades = unRes.data;
if (catRes.ok) categorias = extractData(catRes);
if (unRes.ok) unidades = extractData(unRes);
container.innerHTML = `
<div class="page-header">
@@ -50,9 +52,8 @@ 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');
if (!res.ok) { wrap.innerHTML = `<div class="table-empty">Erro ao carregar produtos.</div>`; return; }
_produtosData = res.data;
const products = await fetchAllProducts();
_produtosData = products || [];
renderProdutosTable(_produtosData, categorias, unidades);
}
@@ -364,11 +365,9 @@ function abrirFormularioCategoria(cat, onSuccess, onListUpdate) {
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);
window.electronAPI.get('/categories?page=1&limit=200').then(res => {
if (res.ok) onListUpdate(extractData(res));
});
}, 300);
};
@@ -386,13 +385,12 @@ function abrirFormularioCategoria(cat, onSuccess, onListUpdate) {
if (r.ok) {
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) {
onListUpdate(res.data);
const cats = extractData(res);
onListUpdate(cats);
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);
setTimeout(() => abrirModalGerenciarCategorias(cats), 300);
}
} else {
showToast(r.error, 'error');