diff --git a/src/renderer/api-helpers.js b/src/renderer/api-helpers.js
new file mode 100644
index 0000000..2921f31
--- /dev/null
+++ b/src/renderer/api-helpers.js
@@ -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;
+}
diff --git a/src/renderer/pages/clientes.js b/src/renderer/pages/clientes.js
index 992640c..175bea5 100644
--- a/src/renderer/pages/clientes.js
+++ b/src/renderer/pages/clientes.js
@@ -1,3 +1,5 @@
+import { extractData, fetchAllProducts } from '../api-helpers.js';
+
export async function renderClientes(container) {
container.innerHTML = `
`;
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 = ``;
- 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 = `Erro ao carregar comandas.
`; 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 = `Nenhuma comanda encontrada.
`; return; }
-
- // Limita a exibição às primeiras 100 comandas
- const limitedData = data.slice(0, 100);
+ if (!data.length) {
+ wrap.innerHTML = `Nenhuma comanda encontrada.
${renderPaginationControls()}`;
+ return;
+ }
wrap.innerHTML = `
@@ -365,10 +384,54 @@ function renderComandasTable(data) {
}).join('')}
- ${data.length > 100 ? `Exibindo apenas as últimas 100 de ${data.length} comandas.
` : ''}
+ ${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 += ``;
+ if (startPage > 2) pagesHtml += `...`;
+ }
+
+ for (let i = startPage; i <= endPage; i++) {
+ const active = i === _comandaPage ? 'btn-primary btn-sm' : 'btn-ghost btn-sm';
+ pagesHtml += ``;
+ }
+
+ if (endPage < _comandaTotalPages) {
+ if (endPage < _comandaTotalPages - 1) pagesHtml += `...`;
+ pagesHtml += ``;
+ }
+
+ return `
+ `;
+}
+
+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);
}
diff --git a/src/renderer/pages/dashboard.js b/src/renderer/pages/dashboard.js
index 3c39bee..9872ad2 100644
--- a/src/renderer/pages/dashboard.js
+++ b/src/renderer/pages/dashboard.js
@@ -1,3 +1,5 @@
+import { extractData } from '../api-helpers.js';
+
export async function renderDashboard(container) {
container.innerHTML = `