feat: Add kitchen ticket printing, client debit calculations, and payment details modal
- Add automatic ticket printing for kitchen items after observation modal - Add reprint button for each kitchen item in comanda list - Add reprint button in orders screen - Include comanda name, table, user, observations in kitchen tickets - Calculate remaining debt considering partial payments in clients screen - Show payment history in fiados modal with remaining value - Replace delete button with details modal in payments screen - Fetch client name by ID in payments table - Remove delete option from payment details modal - Add printer error handling with fallback to system dialog - Add product image upload with base64 encoding
This commit is contained in:
@@ -172,7 +172,12 @@ ipcMain.handle('print:direct', async (_, html) => {
|
|||||||
if (success) {
|
if (success) {
|
||||||
resolve({ ok: true });
|
resolve({ ok: true });
|
||||||
} else {
|
} else {
|
||||||
resolve({ ok: false, error: 'Nenhuma impressora configurada ou disponível.' });
|
const errorMsg = errorType || '';
|
||||||
|
if (errorMsg.includes('No printer') || errorMsg.includes('failed to enumerate') || errorMsg.includes('Error getting default')) {
|
||||||
|
resolve({ ok: false, error: 'NO_PRINTER', message: 'Nenhuma impressora configurada. Configure uma impressora nas configurações do sistema.' });
|
||||||
|
} else {
|
||||||
|
resolve({ ok: false, error: errorMsg });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -34,18 +34,19 @@ let _clientesData = [];
|
|||||||
let _comandasData = [];
|
let _comandasData = [];
|
||||||
let _productsMap = {};
|
let _productsMap = {};
|
||||||
let _paymentTypes = [];
|
let _paymentTypes = [];
|
||||||
|
let _paymentsMap = {};
|
||||||
|
|
||||||
async function loadClientes() {
|
async function loadClientes() {
|
||||||
const wrap = document.getElementById('clientes-table');
|
const wrap = document.getElementById('clientes-table');
|
||||||
if (!wrap) return;
|
if (!wrap) return;
|
||||||
wrap.innerHTML = `<div class="loading-screen"><div class="spinner"></div></div>`;
|
wrap.innerHTML = `<div class="loading-screen"><div class="spinner"></div></div>`;
|
||||||
|
|
||||||
// Carrega clientes, produtos, comandas e tipos de pagamento em paralelo
|
const [res, pRes, cRes, ptRes, pagsRes] = await Promise.all([
|
||||||
const [res, pRes, cRes, ptRes] = await Promise.all([
|
|
||||||
window.electronAPI.get('/clients'),
|
window.electronAPI.get('/clients'),
|
||||||
window.electronAPI.get('/products'),
|
window.electronAPI.get('/products'),
|
||||||
window.electronAPI.get('/comandas'),
|
window.electronAPI.get('/comandas'),
|
||||||
window.electronAPI.get('/payment-types')
|
window.electronAPI.get('/payment-types'),
|
||||||
|
window.electronAPI.get('/payments')
|
||||||
]);
|
]);
|
||||||
|
|
||||||
if (ptRes.ok) _paymentTypes = ptRes.data;
|
if (ptRes.ok) _paymentTypes = ptRes.data;
|
||||||
@@ -60,17 +61,24 @@ async function loadClientes() {
|
|||||||
}, {});
|
}, {});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (cRes.ok) _comandasData = cRes.data;
|
_comandasData = cRes.ok ? (cRes.data || []) : [];
|
||||||
|
|
||||||
|
_paymentsMap = {};
|
||||||
|
if (pagsRes.ok) {
|
||||||
|
(pagsRes.data || []).forEach(p => {
|
||||||
|
if (p.comanda) {
|
||||||
|
if (!_paymentsMap[p.comanda]) _paymentsMap[p.comanda] = [];
|
||||||
|
_paymentsMap[p.comanda].push(p);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (!res.ok) { wrap.innerHTML = `<div class="table-empty">Erro ao carregar clientes.</div>`; return; }
|
if (!res.ok) { wrap.innerHTML = `<div class="table-empty">Erro ao carregar clientes.</div>`; return; }
|
||||||
|
|
||||||
_clientesData = res.data || [];
|
_clientesData = res.data || [];
|
||||||
_comandasData = cRes.ok ? (cRes.data || []) : [];
|
|
||||||
|
|
||||||
// Calcula o débito real de cada cliente somando suas comandas FIADO
|
|
||||||
_clientesData.forEach(c => {
|
_clientesData.forEach(c => {
|
||||||
const fiados = _comandasData.filter(com => {
|
const fiados = _comandasData.filter(com => {
|
||||||
// Baseado no model Go: json:"client"
|
|
||||||
const cid = com.client;
|
const cid = com.client;
|
||||||
return String(cid) === String(c.id) && String(com.status).toUpperCase() === 'FIADO';
|
return String(cid) === String(c.id) && String(com.status).toUpperCase() === 'FIADO';
|
||||||
});
|
});
|
||||||
@@ -81,7 +89,10 @@ async function loadClientes() {
|
|||||||
const preco = pInfo ? pInfo.price : parseFloat(item.product_price || 0);
|
const preco = pInfo ? pInfo.price : parseFloat(item.product_price || 0);
|
||||||
return sum + preco;
|
return sum + preco;
|
||||||
}, 0);
|
}, 0);
|
||||||
return acc + totalComanda;
|
const pagamentos = _paymentsMap[com.id] || [];
|
||||||
|
const totalPago = pagamentos.reduce((sum, p) => sum + parseFloat(p.value || 0), 0);
|
||||||
|
const restante = Math.max(0, totalComanda - totalPago);
|
||||||
|
return acc + restante;
|
||||||
}, 0);
|
}, 0);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -293,21 +304,27 @@ async function abrirHistoricoFiados(cliente) {
|
|||||||
const preco = pInfo ? pInfo.price : parseFloat(it.product_price || 0);
|
const preco = pInfo ? pInfo.price : parseFloat(it.product_price || 0);
|
||||||
return acc + preco;
|
return acc + preco;
|
||||||
}, 0);
|
}, 0);
|
||||||
|
const pagamentos = _paymentsMap[f.id] || [];
|
||||||
|
const totalPago = pagamentos.reduce((acc, p) => acc + parseFloat(p.value || 0), 0);
|
||||||
|
const valorRestante = Math.max(0, totalComanda - totalPago);
|
||||||
|
const temPagamentos = pagamentos.length > 0;
|
||||||
|
|
||||||
return `
|
return `
|
||||||
<div class="card card-fiado" style="margin-bottom:15px; border-left: 4px solid var(--warning); position:relative; padding-left:50px">
|
<div class="card card-fiado" style="margin-bottom:15px; border-left: 4px solid ${valorRestante > 0 ? 'var(--warning)' : 'var(--success)'}; position:relative; padding-left:50px">
|
||||||
<div style="position:absolute; left:15px; top:50%; transform:translateY(-50%)">
|
<div style="position:absolute; left:15px; top:50%; transform:translateY(-50%)">
|
||||||
<input type="checkbox" class="fiado-check" data-id="${f.id}" data-total="${totalComanda}" style="width:20px; height:20px; cursor:pointer" />
|
<input type="checkbox" class="fiado-check" data-id="${f.id}" data-total="${valorRestante}" style="width:20px; height:20px; cursor:pointer" ${valorRestante <= 0 ? 'disabled' : ''} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style="display:flex; justify-content:space-between; align-items:flex-start; margin-bottom:10px;">
|
<div style="display:flex; justify-content:space-between; align-items:flex-start; margin-bottom:10px;">
|
||||||
<div>
|
<div>
|
||||||
<div style="font-weight:600; font-size:1.1rem;">Comanda #${f.id} — ${f.name || 'Sem nome'}</div>
|
<div style="font-weight:600; font-size:1.1rem;">Comanda #${f.id} — ${f.name || 'Sem nome'}</div>
|
||||||
<div style="font-size:0.8rem; color:var(--text-muted)">Abertura: ${formatDate(f.dt_open)}</div>
|
<div style="font-size:0.8rem; color:var(--text-muted)">Abertura: ${formatDate(f.dt_open)}</div>
|
||||||
|
${temPagamentos ? `<div style="font-size:0.75rem; color:var(--success); margin-top:2px">Pago: R$ ${totalPago.toFixed(2)}</div>` : ''}
|
||||||
</div>
|
</div>
|
||||||
<div style="text-align:right">
|
<div style="text-align:right">
|
||||||
|
${temPagamentos ? `<div style="font-size:0.85rem; text-decoration:line-through; color:var(--text-muted)">R$ ${totalComanda.toFixed(2)}</div>` : ''}
|
||||||
<div class="badge badge-warning" style="margin-bottom:5px">${f.status}</div>
|
<div class="badge badge-warning" style="margin-bottom:5px">${f.status}</div>
|
||||||
<div style="font-weight:700; color:var(--danger); font-size:1.1rem">R$ ${totalComanda.toFixed(2)}</div>
|
<div style="font-weight:700; color:${valorRestante > 0 ? 'var(--warning)' : 'var(--success)'}; font-size:1.1rem">R$ ${valorRestante.toFixed(2)}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -333,6 +350,24 @@ async function abrirHistoricoFiados(cliente) {
|
|||||||
`}).join('')}
|
`}).join('')}
|
||||||
</ul>
|
</ul>
|
||||||
</details>
|
</details>
|
||||||
|
${temPagamentos ? `
|
||||||
|
<details style="margin-top:10px">
|
||||||
|
<summary style="font-weight:600; font-size:0.8rem; text-transform:uppercase; color:var(--success); cursor:pointer; outline:none">
|
||||||
|
Ver Pagamentos (${pagamentos.length})
|
||||||
|
</summary>
|
||||||
|
<ul style="list-style:none; padding:10px 0 0 0; margin:0; font-size:0.85rem;">
|
||||||
|
${pagamentos.map(p => {
|
||||||
|
const tipoNome = _paymentTypes.find(t => String(t.id) === String(p.type_pay))?.name ||
|
||||||
|
_paymentTypes.find(t => String(t.id) === String(p.type_pay))?.nome || '–';
|
||||||
|
return `
|
||||||
|
<li style="display:flex; justify-content:space-between; padding:4px 0; border-bottom:1px dashed var(--border); color:var(--success)">
|
||||||
|
<span>💰 ${tipoNome}</span>
|
||||||
|
<span>R$ ${parseFloat(p.value || 0).toFixed(2)}</span>
|
||||||
|
</li>
|
||||||
|
`}).join('')}
|
||||||
|
</ul>
|
||||||
|
</details>
|
||||||
|
` : ''}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
@@ -342,13 +377,17 @@ async function abrirHistoricoFiados(cliente) {
|
|||||||
const updateSum = () => {
|
const updateSum = () => {
|
||||||
const checks = listContainer.querySelectorAll('.fiado-check:checked');
|
const checks = listContainer.querySelectorAll('.fiado-check:checked');
|
||||||
let sum = 0;
|
let sum = 0;
|
||||||
checks.forEach(c => sum += parseFloat(c.dataset.total));
|
let count = 0;
|
||||||
|
checks.forEach(c => {
|
||||||
|
sum += parseFloat(c.dataset.total);
|
||||||
|
count++;
|
||||||
|
});
|
||||||
|
|
||||||
document.getElementById('selected-count').textContent = checks.length;
|
document.getElementById('selected-count').textContent = count;
|
||||||
document.getElementById('selected-total').textContent = sum.toFixed(2);
|
document.getElementById('selected-total').textContent = sum.toFixed(2);
|
||||||
|
|
||||||
const btnPagar = document.getElementById('btn-pagar-selecionados');
|
const btnPagar = document.getElementById('btn-pagar-selecionados');
|
||||||
btnPagar.disabled = checks.length === 0;
|
btnPagar.disabled = count === 0 || sum <= 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
listContainer.querySelectorAll('.fiado-check').forEach(chk => {
|
listContainer.querySelectorAll('.fiado-check').forEach(chk => {
|
||||||
|
|||||||
@@ -108,16 +108,113 @@ function imprimirComanda(comanda, pagamentosComanda, totalComanda) {
|
|||||||
window.electronAPI.printDirect(htmlCompleto).then(r => {
|
window.electronAPI.printDirect(htmlCompleto).then(r => {
|
||||||
if (r.ok) {
|
if (r.ok) {
|
||||||
showToast('Impressão enviada!', 'success');
|
showToast('Impressão enviada!', 'success');
|
||||||
} else {
|
} else if (r.error === 'NO_PRINTER') {
|
||||||
showToast('Nenhuma impressora configurada. Abrindo diálogo...', 'warning');
|
showToast('⚠️ Nenhuma impressora configurada. Configure uma impressora nas configurações do sistema.', 'warning', 5000);
|
||||||
const printWindow = window.open('', '', 'width=300,height=600');
|
const printWindow = window.open('', '', 'width=300,height=600');
|
||||||
printWindow.document.write(htmlCompleto);
|
printWindow.document.write(htmlCompleto);
|
||||||
printWindow.document.close();
|
printWindow.document.close();
|
||||||
printWindow.onload = () => printWindow.print();
|
setTimeout(() => printWindow.print(), 300);
|
||||||
|
} else {
|
||||||
|
const printWindow = window.open('', '', 'width=300,height=600');
|
||||||
|
printWindow.document.write(htmlCompleto);
|
||||||
|
printWindow.document.close();
|
||||||
|
setTimeout(() => printWindow.print(), 300);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function imprimirTicketCozinha(comanda, item, product, obs = '', loggedUser = null) {
|
||||||
|
const dataAtual = new Date().toLocaleString('pt-BR');
|
||||||
|
const nomeEstabelecimento = 'RRBEC - Bar & Restaurante';
|
||||||
|
const nomeProduto = product?.name || item.product_name || `Produto #${item.product}`;
|
||||||
|
const observacao = obs || item.obs || '';
|
||||||
|
const usuario = loggedUser?.username || item.applicant || 'Sistema';
|
||||||
|
const nomeComanda = comanda.name || `Comanda #${comanda.id}`;
|
||||||
|
const nomeMesa = comanda.mesa_name || comanda.mesa || '–';
|
||||||
|
|
||||||
|
const htmlTicket = `
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>Ticket Cozinha - ${nomeComanda}</title>
|
||||||
|
<style>
|
||||||
|
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||||
|
body { font-family: 'Courier New', monospace; font-size: 13px; padding: 8px; width: 80mm; }
|
||||||
|
.ticket { display: block; color: black; }
|
||||||
|
.ticket * { color: black !important; background: transparent !important; }
|
||||||
|
.header { text-align: center; border-bottom: 2px solid #000; padding-bottom: 8px; margin-bottom: 8px; }
|
||||||
|
.title { font-size: 16px; font-weight: bold; text-transform: uppercase; }
|
||||||
|
.subtitle { font-size: 12px; margin-top: 4px; }
|
||||||
|
.info { margin: 4px 0; font-size: 12px; }
|
||||||
|
.info strong { font-size: 14px; }
|
||||||
|
.product { font-size: 18px; font-weight: bold; text-align: center; margin: 12px 0; padding: 8px; border: 3px double #000; text-transform: uppercase; }
|
||||||
|
.obs { font-style: italic; font-size: 11px; text-align: center; margin-top: 8px; padding: 6px; border: 1px dashed #000; }
|
||||||
|
.footer { text-align: center; font-size: 10px; margin-top: 8px; color: #666; }
|
||||||
|
@media print {
|
||||||
|
@page { size: 80mm auto; margin: 0; }
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="ticket">
|
||||||
|
<div class="header">
|
||||||
|
<div class="title">🍳 COZINHA</div>
|
||||||
|
<div class="subtitle">${nomeEstabelecimento}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="info" style="text-align:center">
|
||||||
|
<strong>${nomeComanda}</strong>
|
||||||
|
</div>
|
||||||
|
<div class="info" style="display:flex;justify-content:space-between">
|
||||||
|
<span>Mesa: ${nomeMesa}</span>
|
||||||
|
<span>${dataAtual}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="product">
|
||||||
|
${nomeProduto}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
${observacao ? `<div class="obs">OBS: ${observacao}</div>` : ''}
|
||||||
|
|
||||||
|
<div class="footer">
|
||||||
|
Atendido por: ${usuario}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
`;
|
||||||
|
|
||||||
|
window.electronAPI.printDirect(htmlTicket).then(r => {
|
||||||
|
if (r.ok) {
|
||||||
|
// OK
|
||||||
|
} else if (r.error === 'NO_PRINTER') {
|
||||||
|
showToast('⚠️ Nenhuma impressora configurada. Abra as configurações do sistema para adicionar uma.', 'warning', 5000);
|
||||||
|
const printWindow = window.open('', '', 'width=300,height=400');
|
||||||
|
printWindow.document.write(htmlTicket);
|
||||||
|
printWindow.document.close();
|
||||||
|
setTimeout(() => printWindow.print(), 300);
|
||||||
|
} else {
|
||||||
|
const printWindow = window.open('', '', 'width=300,height=400');
|
||||||
|
printWindow.document.write(htmlTicket);
|
||||||
|
printWindow.document.close();
|
||||||
|
setTimeout(() => printWindow.print(), 300);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function getImageUrl(imagePath) {
|
||||||
|
if (!imagePath) return null;
|
||||||
|
if (imagePath.startsWith('data:')) return imagePath;
|
||||||
|
if (imagePath.startsWith('http://') || imagePath.startsWith('https://')) return imagePath;
|
||||||
|
return `http://localhost:8080${imagePath}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getProductImage(product) {
|
||||||
|
const img = getImageUrl(product.image);
|
||||||
|
return img || 'https://wallpapers.com/images/featured/fundo-abstrato-escuro-27kvn4ewpldsngbu.jpg';
|
||||||
|
}
|
||||||
|
|
||||||
export async function renderComandas(container) {
|
export async function renderComandas(container) {
|
||||||
container.innerHTML = `
|
container.innerHTML = `
|
||||||
<div class="page-header">
|
<div class="page-header">
|
||||||
@@ -471,9 +568,11 @@ async function abrirItensComanda(comandaIdOrObj) {
|
|||||||
const ativa = comanda.status === 'OPEN' || comanda.status === 'PAYING';
|
const ativa = comanda.status === 'OPEN' || comanda.status === 'PAYING';
|
||||||
const podeAdd = comanda.status === 'OPEN';
|
const podeAdd = comanda.status === 'OPEN';
|
||||||
|
|
||||||
|
// Carrega produtos (ativos) e usuário logado
|
||||||
// Carrega produtos (ativos)
|
const [pRes, loggedUser] = await Promise.all([
|
||||||
const pRes = await window.electronAPI.get('/products');
|
window.electronAPI.get('/products'),
|
||||||
|
window.electronAPI.getUser()
|
||||||
|
]);
|
||||||
let todosProdutos = pRes.ok ? pRes.data.filter(p => p.active) : [];
|
let todosProdutos = pRes.ok ? pRes.data.filter(p => p.active) : [];
|
||||||
|
|
||||||
openModal({
|
openModal({
|
||||||
@@ -497,7 +596,7 @@ async function abrirItensComanda(comandaIdOrObj) {
|
|||||||
</div>
|
</div>
|
||||||
</div>`,
|
</div>`,
|
||||||
footer: `
|
footer: `
|
||||||
<button class="btn btn-secondary btn-md" id="btn-pdv-imprimir">🖨️ Imprimir</button>
|
<button class="btn btn-secondary btn-md" id="btn-pdv-imprimir">🖨️ Imprimir Comanda</button>
|
||||||
<button class="btn btn-secondary btn-md" onclick="closeModal()">Sair do PDV</button>
|
<button class="btn btn-secondary btn-md" onclick="closeModal()">Sair do PDV</button>
|
||||||
`,
|
`,
|
||||||
});
|
});
|
||||||
@@ -531,7 +630,7 @@ async function abrirItensComanda(comandaIdOrObj) {
|
|||||||
<thead><tr style="color:var(--text-muted);border-bottom:1px solid var(--border)">
|
<thead><tr style="color:var(--text-muted);border-bottom:1px solid var(--border)">
|
||||||
<th style="text-align:left;padding:8px 0">Produto</th>
|
<th style="text-align:left;padding:8px 0">Produto</th>
|
||||||
<th style="text-align:right;padding:8px 0">Preço</th>
|
<th style="text-align:right;padding:8px 0">Preço</th>
|
||||||
${podeAdd ? '<th style="text-align:center;padding:8px 0">Ação</th>' : ''}
|
<th style="text-align:center;padding:8px 0">Ações</th>
|
||||||
</tr></thead>
|
</tr></thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
${itens.map(it => {
|
${itens.map(it => {
|
||||||
@@ -542,24 +641,29 @@ async function abrirItensComanda(comandaIdOrObj) {
|
|||||||
return `
|
return `
|
||||||
<tr data-item-id="${it.id}">
|
<tr data-item-id="${it.id}">
|
||||||
<td style="padding:10px 0;border-bottom:1px solid var(--border)" ${tooltip}>
|
<td style="padding:10px 0;border-bottom:1px solid var(--border)" ${tooltip}>
|
||||||
${prod?.name || it.product_name || `Produto #${it.product}`}
|
${isCuisine ? '🍳 ' : ''}${prod?.name || it.product_name || `Produto #${it.product}`}
|
||||||
|
${it.obs ? `<br><small style="color:var(--text-muted);font-size:0.75rem">OBS: ${it.obs}</small>` : ''}
|
||||||
</td>
|
</td>
|
||||||
<td style="padding:10px 0;text-align:right;border-bottom:1px solid var(--border)">
|
<td style="padding:10px 0;text-align:right;border-bottom:1px solid var(--border)">
|
||||||
R$ ${(_productsMap[String(it.product)] || 0).toFixed(2)}
|
R$ ${(_productsMap[String(it.product)] || 0).toFixed(2)}
|
||||||
</td>
|
</td>
|
||||||
${podeAdd ? `
|
|
||||||
<td style="padding:10px 0;text-align:center;border-bottom:1px solid var(--border)">
|
<td style="padding:10px 0;text-align:center;border-bottom:1px solid var(--border)">
|
||||||
<div style="display:flex; gap:8px; justify-content:center">
|
<div style="display:flex; gap:8px; justify-content:center">
|
||||||
${isCuisine ? `
|
${isCuisine ? `
|
||||||
<button class="btn btn-ghost btn-sm btn-edit-obs" data-id="${it.id}" title="Editar observação" style="color: var(--primary); font-size: 1rem;">
|
<button class="btn btn-ghost btn-sm btn-reprint-cozinha" data-id="${it.id}" title="Reimprimir Ticket" style="color: var(--warning); font-size: 0.9rem;">
|
||||||
|
🖨️
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-ghost btn-sm btn-edit-obs" data-id="${it.id}" title="Editar observação" style="color: var(--primary); font-size: 0.9rem;">
|
||||||
📝
|
📝
|
||||||
</button>
|
</button>
|
||||||
` : ''}
|
` : ''}
|
||||||
<button class="btn btn-ghost btn-sm btn-del-item" data-id="${it.id}" title="Excluir item" style="color: var(--danger); font-size: 1rem;">
|
${podeAdd ? `
|
||||||
🗑️
|
<button class="btn btn-ghost btn-sm btn-del-item" data-id="${it.id}" title="Excluir item" style="color: var(--danger); font-size: 0.9rem;">
|
||||||
</button>
|
🗑️
|
||||||
|
</button>
|
||||||
|
` : ''}
|
||||||
</div>
|
</div>
|
||||||
</td>` : ''}
|
</td>
|
||||||
</tr>`;
|
</tr>`;
|
||||||
}).join('')}
|
}).join('')}
|
||||||
</tbody>
|
</tbody>
|
||||||
@@ -635,6 +739,19 @@ async function abrirItensComanda(comandaIdOrObj) {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Listeners de reimpressão de ticket de cozinha
|
||||||
|
container.querySelectorAll('.btn-reprint-cozinha').forEach(btn => {
|
||||||
|
btn.addEventListener('click', () => {
|
||||||
|
const itemId = String(btn.dataset.id);
|
||||||
|
const item = comanda.items.find(it => String(it.id) === itemId);
|
||||||
|
const prod = todosProdutos.find(p => String(p.id) === String(item?.product));
|
||||||
|
|
||||||
|
if (item && prod) {
|
||||||
|
imprimirTicketCozinha(comanda, item, prod, item.obs, loggedUser);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
// Listeners de edição de observação
|
// Listeners de edição de observação
|
||||||
container.querySelectorAll('.btn-edit-obs').forEach(btn => {
|
container.querySelectorAll('.btn-edit-obs').forEach(btn => {
|
||||||
btn.addEventListener('click', async () => {
|
btn.addEventListener('click', async () => {
|
||||||
@@ -707,31 +824,33 @@ async function abrirItensComanda(comandaIdOrObj) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const processarResultadoAdd = async (r, prod, obs = '') => {
|
const processarResultadoAdd = async (r, prod, obs = '', loggedUser = null) => {
|
||||||
if (r.ok) {
|
if (r.ok) {
|
||||||
if (!comanda.items) comanda.items = [];
|
if (!comanda.items) comanda.items = [];
|
||||||
|
|
||||||
const novoItem = r.data;
|
const novoItem = r.data;
|
||||||
// Normaliza o campo product caso o servidor retorne product_id
|
|
||||||
if (!novoItem.product && novoItem.product_id) novoItem.product = novoItem.product_id;
|
if (!novoItem.product && novoItem.product_id) novoItem.product = novoItem.product_id;
|
||||||
|
|
||||||
comanda.items.push(novoItem);
|
comanda.items.push(novoItem);
|
||||||
renderLeft();
|
renderLeft();
|
||||||
loadComandas(_mesasRef);
|
loadComandas(_mesasRef);
|
||||||
|
|
||||||
// Se o produto for de cozinha, cria a order na nova rota
|
|
||||||
if (prod && prod.cuisine) {
|
if (prod && prod.cuisine) {
|
||||||
const orderPayload = {
|
const orderPayload = {
|
||||||
productComanda: novoItem.id, // ID do item vinculado
|
productComanda: novoItem.id,
|
||||||
id_product: prod.id,
|
id_product: prod.id,
|
||||||
id_comanda: comanda.id,
|
id_comanda: comanda.id,
|
||||||
obs: obs || novoItem.obs || ''
|
obs: obs || novoItem.obs || '',
|
||||||
|
applicant: loggedUser?.username || 'Sistema'
|
||||||
};
|
};
|
||||||
|
|
||||||
console.log('[PDV] Criando pedido na cozinha:', orderPayload);
|
console.log('[PDV] Criando pedido na cozinha:', orderPayload);
|
||||||
const orderRes = await window.electronAPI.post('/orders', orderPayload);
|
const orderRes = await window.electronAPI.post('/orders', orderPayload);
|
||||||
if (orderRes.ok) {
|
if (orderRes.ok) {
|
||||||
showToast('Pedido enviado para a cozinha!', 'success');
|
showToast('Pedido enviado para a cozinha!', 'success');
|
||||||
|
setTimeout(() => {
|
||||||
|
imprimirTicketCozinha(comanda, novoItem, prod, obs, loggedUser);
|
||||||
|
}, 300);
|
||||||
} else {
|
} else {
|
||||||
showToast('Item adicionado, mas falhou ao enviar para cozinha.', 'warning');
|
showToast('Item adicionado, mas falhou ao enviar para cozinha.', 'warning');
|
||||||
}
|
}
|
||||||
@@ -763,7 +882,7 @@ async function abrirItensComanda(comandaIdOrObj) {
|
|||||||
obs: obs,
|
obs: obs,
|
||||||
applicant: loggedUser?.username || 'Sistema'
|
applicant: loggedUser?.username || 'Sistema'
|
||||||
});
|
});
|
||||||
processarResultadoAdd(r, prod, obs);
|
processarResultadoAdd(r, prod, obs, loggedUser);
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
const loggedUser = await window.electronAPI.getUser();
|
const loggedUser = await window.electronAPI.getUser();
|
||||||
@@ -786,7 +905,7 @@ async function abrirItensComanda(comandaIdOrObj) {
|
|||||||
|
|
||||||
// console.log('Produtos carregados no PDV:', todosProdutos);
|
// console.log('Produtos carregados no PDV:', todosProdutos);
|
||||||
container.innerHTML = filtrados.map(p => {
|
container.innerHTML = filtrados.map(p => {
|
||||||
const imgTarget = p.image ? `url('${p.image}')` : `url('https://wallpapers.com/images/featured/fundo-abstrato-escuro-27kvn4ewpldsngbu.jpg')`;
|
const imgTarget = `url('${getProductImage(p)}')`;
|
||||||
return `
|
return `
|
||||||
<div class="pdv-product-card" data-id="${p.id}">
|
<div class="pdv-product-card" data-id="${p.id}">
|
||||||
<div class="pdv-product-bg" style="background-image: ${imgTarget}"></div>
|
<div class="pdv-product-bg" style="background-image: ${imgTarget}"></div>
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
export async function renderPagamentos(container) {
|
export async function renderPagamentos(container) {
|
||||||
// Carrega tipos de pagamento para o formulário de novo registro
|
const [tRes, cRes, cliRes] = await Promise.all([
|
||||||
const [tRes, cRes] = await Promise.all([
|
|
||||||
window.electronAPI.get('/payment-types'),
|
window.electronAPI.get('/payment-types'),
|
||||||
window.electronAPI.get('/comandas'),
|
window.electronAPI.get('/comandas'),
|
||||||
|
window.electronAPI.get('/clients'),
|
||||||
]);
|
]);
|
||||||
const tiposPag = tRes.ok ? tRes.data : [];
|
const tiposPag = tRes.ok ? tRes.data : [];
|
||||||
const comandas = cRes.ok ? cRes.data : [];
|
const comandas = cRes.ok ? cRes.data : [];
|
||||||
|
const clientes = cliRes.ok ? cliRes.data : [];
|
||||||
|
|
||||||
container.innerHTML = `
|
container.innerHTML = `
|
||||||
<div class="page-header">
|
<div class="page-header">
|
||||||
@@ -28,45 +29,62 @@ export async function renderPagamentos(container) {
|
|||||||
<div id="pagamentos-table"></div>
|
<div id="pagamentos-table"></div>
|
||||||
</div>`;
|
</div>`;
|
||||||
|
|
||||||
await loadPagamentos(tiposPag, comandas);
|
await loadPagamentos(tiposPag, comandas, clientes);
|
||||||
|
|
||||||
document.getElementById('btn-novo-pag').addEventListener('click', () => abrirModalPagamento(tiposPag, comandas));
|
document.getElementById('btn-novo-pag').addEventListener('click', () => abrirModalPagamento(tiposPag, comandas));
|
||||||
document.getElementById('search-pag').addEventListener('input', () => filtrarPagamentos(tiposPag));
|
document.getElementById('search-pag').addEventListener('input', () => filtrarPagamentos());
|
||||||
document.getElementById('filter-tipo').addEventListener('change', () => filtrarPagamentos(tiposPag));
|
document.getElementById('filter-tipo').addEventListener('change', () => filtrarPagamentos());
|
||||||
}
|
}
|
||||||
|
|
||||||
let _pagsData = [];
|
let _pagsData = [];
|
||||||
|
let _cmdMap = {};
|
||||||
|
let _tiposPag = [];
|
||||||
|
let _clientesMap = {};
|
||||||
|
|
||||||
async function loadPagamentos(tiposPag, comandas) {
|
async function loadPagamentos(tiposPag, comandas, clientes) {
|
||||||
const wrap = document.getElementById('pagamentos-table');
|
const wrap = document.getElementById('pagamentos-table');
|
||||||
if (!wrap) return;
|
if (!wrap) return;
|
||||||
wrap.innerHTML = `<div class="loading-screen"><div class="spinner"></div></div>`;
|
wrap.innerHTML = `<div class="loading-screen"><div class="spinner"></div></div>`;
|
||||||
|
|
||||||
|
_tiposPag = tiposPag;
|
||||||
|
|
||||||
const res = await window.electronAPI.get('/payments');
|
const res = await window.electronAPI.get('/payments');
|
||||||
if (!res.ok) { wrap.innerHTML = `<div class="table-empty">Erro ao carregar pagamentos.</div>`; return; }
|
if (!res.ok) { wrap.innerHTML = `<div class="table-empty">Erro ao carregar pagamentos.</div>`; return; }
|
||||||
|
|
||||||
// Ordena decrescente (mais novos primeiro)
|
|
||||||
_pagsData = (res.data || []).sort((a, b) => b.id - a.id);
|
_pagsData = (res.data || []).sort((a, b) => b.id - a.id);
|
||||||
|
|
||||||
// Cria mapa de comandas para consulta rápida
|
_cmdMap = (comandas || []).reduce((acc, c) => {
|
||||||
const cmdMap = (comandas || []).reduce((acc, c) => {
|
acc[String(c.id)] = {
|
||||||
acc[String(c.id)] = `${c.name || 'Sem nome'} (${c.mesa_name || `Mesa ${c.mesa}`})`;
|
name: c.name || 'Sem nome',
|
||||||
|
mesa: c.mesa_name || `Mesa ${c.mesa}`,
|
||||||
|
mesa_name: c.mesa_name || '',
|
||||||
|
status: c.status,
|
||||||
|
dt_open: c.dt_open,
|
||||||
|
client: c.client
|
||||||
|
};
|
||||||
return acc;
|
return acc;
|
||||||
}, {});
|
}, {});
|
||||||
|
|
||||||
renderPagsTable(_pagsData, cmdMap);
|
if (clientes) {
|
||||||
|
_clientesMap = (clientes || []).reduce((acc, c) => {
|
||||||
|
acc[String(c.id)] = c.name || '–';
|
||||||
|
return acc;
|
||||||
|
}, {});
|
||||||
|
}
|
||||||
|
|
||||||
|
renderPagsTable();
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderPagsTable(data, cmdMap = {}) {
|
function renderPagsTable() {
|
||||||
const wrap = document.getElementById('pagamentos-table');
|
const wrap = document.getElementById('pagamentos-table');
|
||||||
if (!wrap) return;
|
if (!wrap) return;
|
||||||
|
|
||||||
if (!data.length) {
|
if (!_pagsData.length) {
|
||||||
wrap.innerHTML = `<div class="table-empty">Nenhum pagamento registrado.</div>`;
|
wrap.innerHTML = `<div class="table-empty">Nenhum pagamento registrado.</div>`;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Soma total dos pagamentos exibidos
|
const total = _pagsData.reduce((acc, p) => acc + parseFloat(p.value || 0), 0);
|
||||||
const total = data.reduce((acc, p) => acc + parseFloat(p.value || 0), 0);
|
|
||||||
|
|
||||||
wrap.innerHTML = `
|
wrap.innerHTML = `
|
||||||
<table>
|
<table>
|
||||||
@@ -81,12 +99,14 @@ function renderPagsTable(data, cmdMap = {}) {
|
|||||||
<th>Ações</th>
|
<th>Ações</th>
|
||||||
</tr></thead>
|
</tr></thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
${data.map(p => {
|
${_pagsData.map(p => {
|
||||||
const cDesc = cmdMap[String(p.comanda)] || p.comanda_name || '–';
|
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 `
|
return `
|
||||||
<tr>
|
<tr>
|
||||||
<td style="color:var(--text-muted)">#${p.id}</td>
|
<td style="color:var(--text-muted)">#${p.id}</td>
|
||||||
<td>${p.client_name || '–'}</td>
|
<td>${clienteNome}</td>
|
||||||
<td>
|
<td>
|
||||||
${p.comanda ? `<span style="font-size:0.8rem">
|
${p.comanda ? `<span style="font-size:0.8rem">
|
||||||
<span style="color:var(--text-muted)">#${p.comanda}</span>
|
<span style="color:var(--text-muted)">#${p.comanda}</span>
|
||||||
@@ -100,7 +120,7 @@ function renderPagsTable(data, cmdMap = {}) {
|
|||||||
</td>
|
</td>
|
||||||
<td style="white-space:nowrap;font-size:0.82rem">${formatDate(p.datetime)}</td>
|
<td style="white-space:nowrap;font-size:0.82rem">${formatDate(p.datetime)}</td>
|
||||||
<td>
|
<td>
|
||||||
<button class="btn btn-danger btn-sm btn-del-pag" data-id="${p.id}">Excluir</button>
|
<button class="btn btn-secondary btn-sm btn-view-pag" data-id="${p.id}">🔍 Ver</button>
|
||||||
</td>
|
</td>
|
||||||
</tr>`;
|
</tr>`;
|
||||||
}).join('')}
|
}).join('')}
|
||||||
@@ -111,29 +131,161 @@ function renderPagsTable(data, cmdMap = {}) {
|
|||||||
<strong style="color:var(--success);font-size:1rem">R$ ${total.toFixed(2)}</strong>
|
<strong style="color:var(--success);font-size:1rem">R$ ${total.toFixed(2)}</strong>
|
||||||
</div>`;
|
</div>`;
|
||||||
|
|
||||||
wrap.querySelectorAll('.btn-del-pag').forEach(btn =>
|
wrap.querySelectorAll('.btn-view-pag').forEach(btn =>
|
||||||
btn.addEventListener('click', async () => {
|
btn.addEventListener('click', () => {
|
||||||
const r = await window.electronAPI.delete(`/payments/${btn.dataset.id}`);
|
const pag = _pagsData.find(p => String(p.id) === String(btn.dataset.id));
|
||||||
if (r.ok) { showToast('Pagamento excluído!', 'success'); loadPagamentos([], []); }
|
if (pag) abrirDetalhesPagamento(pag);
|
||||||
else showToast(r.error, 'error');
|
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function filtrarPagamentos(tiposPag) {
|
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>`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function filtrarPagamentos() {
|
||||||
const q = document.getElementById('search-pag')?.value.toLowerCase() || '';
|
const q = document.getElementById('search-pag')?.value.toLowerCase() || '';
|
||||||
const tipo = parseInt(document.getElementById('filter-tipo')?.value) || null;
|
const tipo = parseInt(document.getElementById('filter-tipo')?.value) || null;
|
||||||
|
|
||||||
const filtered = _pagsData.filter(p => {
|
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 ||
|
const matchQ = !q ||
|
||||||
(p.client_name || '').toLowerCase().includes(q) ||
|
clienteNome.toLowerCase().includes(q) ||
|
||||||
(p.comanda_name || '').toLowerCase().includes(q) ||
|
comandaNome.toLowerCase().includes(q) ||
|
||||||
(p.description || '').toLowerCase().includes(q) ||
|
(p.description || '').toLowerCase().includes(q) ||
|
||||||
String(p.id).includes(q);
|
String(p.id).includes(q);
|
||||||
const matchTipo = !tipo || p.type_pay === tipo;
|
const matchTipo = !tipo || p.type_pay === tipo;
|
||||||
return matchQ && matchTipo;
|
return matchQ && matchTipo;
|
||||||
});
|
});
|
||||||
renderPagsTable(filtered);
|
|
||||||
|
const wrap = document.getElementById('pagamentos-table');
|
||||||
|
if (!wrap) return;
|
||||||
|
|
||||||
|
if (!filtered.length) {
|
||||||
|
wrap.innerHTML = `<div class="table-empty">Nenhum pagamento encontrado.</div>`;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const total = filtered.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>
|
||||||
|
${filtered.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 abrirModalPagamento(tiposPag, comandas) {
|
function abrirModalPagamento(tiposPag, comandas) {
|
||||||
@@ -185,7 +337,7 @@ function abrirModalPagamento(tiposPag, comandas) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const r = await window.electronAPI.post('/payments', data);
|
const r = await window.electronAPI.post('/payments', data);
|
||||||
if (r.ok) { showToast('Pagamento registrado!', 'success'); closeModal(); loadPagamentos(tiposPag, comandas); }
|
if (r.ok) { showToast('Pagamento registrado!', 'success'); closeModal(); loadPagamentos(_tiposPag, [], null); }
|
||||||
else showToast(r.error, 'error');
|
else showToast(r.error, 'error');
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -54,6 +54,7 @@ let _ordersData = [];
|
|||||||
let _productsMap = {};
|
let _productsMap = {};
|
||||||
let _comandasMap = {};
|
let _comandasMap = {};
|
||||||
let _mesasMap = {};
|
let _mesasMap = {};
|
||||||
|
let _productsFullMap = {};
|
||||||
|
|
||||||
async function loadOrders() {
|
async function loadOrders() {
|
||||||
const wrap = document.getElementById('orders-table');
|
const wrap = document.getElementById('orders-table');
|
||||||
@@ -70,13 +71,17 @@ async function loadOrders() {
|
|||||||
|
|
||||||
if (!res.ok) { wrap.innerHTML = `<div class="table-empty">Erro ao carregar pedidos.</div>`; return; }
|
if (!res.ok) { wrap.innerHTML = `<div class="table-empty">Erro ao carregar pedidos.</div>`; return; }
|
||||||
|
|
||||||
// Constrói mapas para consulta rápida (IDs como string para segurança)
|
if (pRes.ok) {
|
||||||
if (pRes.ok) _productsMap = pRes.data.reduce((acc, p) => { acc[String(p.id)] = p.name; return acc; }, {});
|
_productsMap = pRes.data.reduce((acc, p) => { acc[String(p.id)] = p.name; return acc; }, {});
|
||||||
|
_productsFullMap = pRes.data.reduce((acc, p) => { acc[String(p.id)] = p; return acc; }, {});
|
||||||
|
}
|
||||||
if (mRes.ok) _mesasMap = mRes.data.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) _comandasMap = cRes.data.reduce((acc, c) => {
|
||||||
acc[String(c.id)] = {
|
acc[String(c.id)] = {
|
||||||
|
id: c.id,
|
||||||
name: c.name || '–',
|
name: c.name || '–',
|
||||||
mesa: _mesasMap[String(c.mesa)] || `Mesa ${c.mesa}` || '–'
|
mesa: _mesasMap[String(c.mesa)] || `Mesa ${c.mesa}` || '–',
|
||||||
|
mesa_name: c.mesa_name || _mesasMap[String(c.mesa)] || '–'
|
||||||
};
|
};
|
||||||
return acc;
|
return acc;
|
||||||
}, {});
|
}, {});
|
||||||
@@ -132,6 +137,7 @@ function renderOrdersTable(data) {
|
|||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<div style="display:flex;gap:6px;flex-wrap:wrap">
|
<div style="display:flex;gap:6px;flex-wrap:wrap">
|
||||||
|
<button class="btn btn-secondary btn-sm btn-reprint" data-id="${o.id}" title="Reimprimir Ticket">🖨️</button>
|
||||||
${cfg.next ? `<button class="btn btn-success btn-sm btn-avanca" data-id="${o.id}" data-next="${cfg.next}">${cfg.nextLabel}</button>` : ''}
|
${cfg.next ? `<button class="btn btn-success btn-sm btn-avanca" data-id="${o.id}" data-next="${cfg.next}">${cfg.nextLabel}</button>` : ''}
|
||||||
${o.status !== 'Cancelado' && o.status !== 'Entregue'
|
${o.status !== 'Cancelado' && o.status !== 'Entregue'
|
||||||
? `<button class="btn btn-danger btn-sm btn-cancela" data-id="${o.id}">✕</button>`
|
? `<button class="btn btn-danger btn-sm btn-cancela" data-id="${o.id}">✕</button>`
|
||||||
@@ -169,6 +175,92 @@ function renderOrdersTable(data) {
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Reimprimir ticket de cozinha
|
||||||
|
wrap.querySelectorAll('.btn-reprint').forEach(btn =>
|
||||||
|
btn.addEventListener('click', async () => {
|
||||||
|
const orderId = btn.dataset.id;
|
||||||
|
const order = _ordersData.find(o => String(o.id) === String(orderId));
|
||||||
|
if (!order) return showToast('Pedido não encontrado.', 'error');
|
||||||
|
|
||||||
|
const comanda = _comandasMap[String(order.id_comanda)];
|
||||||
|
const product = _productsFullMap[String(order.id_product)];
|
||||||
|
|
||||||
|
const dataAtual = new Date().toLocaleString('pt-BR');
|
||||||
|
const nomeEstabelecimento = 'RRBEC - Bar & Restaurante';
|
||||||
|
const nomeProduto = product?.name || order.product_name || `Produto #${order.id_product}`;
|
||||||
|
const obs = order.obs || '';
|
||||||
|
const usuario = order.applicant || 'Sistema';
|
||||||
|
const nomeComanda = comanda?.name || `Comanda #${comanda?.id || order.id_comanda}`;
|
||||||
|
const nomeMesa = comanda?.mesa_name || comanda?.mesa || '–';
|
||||||
|
|
||||||
|
const htmlTicket = `
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>Ticket Cozinha - ${nomeComanda}</title>
|
||||||
|
<style>
|
||||||
|
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||||
|
body { font-family: 'Courier New', monospace; font-size: 13px; padding: 8px; width: 80mm; }
|
||||||
|
.ticket { display: block; color: black; }
|
||||||
|
.ticket * { color: black !important; background: transparent !important; }
|
||||||
|
.header { text-align: center; border-bottom: 2px solid #000; padding-bottom: 8px; margin-bottom: 8px; }
|
||||||
|
.title { font-size: 16px; font-weight: bold; text-transform: uppercase; }
|
||||||
|
.subtitle { font-size: 12px; margin-top: 4px; }
|
||||||
|
.info { margin: 4px 0; font-size: 12px; }
|
||||||
|
.info strong { font-size: 14px; }
|
||||||
|
.product { font-size: 18px; font-weight: bold; text-align: center; margin: 12px 0; padding: 8px; border: 3px double #000; text-transform: uppercase; }
|
||||||
|
.obs { font-style: italic; font-size: 11px; text-align: center; margin-top: 8px; padding: 6px; border: 1px dashed #000; }
|
||||||
|
.footer { text-align: center; font-size: 10px; margin-top: 8px; color: #666; }
|
||||||
|
@media print { @page { size: 80mm auto; margin: 0; } }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="ticket">
|
||||||
|
<div class="header">
|
||||||
|
<div class="title">🍳 COZINHA - REIMPRESSÃO</div>
|
||||||
|
<div class="subtitle">${nomeEstabelecimento}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="info" style="text-align:center">
|
||||||
|
<strong>${nomeComanda}</strong>
|
||||||
|
</div>
|
||||||
|
<div class="info" style="display:flex;justify-content:space-between">
|
||||||
|
<span>Mesa: ${nomeMesa}</span>
|
||||||
|
<span>${dataAtual}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="product">${nomeProduto}</div>
|
||||||
|
|
||||||
|
${obs ? `<div class="obs">OBS: ${obs}</div>` : ''}
|
||||||
|
|
||||||
|
<div class="footer">
|
||||||
|
Atendido por: ${usuario}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
`;
|
||||||
|
|
||||||
|
window.electronAPI.printDirect(htmlTicket).then(r => {
|
||||||
|
if (r.ok) {
|
||||||
|
showToast('Ticket reimpresso!', 'success');
|
||||||
|
} else if (r.error === 'NO_PRINTER') {
|
||||||
|
showToast('⚠️ Nenhuma impressora configurada. Configure uma impressora nas configurações do sistema.', 'warning', 5000);
|
||||||
|
const printWindow = window.open('', '', 'width=300,height=400');
|
||||||
|
printWindow.document.write(htmlTicket);
|
||||||
|
printWindow.document.close();
|
||||||
|
setTimeout(() => printWindow.print(), 300);
|
||||||
|
} else {
|
||||||
|
const printWindow = window.open('', '', 'width=300,height=400');
|
||||||
|
printWindow.document.write(htmlTicket);
|
||||||
|
printWindow.document.close();
|
||||||
|
setTimeout(() => printWindow.print(), 300);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
// Editar observação
|
// Editar observação
|
||||||
wrap.querySelectorAll('.btn-edit-obs').forEach(btn =>
|
wrap.querySelectorAll('.btn-edit-obs').forEach(btn =>
|
||||||
btn.addEventListener('click', () => {
|
btn.addEventListener('click', () => {
|
||||||
|
|||||||
@@ -146,10 +146,21 @@ function filtrarProdutos(categorias, unidades) {
|
|||||||
|
|
||||||
function abrirModalProduto(produto, categorias, unidades) {
|
function abrirModalProduto(produto, categorias, unidades) {
|
||||||
const isEdit = !!produto;
|
const isEdit = !!produto;
|
||||||
|
const imagemAtual = produto?.image || '';
|
||||||
|
|
||||||
openModal({
|
openModal({
|
||||||
title: isEdit ? `Editar: ${produto.name}` : 'Novo Produto',
|
title: isEdit ? `Editar: ${produto.name}` : 'Novo Produto',
|
||||||
body: `
|
body: `
|
||||||
<div class="form-grid">
|
<div class="form-grid">
|
||||||
|
<div class="form-group" style="grid-column:1/-1; text-align:center">
|
||||||
|
<div id="prod-img-preview" style="width:120px;height:120px;border-radius:var(--radius-sm);border:2px dashed var(--border);margin:0 auto 10px;display:flex;align-items:center;justify-content:center;overflow:hidden;background:var(--bg-elevated);cursor:pointer">
|
||||||
|
${imagemAtual ? `<img src="${imagemAtual}" style="max-width:100%;max-height:100%;object-fit:cover" />` : '<span style="font-size:2rem;color:var(--text-muted)">📷</span>'}
|
||||||
|
</div>
|
||||||
|
<input type="file" id="prod-img-file" accept="image/*" style="display:none" />
|
||||||
|
<button type="button" class="btn btn-secondary btn-sm" id="btn-select-img">Selecionar Imagem</button>
|
||||||
|
${imagemAtual ? '<button type="button" class="btn btn-ghost btn-sm" id="btn-remove-img" style="color:var(--danger)">Remover</button>' : ''}
|
||||||
|
<input type="hidden" id="prod-img" value="${imagemAtual}" />
|
||||||
|
</div>
|
||||||
<div class="form-group" style="grid-column:1/-1">
|
<div class="form-group" style="grid-column:1/-1">
|
||||||
<label>Nome</label>
|
<label>Nome</label>
|
||||||
<input type="text" id="prod-nome" class="form-control" value="${produto?.name || ''}" placeholder="Nome do produto" />
|
<input type="text" id="prod-nome" class="form-control" value="${produto?.name || ''}" placeholder="Nome do produto" />
|
||||||
@@ -194,16 +205,43 @@ function abrirModalProduto(produto, categorias, unidades) {
|
|||||||
<label>Descrição</label>
|
<label>Descrição</label>
|
||||||
<input type="text" id="prod-desc" class="form-control" value="${produto?.description || ''}" placeholder="Descrição opcional" />
|
<input type="text" id="prod-desc" class="form-control" value="${produto?.description || ''}" placeholder="Descrição opcional" />
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group" style="grid-column:1/-1">
|
|
||||||
<label>URL da Imagem</label>
|
|
||||||
<input type="text" id="prod-img" class="form-control" value="${produto?.image || ''}" placeholder="http://..." />
|
|
||||||
</div>
|
|
||||||
</div>`,
|
</div>`,
|
||||||
footer: `
|
footer: `
|
||||||
<button class="btn btn-secondary btn-md" onclick="closeModal()">Cancelar</button>
|
<button class="btn btn-secondary btn-md" onclick="closeModal()">Cancelar</button>
|
||||||
<button class="btn btn-primary btn-md" id="btn-salvar-prod">${isEdit ? 'Salvar Alterações' : 'Criar Produto'}</button>`,
|
<button class="btn btn-primary btn-md" id="btn-salvar-prod">${isEdit ? 'Salvar Alterações' : 'Criar Produto'}</button>`,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const previewEl = document.getElementById('prod-img-preview');
|
||||||
|
const fileInput = document.getElementById('prod-img-file');
|
||||||
|
const imgInput = document.getElementById('prod-img');
|
||||||
|
|
||||||
|
previewEl.addEventListener('click', () => fileInput.click());
|
||||||
|
document.getElementById('btn-select-img').addEventListener('click', () => fileInput.click());
|
||||||
|
|
||||||
|
fileInput.addEventListener('change', () => {
|
||||||
|
const file = fileInput.files[0];
|
||||||
|
if (!file) return;
|
||||||
|
if (file.size > 5 * 1024 * 1024) {
|
||||||
|
showToast('Imagem muito grande. Máximo 5MB.', 'warning');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onload = (e) => {
|
||||||
|
const base64 = e.target.result;
|
||||||
|
imgInput.value = base64;
|
||||||
|
previewEl.innerHTML = `<img src="${base64}" style="max-width:100%;max-height:100%;object-fit:cover" />`;
|
||||||
|
};
|
||||||
|
reader.readAsDataURL(file);
|
||||||
|
});
|
||||||
|
|
||||||
|
const btnRemoveImg = document.getElementById('btn-remove-img');
|
||||||
|
if (btnRemoveImg) {
|
||||||
|
btnRemoveImg.addEventListener('click', () => {
|
||||||
|
imgInput.value = '';
|
||||||
|
previewEl.innerHTML = '<span style="font-size:2rem;color:var(--text-muted)">📷</span>';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
document.getElementById('btn-salvar-prod').addEventListener('click', async () => {
|
document.getElementById('btn-salvar-prod').addEventListener('click', async () => {
|
||||||
const btn = document.getElementById('btn-salvar-prod');
|
const btn = document.getElementById('btn-salvar-prod');
|
||||||
btn.disabled = true;
|
btn.disabled = true;
|
||||||
@@ -212,7 +250,6 @@ function abrirModalProduto(produto, categorias, unidades) {
|
|||||||
const catVal = parseInt(document.getElementById('prod-cat').value);
|
const catVal = parseInt(document.getElementById('prod-cat').value);
|
||||||
const unitVal = parseInt(document.getElementById('prod-unit').value);
|
const unitVal = parseInt(document.getElementById('prod-unit').value);
|
||||||
|
|
||||||
// Constrói o payload enviando apenas o que é necessário/preenchido
|
|
||||||
const data = {
|
const data = {
|
||||||
name: document.getElementById('prod-nome').value.trim(),
|
name: document.getElementById('prod-nome').value.trim(),
|
||||||
description: document.getElementById('prod-desc').value.trim(),
|
description: document.getElementById('prod-desc').value.trim(),
|
||||||
@@ -220,9 +257,11 @@ function abrirModalProduto(produto, categorias, unidades) {
|
|||||||
quantity: parseInt(document.getElementById('prod-qty').value) || 0,
|
quantity: parseInt(document.getElementById('prod-qty').value) || 0,
|
||||||
active: document.getElementById('prod-ativo').value === 'true',
|
active: document.getElementById('prod-ativo').value === 'true',
|
||||||
cuisine: document.getElementById('prod-cuisine').value === 'true',
|
cuisine: document.getElementById('prod-cuisine').value === 'true',
|
||||||
image: document.getElementById('prod-img').value.trim(),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const imgValue = document.getElementById('prod-img').value.trim();
|
||||||
|
if (imgValue) data.image = imgValue;
|
||||||
|
|
||||||
if (catVal) data.category = catVal;
|
if (catVal) data.category = catVal;
|
||||||
if (unitVal) data.unit_of_measure = unitVal;
|
if (unitVal) data.unit_of_measure = unitVal;
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user