diff --git a/src/renderer/app.js b/src/renderer/app.js index 0036033..a56247f 100644 --- a/src/renderer/app.js +++ b/src/renderer/app.js @@ -11,12 +11,14 @@ import { renderClientes } from './pages/clientes.js'; import { renderPagamentos } from './pages/pagamentos.js'; import { renderLogin } from './pages/login.js'; import { renderConfig } from './pages/config.js'; +import { renderPdv } from './pages/pdv.js'; // ─── Roteador ──────────────────────────────────────────────────────────────── const PAGES = { dashboard: renderDashboard, mesas: renderMesas, comandas: renderComandas, + pdv: renderPdv, pedidos: renderPedidos, produtos: renderProdutos, clientes: renderClientes, diff --git a/src/renderer/index.html b/src/renderer/index.html index 86594f5..0118914 100644 --- a/src/renderer/index.html +++ b/src/renderer/index.html @@ -74,6 +74,9 @@ 📋Comandas + + 🖨️PDV Fichas + 🛒Pedidos diff --git a/src/renderer/pages/pdv.js b/src/renderer/pages/pdv.js new file mode 100644 index 0000000..3a13f6d --- /dev/null +++ b/src/renderer/pages/pdv.js @@ -0,0 +1,587 @@ +let _productsMap = {}; +let _productsNames = {}; +let _paymentTypes = []; +let _clients = []; +let _mesas = []; +let _pdvComanda = null; +let _pdvItems = []; +let _loggedUser = null; + +export async function renderPdv(container) { + container.innerHTML = ` + +
+
+
+
+
+
+ +
+
+
+
+ `; + + _loggedUser = await window.electronAPI.getUser(); + await criarComandaPdv(); + await loadPdvData(); + + setTimeout(() => { + const btnLimpar = document.getElementById('btn-pdv-limpar'); + const btnImprimir = document.getElementById('btn-pdv-imprimir-fichas'); + const btnPagamento = document.getElementById('btn-pdv-pagamento'); + const searchInput = document.getElementById('pdv-search'); + + if (btnLimpar) { + btnLimpar.onclick = async () => { + if (!_pdvItems.length) return; + if (confirm('Deseja limpar todos os itens do PDV?')) { + await limparPdv(); + } + }; + } + + if (btnImprimir) { + btnImprimir.onclick = () => { + imprimirFichasSelecionadas(); + }; + } + + if (btnPagamento) { + btnPagamento.onclick = () => { + abrirModalPagamentoPdv(); + }; + } + + if (searchInput) { + searchInput.oninput = async (e) => { + const pRes = await window.electronAPI.get('/products'); + const todosProdutos = pRes.ok ? pRes.data.filter(p => p.active) : []; + renderRight(todosProdutos, e.target.value); + }; + } + }, 150); +} + +async function criarComandaPdv() { + const mesasRes = await window.electronAPI.get('/mesas'); + if (mesasRes.ok) { + _mesas = mesasRes.data; + } + + const comandasRes = await window.electronAPI.get('/comandas'); + if (comandasRes.ok) { + const existing = comandasRes.data.find(c => c.name === 'PDV-BALCAO' && c.status === 'OPEN'); + if (existing) { + _pdvComanda = existing; + console.log('[PDV] ComandaPDV encontrada:', _pdvComanda); + await recarregarComanda(); + return; + } + } + + const now = new Date(); + const dataStr = now.toLocaleDateString('pt-BR', { day: '2-digit', month: '2-digit' }); + const horaStr = now.toLocaleTimeString('pt-BR', { hour: '2-digit', minute: '2-digit' }).replace(':', ''); + const userId = _loggedUser?.id || 1; + + const nomeComanda = 'PDV-BALCAO'; + + const mesaId = _mesas[0]?.id || 1; + + const r = await window.electronAPI.post('/comandas', { + user: userId, + mesa: mesaId, + name: nomeComanda, + status: 'OPEN' + }); + + if (r.ok) { + _pdvComanda = r.data; + console.log('[PDV] ComandaPDV criada:', _pdvComanda); + } else { + console.error('[PDV] Erro ao criar comanda:', r.error); + } +} + +async function loadPdvData() { + const [pRes, ptRes, cRes] = await Promise.all([ + window.electronAPI.get('/products'), + window.electronAPI.get('/payment-types'), + window.electronAPI.get('/clients') + ]); + + if (pRes.ok) { + _productsMap = pRes.data.reduce((acc, p) => { + acc[String(p.id)] = parseFloat(p.price || 0); + return acc; + }, {}); + _productsNames = pRes.data.reduce((acc, p) => { + acc[String(p.id)] = p.name || `Produto #${p.id}`; + return acc; + }, {}); + } + + if (ptRes.ok) _paymentTypes = ptRes.data; + if (cRes.ok) _clients = cRes.data; + + const todosProdutos = pRes.ok ? pRes.data.filter(p => p.active) : []; + renderRight(todosProdutos); + renderLeft(); +} + +function renderLeft() { + const container = document.getElementById('pdv-ficha-items'); + if (!container) return; + + if (!_pdvComanda || !_pdvComanda.items || _pdvComanda.items.length === 0) { + container.innerHTML = ` +
+
📋
+
Nenhum item adicionado
+
Clique nos produtos para adicionar
+
`; + updateFooterCounts(); + return; + } + + _pdvItems = _pdvComanda.items || []; + + container.innerHTML = ` +
+
+ + +
+ + + + + + + + + + + ${_pdvItems.map((it, index) => { + const price = _productsMap[String(it.product)] || 0; + const name = _productsNames[String(it.product)] || it.product_name || `Produto #${it.product}`; + return ` + + + + + + `; + }).join('')} + +
ProdutoPreçoAções
+ + + ${it.product_name || name} + ${it.obs ? `
OBS: ${it.obs}` : ''} +
+ R$ ${price.toFixed(2)} + + +
+
`; + + container.querySelectorAll('.item-checkbox').forEach(cb => { + cb.addEventListener('change', () => { + const idx = parseInt(cb.dataset.index); + if (!_pdvItems[idx]) return; + _pdvItems[idx].selected = cb.checked; + updateFooterCounts(); + }); + }); + + container.querySelector('#check-all')?.addEventListener('change', (e) => { + const checked = e.target.checked; + _pdvItems.forEach(it => it.selected = checked); + container.querySelectorAll('.item-checkbox').forEach(cb => cb.checked = checked); + updateFooterCounts(); + }); + + container.querySelectorAll('.btn-remove-item').forEach(btn => { + btn.addEventListener('click', async () => { + const itemId = btn.dataset.id; + if (confirm('Deseja realmente excluir este item?')) { + const r = await window.electronAPI.delete(`/items-comanda/${itemId}`); + if (r.ok) { + showToast('Item excluído!', 'success'); + await recarregarComanda(); + } else { + showToast(r.error, 'error'); + } + } + }); + }); + + updateFooterCounts(); +} + +async function recarregarComanda() { + if (!_pdvComanda) return; + const r = await window.electronAPI.get(`/comandas/${_pdvComanda.id}`); + if (r.ok) { + _pdvComanda = r.data; + renderLeft(); + } +} + +function updateFooterCounts() { + const totalAll = (_pdvItems || []).reduce((acc, it) => acc + (_productsMap[String(it.product)] || 0), 0); + const selected = (_pdvItems || []).filter(it => it.selected !== false); + const totalSelected = selected.reduce((acc, it) => acc + (_productsMap[String(it.product)] || 0), 0); + + const totalAllEl = document.getElementById('pdv-total-all'); + const totalSelectedEl = document.getElementById('pdv-total-selected'); + const printBtn = document.getElementById('btn-pdv-imprimir-fichas'); + + if (totalAllEl) totalAllEl.textContent = `R$ ${totalAll.toFixed(2)}`; + if (totalSelectedEl) totalSelectedEl.textContent = `R$ ${totalSelected.toFixed(2)}`; + if (printBtn) { + printBtn.disabled = selected.length === 0; + printBtn.innerHTML = selected.length > 0 + ? `🖨️ Imprimir ${selected.length} Ficha${selected.length > 1 ? 's' : ''}` + : '🖨️ Imprimir Fichas'; + } +} + +function renderRight(todosProdutos, filtro = '') { + const container = document.getElementById('pdv-products-grid'); + if (!container) return; + + const filtrados = todosProdutos.filter(p => !filtro || p.name.toLowerCase().includes(filtro.toLowerCase())).slice(0, 20); + + container.innerHTML = filtrados.map(p => { + const imgUrl = p.image + ? (p.image.startsWith('data:') || p.image.startsWith('http') + ? p.image + : `http://localhost:8080${p.image}`) + : 'https://wallpapers.com/images/featured/fundo-abstrato-escuro-27kvn4ewpldsngbu.jpg'; + + return ` +
+
+
+
${p.cuisine ? '🍳 ' : ''}${p.name}
+
R$ ${parseFloat(p.price || 0).toFixed(2)}
+
+
`; + }).join(''); + + container.querySelectorAll('.pdv-product-card').forEach(card => { + card.addEventListener('click', async () => { + const pId = String(card.dataset.id); + const prod = todosProdutos.find(x => String(x.id) === pId); + + card.style.transform = 'scale(0.95)'; + setTimeout(() => card.style.transform = '', 100); + + if (!prod) return showToast('Erro: Produto não encontrado.', 'error'); + if (!_pdvComanda) return showToast('Erro: Comanda PDV não criada.', 'error'); + + if (prod.cuisine) { + window.abrirModalObsCozinhaGlobal(prod.name, '', async (obs) => { + if (obs === null) return; + const loggedUser = await window.electronAPI.getUser(); + const r = await window.electronAPI.post('/items-comanda', { + comanda: _pdvComanda.id, + product: prod.id, + obs: obs, + applicant: loggedUser?.username || 'Sistema' + }); + await processarResultadoAdd(r, prod, obs, loggedUser); + }); + } else { + const loggedUser = await window.electronAPI.getUser(); + const r = await window.electronAPI.post('/items-comanda', { + comanda: _pdvComanda.id, + product: prod.id, + applicant: loggedUser?.username || 'Sistema' + }); + await processarResultadoAdd(r, prod); + } + }); + }); +} + +async function processarResultadoAdd(r, prod, obs = '', loggedUser = null) { + if (r.ok) { + await recarregarComanda(); + + if (prod && prod.cuisine) { + const orderPayload = { + productComanda: r.data.id, + id_product: prod.id, + id_comanda: _pdvComanda.id, + obs: obs || r.data.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'); + } else { + showToast('Item adicionado, mas falhou ao enviar para cozinha.', 'warning'); + } + } + } else { + showToast(r.error, 'error'); + } +} + +async function imprimirFichasSelecionadas() { + const selected = _pdvItems.filter(it => it.selected !== false); + if (!selected.length) return showToast('Nenhum item selecionado para imprimir.', 'warning'); + + const loggedUser = await window.electronAPI.getUser(); + + for (const item of selected) { + const prod = { name: _productsNames[String(item.product)] || item.product_name }; + const htmlTicket = gerarHtmlTicket(item, prod, loggedUser); + + window.electronAPI.printDirect(htmlTicket).then(r => { + if (r.ok) { + // OK + } else if (r.error === 'NO_PRINTER') { + showToast('Nenhuma impressora configurada. Abra as configuracoes 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); + } + }); + + await new Promise(r => setTimeout(r, 200)); + } + + showToast(`${selected.length} ficha${selected.length > 1 ? 's' : ''} enviada(s) para impressao!`, 'success'); +} + +function gerarHtmlTicket(item, product, loggedUser) { + const dataAtual = new Date().toLocaleString('pt-BR'); + const nomeEstabelecimento = 'Raul Rock Bar & Café'; + const nomeProduto = product?.name || item.product_name || `Produto #${item.product}`; + const observacao = item.obs || ''; + + const htmlTicket = ` + + + + + Ticket PDV - ${nomeProduto} + + + +
+
+
${nomeEstabelecimento}
+
+ +
+ ${_pdvComanda?.name || 'PDV'} +
+
+ PDV + ${dataAtual} +
+ +
+ ${nomeProduto} +
+ + ${observacao ? `
OBS: ${observacao}
` : ''} + + +
+
+
+ + + `; + + return htmlTicket; +} + +function calcularTotal() { + return _pdvItems.reduce((acc, it) => { + if (it.selected !== false) { + return acc + (_productsMap[String(it.product)] || 0); + } + return acc; + }, 0); +} + +function abrirModalPagamentoPdv() { + if (!_pdvComanda || !_pdvItems.length) return showToast('Adicione itens primeiro.', 'warning'); + + const total = calcularTotal(); + const valorRestante = total; + + openModal({ + title: `💰 Pagamento - ${_pdvComanda.name}`, + body: ` +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
`, + footer: ` + + ` + }); + + const btnConfirmar = document.getElementById('btn-confirmar-pagamento'); + + btnConfirmar.addEventListener('click', async () => { + const payTypeId = parseInt(document.getElementById('pay-type').value); + const clientId = document.getElementById('pay-client').value || null; + const valor = parseFloat(document.getElementById('pay-value').value); + + if (isNaN(valor) || valor <= 0) { + return showToast('Informe um valor válido.', 'error'); + } + + try { + btnConfirmar.disabled = true; + btnConfirmar.textContent = 'Processando...'; + + const rPay = await window.electronAPI.post(`/comandas/${_pdvComanda.id}/pagar`, { + value: valor, + type_pay: payTypeId, + client: clientId ? parseInt(clientId) : null, + description: document.getElementById('pay-desc').value || 'Pagamento PDV', + status: 'CLOSED' + }); + + if (rPay.ok) { + showToast('Pagamento realizado com sucesso!', 'success'); + closeModal(); + + await window.electronAPI.patch(`/comandas/${_pdvComanda.id}`, { + status: 'CLOSED' + }); + + _pdvItems = []; + _pdvComanda = null; + await criarComandaPdv(); + renderLeft(); + updateFooterCounts(); + } else { + throw new Error(rPay.error || 'Erro ao registrar pagamento.'); + } + } catch (err) { + showToast(err.message, 'error'); + btnConfirmar.disabled = false; + btnConfirmar.textContent = 'Confirmar Recebimento'; + } + }); +} + +async function excluirComandaPdv() { + if (!_pdvComanda) return; + + const itensToDelete = _pdvComanda.items || []; + for (const item of itensToDelete) { + await window.electronAPI.delete(`/items-comanda/${item.id}`); + } + + _pdvItems = []; + await recarregarComanda(); + showToast('PDV limpo!', 'info'); +} + +async function limparPdv() { + if (!_pdvComanda || !_pdvComanda.items?.length) { + _pdvItems = []; + renderLeft(); + return; + } + + for (const item of _pdvComanda.items) { + await window.electronAPI.delete(`/items-comanda/${item.id}`); + } + + _pdvItems = []; + await recarregarComanda(); + showToast('PDV limpo!', 'info'); +} \ No newline at end of file diff --git a/src/renderer/style.css b/src/renderer/style.css index 0444afb..c389883 100644 --- a/src/renderer/style.css +++ b/src/renderer/style.css @@ -756,7 +756,7 @@ select.form-control option { .pdv-container { display: grid; grid-template-columns: 480px 1fr; - height: 100%; + height: calc(100vh - 180px); gap: 1px; background: var(--border); } @@ -1056,3 +1056,24 @@ select.form-control option { font-size: 10px; } } + +/* PDV Footer Bar */ +.pdv-footer-bar { + display: flex; + align-items: center; + justify-content: space-between; + padding: 16px 24px; + background: var(--bg-surface); + border-top: 1px solid var(--border); +} + +.pdv-footer-info { + display: flex; + gap: 16px; + font-size: 0.9rem; +} + +.pdv-footer-actions { + display: flex; + gap: 12px; +}