Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 60e15dbed3 | |||
| 53767df80c |
Binary file not shown.
Binary file not shown.
Executable
BIN
Binary file not shown.
@@ -122,7 +122,6 @@ impl ReciboData {
|
|||||||
|
|
||||||
// Corta papel
|
// Corta papel
|
||||||
out.extend(escpos::FEED_CUT);
|
out.extend(escpos::FEED_CUT);
|
||||||
out.extend(escpos::CUT);
|
|
||||||
|
|
||||||
out
|
out
|
||||||
}
|
}
|
||||||
|
|||||||
+177
-16
@@ -1191,6 +1191,138 @@ async function tauriEnviar() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ─── Print Preview Window ──────────────────────────────────────────────────
|
||||||
|
function buildPrintHTML(data) {
|
||||||
|
const fmt = v => 'R$ ' + (v || 0).toFixed(2).replace('.', ',');
|
||||||
|
const diff = (data.saldo_troco - data.saldo_esperado);
|
||||||
|
const diffStr = diff >= 0 ? `+${fmt(diff)}` : fmt(diff);
|
||||||
|
const diffColor = diff === 0 ? '#2e7d32' : (diff > 0 ? '#e65100' : '#c0272d');
|
||||||
|
|
||||||
|
let rows = '';
|
||||||
|
// Sangrias
|
||||||
|
if (data.sangrias && data.sangrias.length > 0) {
|
||||||
|
data.sangrias.forEach(s => { if (s.hora || s.retirada) rows += `<tr><td>${s.hora||''}</td><td>${fmt(s.retirada)}</td><td>${s.gerente||''}</td></tr>`; });
|
||||||
|
}
|
||||||
|
// Despesas
|
||||||
|
if (data.despesas && data.despesas.length > 0) {
|
||||||
|
data.despesas.forEach(d => { if (d.desc || d.valor) rows += `<tr><td>${d.desc||''}</td><td>${fmt(d.valor)}</td><td>${d.obs||''}</td></tr>`; });
|
||||||
|
}
|
||||||
|
// Vales
|
||||||
|
if (data.vales && data.vales.length > 0) {
|
||||||
|
data.vales.forEach(v => { if (v.nome || v.valor) rows += `<tr><td>${v.nome||''}</td><td>${fmt(v.valor)}</td><td>${v.obs||''}</td></tr>`; });
|
||||||
|
}
|
||||||
|
// Receber
|
||||||
|
if (data.receber && data.receber.length > 0) {
|
||||||
|
data.receber.forEach(r => { if (r.nome || r.valor) rows += `<tr><td>${r.nome||''}</td><td>${fmt(r.valor)}</td><td></td></tr>`; });
|
||||||
|
}
|
||||||
|
// PIX CNPJ
|
||||||
|
if (data.pixcnpj && data.pixcnpj.length > 0) {
|
||||||
|
data.pixcnpj.forEach(p => { if (p.nome || p.valor) rows += `<tr><td>PIX: ${p.nome||''}</td><td>${fmt(p.valor)}</td><td></td></tr>`; });
|
||||||
|
}
|
||||||
|
// Cancelamentos
|
||||||
|
if (data.cancelamentos && data.cancelamentos.length > 0) {
|
||||||
|
data.cancelamentos.forEach(c => { if (c.numero || c.valor) rows += `<tr><td>${c.numero||''}</td><td>${fmt(c.valor)}</td><td>${c.motivo||''}</td></tr>`; });
|
||||||
|
}
|
||||||
|
|
||||||
|
return `<!DOCTYPE html>
|
||||||
|
<html><head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>Relatório de Fechamento</title>
|
||||||
|
<style>
|
||||||
|
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||||
|
body { font-family: 'Courier New', monospace; font-size: 13px; width: 80mm; margin: 0 auto; padding: 8px; }
|
||||||
|
.header { text-align: center; margin-bottom: 8px; }
|
||||||
|
.header h1 { font-size: 18px; }
|
||||||
|
.header h2 { font-size: 14px; font-weight: normal; }
|
||||||
|
hr { border: none; border-top: 1px dashed #000; margin: 6px 0; }
|
||||||
|
.section { margin: 6px 0; }
|
||||||
|
.section-title { font-weight: bold; font-size: 14px; border-top: 1px dashed #000; padding-top: 4px; margin-top: 6px; }
|
||||||
|
.row { display: flex; justify-content: space-between; padding: 2px 0; }
|
||||||
|
.row.space-between { justify-content: space-between; }
|
||||||
|
.row.below { margin-top: 4px; }
|
||||||
|
table { width: 100%; border-collapse: collapse; font-size: 12px; }
|
||||||
|
td { padding: 2px 4px; vertical-align: top; }
|
||||||
|
td:nth-child(2) { text-align: right; }
|
||||||
|
td:nth-child(3) { text-align: right; color: #555; font-size: 11px; }
|
||||||
|
.total-row { font-weight: bold; border-top: 1px dashed #000; }
|
||||||
|
.grand-total { font-size: 16px; font-weight: bold; text-align: center; padding: 6px; border: 2px solid #000; margin: 8px 0; }
|
||||||
|
.obs { font-size: 11px; color: #555; margin-top: 6px; white-space: pre-wrap; }
|
||||||
|
.footer { text-align: center; font-size: 11px; color: #555; margin-top: 8px; }
|
||||||
|
@media print { body { margin: 0; } }
|
||||||
|
</style>
|
||||||
|
</head><body>
|
||||||
|
<div class="header">
|
||||||
|
<h1>${data.loja}</h1>
|
||||||
|
<h2>Fechamento de Caixa</h2>
|
||||||
|
</div>
|
||||||
|
<hr>
|
||||||
|
<div class="row"><span>Data:</span><span>${data.data}</span></div>
|
||||||
|
<div class="row"><span>Operador:</span><span>${data.operador}</span></div>
|
||||||
|
<div class="row"><span>Turno:</span><span>${data.turno}</span></div>
|
||||||
|
|
||||||
|
<div class="section-title">ENTRADAS</div>
|
||||||
|
<div class="row"><span>Saldo Troco:</span><span>${fmt(data.saldo_troco)}</span></div>
|
||||||
|
<div class="row"><span>Fechamento:</span><span>${fmt(data.fechamento)}</span></div>
|
||||||
|
<div class="row"><span>Saldo Esperado:</span><span>${fmt(data.saldo_esperado)}</span></div>
|
||||||
|
|
||||||
|
${rows ? `<div class="section-title">DETALHES</div><table><tbody>${rows}</tbody></table>` : ''}
|
||||||
|
|
||||||
|
${data.observacoes ? `<div class="obs">Obs: ${data.observacoes}</div>` : ''}
|
||||||
|
|
||||||
|
<hr>
|
||||||
|
<div class="section-title">CARTÕES</div>
|
||||||
|
<div class="row"><span>Crédito:</span><span>${fmt(data.credito)}</span></div>
|
||||||
|
<div class="row"><span>Débito:</span><span>${fmt(data.debito)}</span></div>
|
||||||
|
<div class="row"><span>Alimentação:</span><span>${fmt(data.alimentacao)}</span></div>
|
||||||
|
<div class="row"><span>PIX Cartão:</span><span>${fmt(data.cartoes?.pix||0)}</span></div>
|
||||||
|
<div class="row"><span>Cancelamentos:</span><span>${fmt(data.cancelamentos)}</span></div>
|
||||||
|
|
||||||
|
<hr>
|
||||||
|
<div class="grand-total">DIFERENÇA: ${diffStr}</div>
|
||||||
|
<div class="footer">Gerado em ${new Date().toLocaleString('pt-BR')}</div>
|
||||||
|
</body></html>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function openPrintPreview(data) {
|
||||||
|
// Gera HTML e injeta no body de uma div temporária para window.print()
|
||||||
|
const html = buildPrintHTML(data);
|
||||||
|
const style = document.createElement('style');
|
||||||
|
style.textContent = `
|
||||||
|
@media print {
|
||||||
|
body { width: 80mm; margin: 0 auto; font-family: 'Courier New', monospace; font-size: 13px; }
|
||||||
|
.no-print { display: none !important; }
|
||||||
|
}
|
||||||
|
.print-actions { position: fixed; top: 10px; right: 10px; z-index: 9999; }
|
||||||
|
.print-actions button { padding: 8px 16px; cursor: pointer; background: #2563eb; color: #fff; border: none; border-radius: 4px; font-size: 14px; }
|
||||||
|
.print-actions button:hover { background: #1d4ed8; }
|
||||||
|
`;
|
||||||
|
const actions = document.createElement('div');
|
||||||
|
actions.className = 'print-actions no-print';
|
||||||
|
actions.innerHTML = '<button onclick="window.print()">🖨️ Imprimir (Ctrl+P)</button>';
|
||||||
|
|
||||||
|
// Guarda conteúdo original
|
||||||
|
const originalBody = document.body.innerHTML;
|
||||||
|
|
||||||
|
document.body.innerHTML = html;
|
||||||
|
document.head.appendChild(style);
|
||||||
|
document.body.insertBefore(actions, document.body.firstChild);
|
||||||
|
|
||||||
|
// Quando fechar print dialog, restaura
|
||||||
|
const onAfterPrint = () => {
|
||||||
|
document.body.innerHTML = originalBody;
|
||||||
|
window.removeEventListener('afterprint', onAfterPrint);
|
||||||
|
// Re-carrega scripts essenciais
|
||||||
|
location.reload();
|
||||||
|
};
|
||||||
|
window.addEventListener('afterprint', onAfterPrint);
|
||||||
|
}
|
||||||
|
|
||||||
|
function triggerWindowPrint() {
|
||||||
|
// Chama window.print() na janela pai (o próprio app)
|
||||||
|
window.print();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Recibo 80mm via Tauri (ESC/POS USB) ─────────────────────────────────
|
||||||
async function tauriRecibo() {
|
async function tauriRecibo() {
|
||||||
// Build flat totals for the Rust ReciboData struct
|
// Build flat totals for the Rust ReciboData struct
|
||||||
const cartCred = estado.cartoes.credito.reduce((s,r) => s+(r.valor||0), 0);
|
const cartCred = estado.cartoes.credito.reduce((s,r) => s+(r.valor||0), 0);
|
||||||
@@ -1239,7 +1371,15 @@ async function tauriRecibo() {
|
|||||||
observacoes: estado.observacoes,
|
observacoes: estado.observacoes,
|
||||||
sync_status: estado.sync_status,
|
sync_status: estado.sync_status,
|
||||||
};
|
};
|
||||||
return await window.__TAURI__.core.invoke('imprimir_recibo', { estado: JSON.stringify(data) });
|
|
||||||
|
// Tenta ESC/POS via Tauri primeiro; se falhar, abre preview
|
||||||
|
try {
|
||||||
|
return await window.__TAURI__.core.invoke('imprimir_recibo', { estado: JSON.stringify(data) });
|
||||||
|
} catch(e) {
|
||||||
|
console.warn('ESC/POS falhou, abrindo preview:', e);
|
||||||
|
openPrintPreview(data);
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function tauriCarregarRascunho() {
|
async function tauriCarregarRascunho() {
|
||||||
@@ -1303,19 +1443,30 @@ document.getElementById('confirm-close').addEventListener('click', closeConfirmM
|
|||||||
document.getElementById('confirm-cancel').addEventListener('click', closeConfirmModal);
|
document.getElementById('confirm-cancel').addEventListener('click', closeConfirmModal);
|
||||||
document.getElementById('modal-confirm').addEventListener('click', e => { if (e.target.id === 'modal-confirm') closeConfirmModal(); });
|
document.getElementById('modal-confirm').addEventListener('click', e => { if (e.target.id === 'modal-confirm') closeConfirmModal(); });
|
||||||
|
|
||||||
// Print from confirm modal
|
// Print from confirm modal — abre preview no navegador
|
||||||
document.getElementById('confirm-print').addEventListener('click', async () => {
|
document.getElementById('confirm-print').addEventListener('click', async () => {
|
||||||
closeConfirmModal();
|
closeConfirmModal();
|
||||||
const btn = document.getElementById('btn-recibo');
|
// Reconstrói os mesmos dados do tauriRecibo e abre preview
|
||||||
btn.disabled = true;
|
const cartCred = estado.cartoes.credito.reduce((s,r) => s+(r.valor||0), 0);
|
||||||
try {
|
const cartDeb = estado.cartoes.debito.reduce((s,r) => s+(r.valor||0), 0);
|
||||||
await tauriRecibo();
|
const cartAli = estado.cartoes.alimentacao.reduce((s,r) => s+(r.valor||0), 0);
|
||||||
alert('✅ Impressão enviada!');
|
const cartPix = estado.cartoes.pix.reduce((s,r) => s+(r.valor||0), 0);
|
||||||
} catch(e) {
|
const data = {
|
||||||
alert('Erro ao imprimir: ' + (e.message || e));
|
loja: estado.loja, data: estado.data, operador: estado.operador, turno: estado.turno,
|
||||||
} finally {
|
saldo_troco: parseNum(val('sys-troco')) || 0,
|
||||||
btn.disabled = false;
|
saldo_esperado: parseNum(val('f-saldo-esperado')) || 0,
|
||||||
}
|
fechamento: parseNum(val('f-fech-dinheiro')) + parseNum(val('f-fech-cartoes')) + parseNum(val('f-fech-receber')),
|
||||||
|
despesas: estado.despesas.map(r => ({ desc: r.desc||'', obs: r.obs||'', valor: r.valor||0 })),
|
||||||
|
sangrias: estado.sangrias.map(r => ({ hora: r.hora||'', retirada: parseNum(r.retirada)||0, gerente: r.gerente||'' })),
|
||||||
|
pixcnpj: estado.pixcnpj.map(r => ({ nome: r.nome||'', valor: r.valor||0 })),
|
||||||
|
vales: estado.vales.map(r => ({ nome: r.nome||'', obs: r.obs||'', valor: r.valor||0 })),
|
||||||
|
receber: estado.receber.map(r => ({ nome: r.nome||'', valor: r.valor||0 })),
|
||||||
|
cancelamentos: estado.cancelamentos.map(r => ({ numero: r.numero||'', valor: r.valor||0, motivo: r.motivo||'' })),
|
||||||
|
credito: cartCred, debito: cartDeb, alimentacao: cartAli,
|
||||||
|
cartoes: { pix: cartPix },
|
||||||
|
observacoes: estado.observacoes,
|
||||||
|
};
|
||||||
|
openPrintPreview(data);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Confirm send
|
// Confirm send
|
||||||
@@ -1328,10 +1479,17 @@ document.getElementById('confirm-send').addEventListener('click', async () => {
|
|||||||
const r = await tauriEnviar();
|
const r = await tauriEnviar();
|
||||||
estado.sync_status = 'synced';
|
estado.sync_status = 'synced';
|
||||||
saveState();
|
saveState();
|
||||||
alert('✅ Enviado com sucesso!');
|
alert('✅ Enviado com sucesso!\n\n📋 Tabela: fechamentos_web\n🌐 Servidor: supabase.ofrangao.com.br\n\nAbra o Supabase Studio para verificar.');
|
||||||
} catch(e) {
|
} catch(e) {
|
||||||
await tauriSaveLocal();
|
await tauriSaveLocal();
|
||||||
alert('Erro ao enviar: ' + (e.message || e) + '\n(Dados salvos localmente)');
|
const msg = e?.message || e?.toString?.() || String(e);
|
||||||
|
alert(
|
||||||
|
'⚠️ Erro ao enviar para o Supabase.\n\n' +
|
||||||
|
'📋 Tabela: fechamentos_web\n' +
|
||||||
|
'🌐 Servidor: supabase.ofrangao.com.br\n\n' +
|
||||||
|
'O servidor pode estar offline. Dados salvos localmente.\n\n' +
|
||||||
|
'Erro: ' + msg
|
||||||
|
);
|
||||||
} finally {
|
} finally {
|
||||||
btn.disabled = false;
|
btn.disabled = false;
|
||||||
btn.textContent = '💾 Enviar Fechamento';
|
btn.textContent = '💾 Enviar Fechamento';
|
||||||
@@ -1347,10 +1505,13 @@ document.getElementById('btn-recibo').addEventListener('click', async () => {
|
|||||||
const btn = document.getElementById('btn-recibo');
|
const btn = document.getElementById('btn-recibo');
|
||||||
btn.disabled = true;
|
btn.disabled = true;
|
||||||
try {
|
try {
|
||||||
|
// Tenta ESC/POS primeiro; se falhar, abre preview no navegador
|
||||||
await tauriRecibo();
|
await tauriRecibo();
|
||||||
alert('✅ Impressão enviada!');
|
// Se chegou aqui sem erro, ESC/POS funcionou
|
||||||
} catch(e) {
|
} catch(e) {
|
||||||
alert('Erro ao imprimir: ' + (e.message || e));
|
// ESC/POS falhou — openPrintPreview já foi chamado dentro de tauriRecibo
|
||||||
|
// Só mostra alerta se nem o preview abriu
|
||||||
|
console.warn('Recibo:', e);
|
||||||
} finally {
|
} finally {
|
||||||
btn.disabled = false;
|
btn.disabled = false;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user