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 = `
+
+
+
+
+
+
+
`;
+
+ 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}
+
+
+
+
+
+
+
+ ${_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;
+}