impressão da comanda

This commit is contained in:
2026-08-03 13:58:46 -03:00
parent b567a45803
commit 8a28ab33ba
2 changed files with 144 additions and 3 deletions

128
web/src/receipt.ts Normal file
View File

@@ -0,0 +1,128 @@
import { formatBRL } from './utils';
function escapeHtml(s: string): string {
return s.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
}
function calcularTotais(c: any) {
const subtotal = c.total || 0;
const desconto = c.desconto || 0;
const acrescimo = c.acrescimo || 0;
const taxaServico = c.taxaServico ? (subtotal - desconto + acrescimo) * 0.1 : 0;
const totalFinal = Math.max(subtotal - desconto + acrescimo + taxaServico, 0);
const valorPago = c.valorPago || 0;
const restante = Math.max(totalFinal - valorPago, 0);
return { subtotal, desconto, acrescimo, taxaServico, totalFinal, valorPago, restante };
}
export function printConta(c: any) {
const itens = c.itens ?? [];
const { subtotal, desconto, acrescimo, taxaServico, totalFinal, valorPago, restante } = calcularTotais(c);
const data = new Date(c.criadoEm).toLocaleString('pt-BR');
const agora = new Date().toLocaleString('pt-BR');
const itemRows = itens.map((i: any) => {
const nome = escapeHtml(i.produto?.nome ?? 'Item');
const sub = formatBRL(i.precoUnitario * i.quantidade);
return `<div class="row"><span>${i.quantidade}x ${nome}</span><span>${sub}</span></div>`;
}).join('');
const obsRows = itens
.filter((i: any) => i.observacao)
.map((i: any) => `<div class="obs">(${escapeHtml(i.observacao)})</div>`)
.join('');
const totalsRows = `
<div class="row"><span>Subtotal</span><span>${formatBRL(subtotal)}</span></div>
${desconto > 0 ? `<div class="row"><span>Desconto</span><span>-${formatBRL(desconto)}</span></div>` : ''}
${acrescimo > 0 ? `<div class="row"><span>Acrescimo</span><span>+${formatBRL(acrescimo)}</span></div>` : ''}
${c.taxaServico ? `<div class="row"><span>Taxa de Servico (10%)</span><span>+${formatBRL(taxaServico)}</span></div>` : ''}
`;
const html = `<!DOCTYPE html>
<html lang="pt-BR">
<head>
<meta charset="UTF-8">
<title>Conta - ${escapeHtml(c.identificador)}</title>
<style>
@page { size: 58mm auto; margin: 0; }
html, body { margin: 0; padding: 0; }
body {
font-family: 'Courier New', Courier, monospace;
font-size: 11px;
line-height: 1.4;
color: #000;
background: #fff;
width: 48mm;
margin: 0 auto;
padding: 2mm 2mm 4mm;
}
.center { text-align: center; }
.logo { font-weight: 700; font-size: 13px; letter-spacing: 0.5px; }
.sub { font-size: 9px; }
.hr { border-top: 1px dashed #000; margin: 2mm 0; }
.row { display: flex; justify-content: space-between; gap: 2mm; }
.row > span:first-child { flex: 1; }
.obs { font-size: 9px; margin: 0 0 1mm 4mm; }
.items-title { font-weight: 700; margin-bottom: 1mm; }
.total-box { border-top: 1px dashed #000; border-bottom: 1px dashed #000; margin: 2mm 0; padding: 1.5mm 0; }
.total { font-weight: 700; font-size: 14px; }
.stamp { font-weight: 700; font-size: 12px; }
.footer { margin-top: 3mm; }
</style>
</head>
<body>
<div class="center logo">CAFE EM SATURNO</div>
<div class="center sub">Gastrobar - Conta de Consumo</div>
<div class="hr"></div>
<div class="row"><span>Comanda</span><span>${escapeHtml(c.identificador)}</span></div>
${c.nomeCliente ? `<div class="row"><span>Cliente</span><span>${escapeHtml(c.nomeCliente)}</span></div>` : ''}
<div class="row"><span>Data</span><span>${data}</span></div>
${c.usuario?.nome ? `<div class="row"><span>Operador</span><span>${escapeHtml(c.usuario.nome)}</span></div>` : ''}
<div class="hr"></div>
<div class="items-title">ITENS CONSUMIDOS</div>
${itemRows || '<div class="row"><span>Sem itens</span></div>'}
${obsRows}
<div class="hr"></div>
${totalsRows}
<div class="total-box">
<div class="row"><span>TOTAL</span><span>${formatBRL(totalFinal)}</span></div>
${valorPago > 0 ? `<div class="row"><span>Pago</span><span>-${formatBRL(valorPago)}</span></div>` : ''}
</div>
${restante > 0
? `<div class="center total">A PAGAR</div>
<div class="center total">${formatBRL(restante)}</div>`
: `<div class="center stamp">PAGO</div>`}
<div class="footer center">Obrigado pela preferencia!</div>
<div class="center sub">Impresso em ${agora}</div>
</body>
</html>`;
let iframe = document.getElementById('print-iframe') as HTMLIFrameElement | null;
if (!iframe) {
iframe = document.createElement('iframe');
iframe.id = 'print-iframe';
iframe.style.cssText = 'position:absolute;width:0;height:0;border:0;visibility:hidden;';
document.body.appendChild(iframe);
}
iframe.onload = () => {
const win = iframe?.contentWindow;
if (win) {
win.focus();
win.print();
}
};
const doc = iframe.contentDocument;
if (!doc) return;
doc.open();
doc.write(html);
doc.close();
}

View File

@@ -1,5 +1,6 @@
import { api } from '../api'; import { api } from '../api';
import { renderNavbar, toast, formatBRL, getUser, statusBadge, parseDisplayDate } from '../utils'; import { renderNavbar, toast, formatBRL, getUser, statusBadge, parseDisplayDate } from '../utils';
import { printConta } from '../receipt';
let localComandas: any[] = []; let localComandas: any[] = [];
@@ -766,9 +767,15 @@ function bindOrderEvents() {
const acoesContainer = document.getElementById('detalhes-acoes-pagamento')!; const acoesContainer = document.getElementById('detalhes-acoes-pagamento')!;
acoesContainer.innerHTML = ''; acoesContainer.innerHTML = '';
const canPay = user && ['ADMIN', 'CAIXA'].includes(user.role); const canPay = user && ['ADMIN', 'CAIXA'].includes(user.role);
const printBtnHtml = `
<button type="button" class="btn btn-secondary" style="flex:1;" id="btn-imprimir-conta">
🖨️ Imprimir Conta
</button>
`;
if (canPay && c.status !== 'PAGA' && c.status !== 'CANCELADA' && restante > 0) { if (canPay && c.status !== 'PAGA' && c.status !== 'CANCELADA' && restante > 0) {
acoesContainer.innerHTML = ` acoesContainer.innerHTML = printBtnHtml + `
<button type="button" class="btn btn-success" style="flex:1;" id="btn-pagamento-parcial"> <button type="button" class="btn btn-success" style="flex:1;" id="btn-pagamento-parcial">
💰 Pagamento Parcial 💰 Pagamento Parcial
</button> </button>
@@ -796,9 +803,15 @@ function bindOrderEvents() {
} }
}); });
} else if (canPay && c.status === 'PAGA') { } else if (canPay && c.status === 'PAGA') {
acoesContainer.innerHTML = `<p style="text-align:center;color:var(--success);width:100%;font-weight:600;">✅ Comanda Paga</p>`; acoesContainer.innerHTML = printBtnHtml + `<p style="text-align:center;color:var(--success);width:100%;font-weight:600;">✅ Comanda Paga</p>`;
} else {
acoesContainer.innerHTML = printBtnHtml;
} }
document.getElementById('btn-imprimir-conta')?.addEventListener('click', () => {
printConta(c);
});
(document.getElementById('modal-detalhes-comanda') as HTMLElement).style.display = 'flex'; (document.getElementById('modal-detalhes-comanda') as HTMLElement).style.display = 'flex';
document.querySelector('.detalhes-edit-section')?.classList.remove('expanded'); document.querySelector('.detalhes-edit-section')?.classList.remove('expanded');
}; };