Compare commits
3 Commits
b022d09a71
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| eb62b890cf | |||
|
|
b710603876 | ||
|
|
1c8568927c |
62
src/renderer/api-helpers.js
Normal file
62
src/renderer/api-helpers.js
Normal 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;
|
||||
}
|
||||
@@ -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) => {
|
||||
acc[String(p.id)] = {
|
||||
name: p.name,
|
||||
price: parseFloat(p.price || 0)
|
||||
};
|
||||
return acc;
|
||||
}, {});
|
||||
}
|
||||
_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 => {
|
||||
|
||||
@@ -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) => {
|
||||
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;
|
||||
}, {});
|
||||
}
|
||||
_productsMap = (products || []).reduce((acc, p) => {
|
||||
acc[String(p.id)] = parseFloat(p.price || 0);
|
||||
return acc;
|
||||
}, {});
|
||||
_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);
|
||||
}
|
||||
|
||||
@@ -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 => {
|
||||
if ((c.status === 'OPEN' || c.status === 'PAYING') && c.mesa) mesasOcupadas.add(c.mesa);
|
||||
});
|
||||
}
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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 || '';
|
||||
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;
|
||||
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 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');
|
||||
|
||||
|
||||
@@ -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) => {
|
||||
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;
|
||||
}, {});
|
||||
}
|
||||
_productsMap = (products || []).reduce((acc, p) => {
|
||||
acc[String(p.id)] = parseFloat(p.price || 0);
|
||||
return acc;
|
||||
}, {});
|
||||
_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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,31 +64,35 @@ 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 (cRes.ok) {
|
||||
const comandas = extractData(cRes);
|
||||
_comandasMap = comandas.reduce((acc, c) => {
|
||||
acc[String(c.id)] = {
|
||||
id: c.id,
|
||||
name: c.name || '–',
|
||||
mesa: _mesasMap[String(c.mesa)] || `Mesa ${c.mesa}` || '–',
|
||||
mesa_name: c.mesa_name || _mesasMap[String(c.mesa)] || '–'
|
||||
};
|
||||
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)] = {
|
||||
id: c.id,
|
||||
name: c.name || '–',
|
||||
mesa: _mesasMap[String(c.mesa)] || `Mesa ${c.mesa}` || '–',
|
||||
mesa_name: c.mesa_name || _mesasMap[String(c.mesa)] || '–'
|
||||
};
|
||||
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');
|
||||
|
||||
@@ -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');
|
||||
|
||||
Reference in New Issue
Block a user