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) {
|
||||
resolve({ ok: true });
|
||||
} 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 _productsMap = {};
|
||||
let _paymentTypes = [];
|
||||
let _paymentsMap = {};
|
||||
|
||||
async function loadClientes() {
|
||||
const wrap = document.getElementById('clientes-table');
|
||||
if (!wrap) return;
|
||||
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] = await Promise.all([
|
||||
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('/payment-types'),
|
||||
window.electronAPI.get('/payments')
|
||||
]);
|
||||
|
||||
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; }
|
||||
|
||||
_clientesData = res.data || [];
|
||||
_comandasData = cRes.ok ? (cRes.data || []) : [];
|
||||
|
||||
// Calcula o débito real de cada cliente somando suas comandas FIADO
|
||||
_clientesData.forEach(c => {
|
||||
const fiados = _comandasData.filter(com => {
|
||||
// Baseado no model Go: json:"client"
|
||||
const cid = com.client;
|
||||
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);
|
||||
return sum + preco;
|
||||
}, 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);
|
||||
});
|
||||
|
||||
@@ -293,21 +304,27 @@ async function abrirHistoricoFiados(cliente) {
|
||||
const preco = pInfo ? pInfo.price : parseFloat(it.product_price || 0);
|
||||
return acc + preco;
|
||||
}, 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 `
|
||||
<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%)">
|
||||
<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 style="display:flex; justify-content:space-between; align-items:flex-start; margin-bottom:10px;">
|
||||
<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>
|
||||
${temPagamentos ? `<div style="font-size:0.75rem; color:var(--success); margin-top:2px">Pago: R$ ${totalPago.toFixed(2)}</div>` : ''}
|
||||
</div>
|
||||
<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 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>
|
||||
|
||||
@@ -333,6 +350,24 @@ async function abrirHistoricoFiados(cliente) {
|
||||
`}).join('')}
|
||||
</ul>
|
||||
</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>
|
||||
`;
|
||||
@@ -342,13 +377,17 @@ async function abrirHistoricoFiados(cliente) {
|
||||
const updateSum = () => {
|
||||
const checks = listContainer.querySelectorAll('.fiado-check:checked');
|
||||
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);
|
||||
|
||||
const btnPagar = document.getElementById('btn-pagar-selecionados');
|
||||
btnPagar.disabled = checks.length === 0;
|
||||
btnPagar.disabled = count === 0 || sum <= 0;
|
||||
};
|
||||
|
||||
listContainer.querySelectorAll('.fiado-check').forEach(chk => {
|
||||
|
||||
@@ -108,16 +108,113 @@ function imprimirComanda(comanda, pagamentosComanda, totalComanda) {
|
||||
window.electronAPI.printDirect(htmlCompleto).then(r => {
|
||||
if (r.ok) {
|
||||
showToast('Impressão enviada!', 'success');
|
||||
} else {
|
||||
showToast('Nenhuma impressora configurada. Abrindo diálogo...', 'warning');
|
||||
} 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=600');
|
||||
printWindow.document.write(htmlCompleto);
|
||||
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) {
|
||||
container.innerHTML = `
|
||||
<div class="page-header">
|
||||
@@ -471,9 +568,11 @@ async function abrirItensComanda(comandaIdOrObj) {
|
||||
const ativa = comanda.status === 'OPEN' || comanda.status === 'PAYING';
|
||||
const podeAdd = comanda.status === 'OPEN';
|
||||
|
||||
|
||||
// Carrega produtos (ativos)
|
||||
const pRes = await window.electronAPI.get('/products');
|
||||
// Carrega produtos (ativos) e usuário logado
|
||||
const [pRes, loggedUser] = await Promise.all([
|
||||
window.electronAPI.get('/products'),
|
||||
window.electronAPI.getUser()
|
||||
]);
|
||||
let todosProdutos = pRes.ok ? pRes.data.filter(p => p.active) : [];
|
||||
|
||||
openModal({
|
||||
@@ -497,7 +596,7 @@ async function abrirItensComanda(comandaIdOrObj) {
|
||||
</div>
|
||||
</div>`,
|
||||
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>
|
||||
`,
|
||||
});
|
||||
@@ -531,7 +630,7 @@ async function abrirItensComanda(comandaIdOrObj) {
|
||||
<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: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>
|
||||
<tbody>
|
||||
${itens.map(it => {
|
||||
@@ -542,24 +641,29 @@ async function abrirItensComanda(comandaIdOrObj) {
|
||||
return `
|
||||
<tr data-item-id="${it.id}">
|
||||
<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 style="padding:10px 0;text-align:right;border-bottom:1px solid var(--border)">
|
||||
R$ ${(_productsMap[String(it.product)] || 0).toFixed(2)}
|
||||
</td>
|
||||
${podeAdd ? `
|
||||
<td style="padding:10px 0;text-align:center;border-bottom:1px solid var(--border)">
|
||||
<div style="display:flex; gap:8px; justify-content:center">
|
||||
${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 class="btn btn-ghost btn-sm btn-del-item" data-id="${it.id}" title="Excluir item" style="color: var(--danger); font-size: 1rem;">
|
||||
🗑️
|
||||
</button>
|
||||
${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>
|
||||
` : ''}
|
||||
</div>
|
||||
</td>` : ''}
|
||||
</td>
|
||||
</tr>`;
|
||||
}).join('')}
|
||||
</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
|
||||
container.querySelectorAll('.btn-edit-obs').forEach(btn => {
|
||||
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 (!comanda.items) comanda.items = [];
|
||||
|
||||
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;
|
||||
|
||||
comanda.items.push(novoItem);
|
||||
renderLeft();
|
||||
loadComandas(_mesasRef);
|
||||
|
||||
// Se o produto for de cozinha, cria a order na nova rota
|
||||
if (prod && prod.cuisine) {
|
||||
const orderPayload = {
|
||||
productComanda: novoItem.id, // ID do item vinculado
|
||||
productComanda: novoItem.id,
|
||||
id_product: prod.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);
|
||||
const orderRes = await window.electronAPI.post('/orders', orderPayload);
|
||||
if (orderRes.ok) {
|
||||
showToast('Pedido enviado para a cozinha!', 'success');
|
||||
setTimeout(() => {
|
||||
imprimirTicketCozinha(comanda, novoItem, prod, obs, loggedUser);
|
||||
}, 300);
|
||||
} else {
|
||||
showToast('Item adicionado, mas falhou ao enviar para cozinha.', 'warning');
|
||||
}
|
||||
@@ -763,7 +882,7 @@ async function abrirItensComanda(comandaIdOrObj) {
|
||||
obs: obs,
|
||||
applicant: loggedUser?.username || 'Sistema'
|
||||
});
|
||||
processarResultadoAdd(r, prod, obs);
|
||||
processarResultadoAdd(r, prod, obs, loggedUser);
|
||||
});
|
||||
} else {
|
||||
const loggedUser = await window.electronAPI.getUser();
|
||||
@@ -786,7 +905,7 @@ async function abrirItensComanda(comandaIdOrObj) {
|
||||
|
||||
// console.log('Produtos carregados no PDV:', todosProdutos);
|
||||
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 `
|
||||
<div class="pdv-product-card" data-id="${p.id}">
|
||||
<div class="pdv-product-bg" style="background-image: ${imgTarget}"></div>
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
export async function renderPagamentos(container) {
|
||||
// Carrega tipos de pagamento para o formulário de novo registro
|
||||
const [tRes, cRes] = await Promise.all([
|
||||
const [tRes, cRes, cliRes] = await Promise.all([
|
||||
window.electronAPI.get('/payment-types'),
|
||||
window.electronAPI.get('/comandas'),
|
||||
window.electronAPI.get('/clients'),
|
||||
]);
|
||||
const tiposPag = tRes.ok ? tRes.data : [];
|
||||
const comandas = cRes.ok ? cRes.data : [];
|
||||
const clientes = cliRes.ok ? cliRes.data : [];
|
||||
|
||||
container.innerHTML = `
|
||||
<div class="page-header">
|
||||
@@ -28,45 +29,62 @@ export async function renderPagamentos(container) {
|
||||
<div id="pagamentos-table"></div>
|
||||
</div>`;
|
||||
|
||||
await loadPagamentos(tiposPag, comandas);
|
||||
await loadPagamentos(tiposPag, comandas, clientes);
|
||||
|
||||
document.getElementById('btn-novo-pag').addEventListener('click', () => abrirModalPagamento(tiposPag, comandas));
|
||||
document.getElementById('search-pag').addEventListener('input', () => filtrarPagamentos(tiposPag));
|
||||
document.getElementById('filter-tipo').addEventListener('change', () => filtrarPagamentos(tiposPag));
|
||||
document.getElementById('search-pag').addEventListener('input', () => filtrarPagamentos());
|
||||
document.getElementById('filter-tipo').addEventListener('change', () => filtrarPagamentos());
|
||||
}
|
||||
|
||||
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');
|
||||
if (!wrap) return;
|
||||
wrap.innerHTML = `<div class="loading-screen"><div class="spinner"></div></div>`;
|
||||
|
||||
_tiposPag = tiposPag;
|
||||
|
||||
const res = await window.electronAPI.get('/payments');
|
||||
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);
|
||||
|
||||
// Cria mapa de comandas para consulta rápida
|
||||
const cmdMap = (comandas || []).reduce((acc, c) => {
|
||||
acc[String(c.id)] = `${c.name || 'Sem nome'} (${c.mesa_name || `Mesa ${c.mesa}`})`;
|
||||
_cmdMap = (comandas || []).reduce((acc, c) => {
|
||||
acc[String(c.id)] = {
|
||||
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;
|
||||
}, {});
|
||||
|
||||
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');
|
||||
if (!wrap) return;
|
||||
|
||||
if (!data.length) {
|
||||
if (!_pagsData.length) {
|
||||
wrap.innerHTML = `<div class="table-empty">Nenhum pagamento registrado.</div>`;
|
||||
return;
|
||||
}
|
||||
|
||||
// Soma total dos pagamentos exibidos
|
||||
const total = data.reduce((acc, p) => acc + parseFloat(p.value || 0), 0);
|
||||
const total = _pagsData.reduce((acc, p) => acc + parseFloat(p.value || 0), 0);
|
||||
|
||||
wrap.innerHTML = `
|
||||
<table>
|
||||
@@ -81,12 +99,14 @@ function renderPagsTable(data, cmdMap = {}) {
|
||||
<th>Ações</th>
|
||||
</tr></thead>
|
||||
<tbody>
|
||||
${data.map(p => {
|
||||
const cDesc = cmdMap[String(p.comanda)] || p.comanda_name || '–';
|
||||
${_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>${p.client_name || '–'}</td>
|
||||
<td>${clienteNome}</td>
|
||||
<td>
|
||||
${p.comanda ? `<span style="font-size:0.8rem">
|
||||
<span style="color:var(--text-muted)">#${p.comanda}</span>
|
||||
@@ -100,7 +120,7 @@ function renderPagsTable(data, cmdMap = {}) {
|
||||
</td>
|
||||
<td style="white-space:nowrap;font-size:0.82rem">${formatDate(p.datetime)}</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>
|
||||
</tr>`;
|
||||
}).join('')}
|
||||
@@ -111,29 +131,161 @@ function renderPagsTable(data, cmdMap = {}) {
|
||||
<strong style="color:var(--success);font-size:1rem">R$ ${total.toFixed(2)}</strong>
|
||||
</div>`;
|
||||
|
||||
wrap.querySelectorAll('.btn-del-pag').forEach(btn =>
|
||||
btn.addEventListener('click', async () => {
|
||||
const r = await window.electronAPI.delete(`/payments/${btn.dataset.id}`);
|
||||
if (r.ok) { showToast('Pagamento excluído!', 'success'); loadPagamentos([], []); }
|
||||
else showToast(r.error, 'error');
|
||||
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 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 tipo = parseInt(document.getElementById('filter-tipo')?.value) || null;
|
||||
|
||||
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 ||
|
||||
(p.client_name || '').toLowerCase().includes(q) ||
|
||||
(p.comanda_name || '').toLowerCase().includes(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;
|
||||
});
|
||||
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) {
|
||||
@@ -185,7 +337,7 @@ function abrirModalPagamento(tiposPag, comandas) {
|
||||
};
|
||||
|
||||
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');
|
||||
});
|
||||
}
|
||||
|
||||
@@ -54,6 +54,7 @@ let _ordersData = [];
|
||||
let _productsMap = {};
|
||||
let _comandasMap = {};
|
||||
let _mesasMap = {};
|
||||
let _productsFullMap = {};
|
||||
|
||||
async function loadOrders() {
|
||||
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; }
|
||||
|
||||
// Constrói mapas para consulta rápida (IDs como string para segurança)
|
||||
if (pRes.ok) _productsMap = pRes.data.reduce((acc, p) => { acc[String(p.id)] = p.name; return acc; }, {});
|
||||
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; }, {});
|
||||
}
|
||||
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: _mesasMap[String(c.mesa)] || `Mesa ${c.mesa}` || '–',
|
||||
mesa_name: c.mesa_name || _mesasMap[String(c.mesa)] || '–'
|
||||
};
|
||||
return acc;
|
||||
}, {});
|
||||
@@ -132,6 +137,7 @@ function renderOrdersTable(data) {
|
||||
</td>
|
||||
<td>
|
||||
<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>` : ''}
|
||||
${o.status !== 'Cancelado' && o.status !== 'Entregue'
|
||||
? `<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
|
||||
wrap.querySelectorAll('.btn-edit-obs').forEach(btn =>
|
||||
btn.addEventListener('click', () => {
|
||||
|
||||
@@ -146,10 +146,21 @@ function filtrarProdutos(categorias, unidades) {
|
||||
|
||||
function abrirModalProduto(produto, categorias, unidades) {
|
||||
const isEdit = !!produto;
|
||||
const imagemAtual = produto?.image || '';
|
||||
|
||||
openModal({
|
||||
title: isEdit ? `Editar: ${produto.name}` : 'Novo Produto',
|
||||
body: `
|
||||
<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">
|
||||
<label>Nome</label>
|
||||
<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>
|
||||
<input type="text" id="prod-desc" class="form-control" value="${produto?.description || ''}" placeholder="Descrição opcional" />
|
||||
</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>`,
|
||||
footer: `
|
||||
<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>`,
|
||||
});
|
||||
|
||||
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 () => {
|
||||
const btn = document.getElementById('btn-salvar-prod');
|
||||
btn.disabled = true;
|
||||
@@ -212,7 +250,6 @@ function abrirModalProduto(produto, categorias, unidades) {
|
||||
const catVal = parseInt(document.getElementById('prod-cat').value);
|
||||
const unitVal = parseInt(document.getElementById('prod-unit').value);
|
||||
|
||||
// Constrói o payload enviando apenas o que é necessário/preenchido
|
||||
const data = {
|
||||
name: document.getElementById('prod-nome').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,
|
||||
active: document.getElementById('prod-ativo').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 (unitVal) data.unit_of_measure = unitVal;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user