diff --git a/src/main/main.js b/src/main/main.js index 8736fbf..c3dc061 100644 --- a/src/main/main.js +++ b/src/main/main.js @@ -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 }); + } } }); }); diff --git a/src/renderer/pages/clientes.js b/src/renderer/pages/clientes.js index 453b30a..992640c 100644 --- a/src/renderer/pages/clientes.js +++ b/src/renderer/pages/clientes.js @@ -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 = `
`; - // 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 = `
Erro ao carregar clientes.
`; 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 ` -
+
- +
Comanda #${f.id} — ${f.name || 'Sem nome'}
Abertura: ${formatDate(f.dt_open)}
+ ${temPagamentos ? `
Pago: R$ ${totalPago.toFixed(2)}
` : ''}
+ ${temPagamentos ? `
R$ ${totalComanda.toFixed(2)}
` : ''}
${f.status}
-
R$ ${totalComanda.toFixed(2)}
+
R$ ${valorRestante.toFixed(2)}
@@ -333,6 +350,24 @@ async function abrirHistoricoFiados(cliente) { `}).join('')} + ${temPagamentos ? ` +
+ + Ver Pagamentos (${pagamentos.length}) + +
    + ${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 ` +
  • + 💰 ${tipoNome} + R$ ${parseFloat(p.value || 0).toFixed(2)} +
  • + `}).join('')} +
+
+ ` : ''}
`; @@ -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 => { diff --git a/src/renderer/pages/comandas.js b/src/renderer/pages/comandas.js index 0507972..833beeb 100644 --- a/src/renderer/pages/comandas.js +++ b/src/renderer/pages/comandas.js @@ -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 = ` + + + + + Ticket Cozinha - ${nomeComanda} + + + +
+
+
🍳 COZINHA
+
${nomeEstabelecimento}
+
+ +
+ ${nomeComanda} +
+
+ Mesa: ${nomeMesa} + ${dataAtual} +
+ +
+ ${nomeProduto} +
+ + ${observacao ? `
OBS: ${observacao}
` : ''} + + +
+ + + `; + + 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 = ` `, footer: ` - + `, }); @@ -531,7 +630,7 @@ async function abrirItensComanda(comandaIdOrObj) { Produto Preço - ${podeAdd ? 'Ação' : ''} + Ações ${itens.map(it => { @@ -542,24 +641,29 @@ async function abrirItensComanda(comandaIdOrObj) { return ` - ${prod?.name || it.product_name || `Produto #${it.product}`} + ${isCuisine ? '🍳 ' : ''}${prod?.name || it.product_name || `Produto #${it.product}`} + ${it.obs ? `
OBS: ${it.obs}` : ''} R$ ${(_productsMap[String(it.product)] || 0).toFixed(2)} - ${podeAdd ? `
${isCuisine ? ` - + ` : ''} - + ${podeAdd ? ` + + ` : ''}
- ` : ''} + `; }).join('')} @@ -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 `
diff --git a/src/renderer/pages/pagamentos.js b/src/renderer/pages/pagamentos.js index 025bfdd..e6d5250 100644 --- a/src/renderer/pages/pagamentos.js +++ b/src/renderer/pages/pagamentos.js @@ -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 = ` `; - 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 = `
`; + + _tiposPag = tiposPag; + const res = await window.electronAPI.get('/payments'); if (!res.ok) { wrap.innerHTML = `
Erro ao carregar pagamentos.
`; 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 = `
Nenhum pagamento registrado.
`; 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 = ` @@ -81,12 +99,14 @@ function renderPagsTable(data, cmdMap = {}) { - ${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 ` - + `; }).join('')} @@ -111,29 +131,161 @@ function renderPagsTable(data, cmdMap = {}) { R$ ${total.toFixed(2)}`; - 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: ` +
+
+
+
+
Valor
+
R$ ${parseFloat(pagamento.value || 0).toFixed(2)}
+
+
+
Forma de Pagamento
+
${tipoNome}
+
+
+
+ +
+
+
Cliente
+
${clienteNome}
+
+
+
Data/Hora
+
${formatDate(pagamento.datetime)}
+
+
+ + ${pagamento.comanda ? ` +
+
Comanda
+
+
+
+
#${pagamento.comanda} — ${cInfo?.name || '–'}
+
${cInfo?.mesa || '–'}
+
+ + ${cInfo?.status || '–'} + +
+ ${cInfo?.dt_open ? ` +
+ Abertura: ${formatDate(cInfo.dt_open)} +
+ ` : ''} +
+
+ ` : ''} + + ${pagamento.description ? ` +
+
Descrição
+
${pagamento.description}
+
+ ` : ''} +
+ `, + footer: `` + }); +} + +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 = `
Nenhum pagamento encontrado.
`; + return; + } + + const total = filtered.reduce((acc, p) => acc + parseFloat(p.value || 0), 0); + + wrap.innerHTML = ` +
Ações
#${p.id}${p.client_name || '–'}${clienteNome} ${p.comanda ? ` #${p.comanda} @@ -100,7 +120,7 @@ function renderPagsTable(data, cmdMap = {}) { ${formatDate(p.datetime)} - +
+ + + + + + + + + + + + ${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 ` + + + + + + + + + + `; + }).join('')} + +
#ClienteComandaTipoValorDescriçãoDataAções
#${p.id}${clienteNome} + ${p.comanda ? ` + #${p.comanda} + ${cDesc} + ` : '–'} + ${p.type_pay_name || '–'}R$ ${parseFloat(p.value || 0).toFixed(2)} + ${p.description || '–'} + ${formatDate(p.datetime)} + +
+
+ Total exibido: + R$ ${total.toFixed(2)} +
`; + + 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'); }); } diff --git a/src/renderer/pages/pedidos.js b/src/renderer/pages/pedidos.js index 239a996..331d56d 100644 --- a/src/renderer/pages/pedidos.js +++ b/src/renderer/pages/pedidos.js @@ -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 = `
Erro ao carregar pedidos.
`; 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) {
+ ${cfg.next ? `` : ''} ${o.status !== 'Cancelado' && o.status !== 'Entregue' ? `` @@ -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 = ` + + + + + Ticket Cozinha - ${nomeComanda} + + + +
+
+
🍳 COZINHA - REIMPRESSÃO
+
${nomeEstabelecimento}
+
+ +
+ ${nomeComanda} +
+
+ Mesa: ${nomeMesa} + ${dataAtual} +
+ +
${nomeProduto}
+ + ${obs ? `
OBS: ${obs}
` : ''} + + +
+ + + `; + + 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', () => { diff --git a/src/renderer/pages/produtos.js b/src/renderer/pages/produtos.js index 85435ae..e58b3da 100644 --- a/src/renderer/pages/produtos.js +++ b/src/renderer/pages/produtos.js @@ -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: `
+
+
+ ${imagemAtual ? `` : '📷'} +
+ + + ${imagemAtual ? '' : ''} + +
@@ -194,16 +205,43 @@ function abrirModalProduto(produto, categorias, unidades) {
-
- - -
`, footer: ` `, }); + 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 = ``; + }; + reader.readAsDataURL(file); + }); + + const btnRemoveImg = document.getElementById('btn-remove-img'); + if (btnRemoveImg) { + btnRemoveImg.addEventListener('click', () => { + imgInput.value = ''; + previewEl.innerHTML = '📷'; + }); + } + 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;