Compare commits

...

3 Commits

Author SHA1 Message Date
root 2253d45d00 v1.2.3: btn voltar no preview; relatorio corrigido (fechamento-esperado); cartoes com detalhes p/ Supabase
Build Fechamento de Caixa / build (macos-14, app) (push) Has been cancelled
Build Fechamento de Caixa / build (ubuntu-22.04, deb) (push) Has been cancelled
Build Fechamento de Caixa / build (windows-2022, msi) (push) Has been cancelled
2026-06-24 01:10:43 +00:00
root 60e15dbed3 fix impressão: window.print() in-app em vez de window.open(); alert com tabela destino no envio
Build Fechamento de Caixa / build (macos-14, app) (push) Has been cancelled
Build Fechamento de Caixa / build (ubuntu-22.04, deb) (push) Has been cancelled
Build Fechamento de Caixa / build (windows-2022, msi) (push) Has been cancelled
2026-06-23 18:28:58 +00:00
root 53767df80c preview+print: janela preview 80mm abre no navegador; btn-recibo e confirm-print abrem preview; ESC/POS fallback automático
Build Fechamento de Caixa / build (macos-14, app) (push) Has been cancelled
Build Fechamento de Caixa / build (ubuntu-22.04, deb) (push) Has been cancelled
Build Fechamento de Caixa / build (windows-2022, msi) (push) Has been cancelled
2026-06-23 17:07:54 +00:00
6 changed files with 217 additions and 23 deletions
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
View File
Binary file not shown.
-1
View File
@@ -122,7 +122,6 @@ impl ReciboData {
// Corta papel
out.extend(escpos::FEED_CUT);
out.extend(escpos::CUT);
out
}
+217 -22
View File
@@ -1034,7 +1034,7 @@ const LS_KEY = 'fc_v7';
const CFG_KEY = 'fc_cfg';
function buildEstadoForBackend() {
// Monta o objeto exactly as o backend espera (storage.rs → salvar_fechamento)
// Monta o objeto que vai para o Supabase — inclui TUDO, todos os detalhes
return {
uuid: estado.uuid,
id_local: estado.id_local,
@@ -1042,7 +1042,7 @@ function buildEstadoForBackend() {
data: estado.data,
operador: estado.operador,
turno: estado.turno,
saldo_troco: parseNum(val('sys-troco')), // troco do sistema
saldo_troco: parseNum(val('sys-troco')),
saldo_esperado: parseNum(val('f-saldo-esperado')),
fechamento: parseNum(val('f-fech-dinheiro')) + parseNum(val('f-fech-cartoes')) + parseNum(val('f-fech-receber')),
diferenca: parseNum(val('f-saldo-esperado')) - (parseNum(val('f-fech-dinheiro')) + parseNum(val('f-fech-cartoes')) + parseNum(val('f-fech-receber'))),
@@ -1052,11 +1052,12 @@ function buildEstadoForBackend() {
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 || '' })),
// Cartões com detalhes completos (não só totais)
cartoes: {
credito: estado.cartoes.credito.map(r => r.valor || 0),
debito: estado.cartoes.debito.map(r => r.valor || 0),
alimentacao: estado.cartoes.alimentacao.map(r => r.valor || 0),
pix: estado.cartoes.pix.map(r => r.valor || 0),
credito: estado.cartoes.credito.map(r => ({ valor: r.valor || 0, obs: r.obs || '' })),
debito: estado.cartoes.debito.map(r => ({ valor: r.valor || 0, obs: r.obs || '' })),
alimentacao: estado.cartoes.alimentacao.map(r => ({ valor: r.valor || 0, obs: r.obs || '' })),
pix: estado.cartoes.pix.map(r => ({ valor: r.valor || 0, obs: r.obs || '' })),
},
observacoes: estado.observacoes,
sync_status: estado.sync_status,
@@ -1191,6 +1192,171 @@ async function tauriEnviar() {
}
}
// ─── Print Preview Window ──────────────────────────────────────────────────
function buildPrintHTML(data) {
const fmt = v => 'R$ ' + (v || 0).toFixed(2).replace('.', ',');
const vSangrias = (data.sangrias||[]).reduce((s,r) => s+(parseFloat(r.retirada)||0), 0);
const vDespesas = (data.despesas||[]).reduce((s,r) => s+(parseFloat(r.valor)||0), 0);
const vVales = (data.vales||[]).reduce((s,r) => s+(parseFloat(r.valor)||0), 0);
const vReceber = (data.receber||[]).reduce((s,r) => s+(parseFloat(r.valor)||0), 0);
const vPixCnpj = (data.pixcnpj||[]).reduce((s,r) => s+(parseFloat(r.valor)||0), 0);
const vCredito = data.cartoes?.credito?.reduce?.((s,r) => s+(parseFloat(r)||0), 0) || data.credito || 0;
const vDebito = data.cartoes?.debito?.reduce?.((s,r) => s+(parseFloat(r)||0), 0) || data.debito || 0;
const vAliment = data.cartoes?.alimentacao?.reduce?.((s,r) => s+(parseFloat(r)||0), 0) || data.alimentacao || 0;
const vPixCartao = data.cartoes?.pix?.reduce?.((s,r) => s+(parseFloat(r)||0), 0) || 0;
const vCancel = (data.cancelamentos||[]).reduce((s,r) => s+(parseFloat(r.valor)||0), 0);
// Diferença = Fechamento Total Saldo Esperado
const vFechamento = (data.fechamento || 0);
const diferenca = vFechamento - (data.saldo_esperado || 0);
const diffStr = diferenca >= 0 ? `+${fmt(diferenca)}` : fmt(diferenca);
const diffColor = diferenca === 0 ? '#2e7d32' : (diferenca > 0 ? '#e65100' : '#c0272d');
const fmtRow = (label, valor) =>
`<div class="row"><span>${label}</span><span>${fmt(valor)}</span></div>`;
const fmtDetail = (label, valor, obs) =>
`<div class="row sub"><span>${label}</span><span>${fmt(valor)}</span></div>`;
const mkDet = (arr, fmtFn) => {
if (!arr || arr.length === 0) return '';
return arr.map(r => {
if (!r) return '';
const v = fmtFn(r);
const nome = r.nome || r.desc || r.hora || r.numero || '';
const obs = r.obs || r.motivo || '';
return fmtDetail(nome, v, obs);
}).join('');
};
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: 12px; width: 80mm; margin: 0 auto; padding: 6px; }
.header { text-align: center; margin-bottom: 6px; }
.header h1 { font-size: 16px; }
.header h2 { font-size: 12px; font-weight: normal; }
hr { border: none; border-top: 1px dashed #000; margin: 4px 0; }
.row { display: flex; justify-content: space-between; padding: 1px 0; }
.row.sub { padding-left: 8px; font-size: 11px; color: #444; }
.row strong { font-weight: bold; }
.section-title { font-weight: bold; font-size: 12px; border-top: 1px dashed #000; padding-top: 3px; margin-top: 5px; }
.grand-total { font-size: 14px; font-weight: bold; text-align: center; padding: 5px; border: 2px solid #000; margin: 6px 0; }
.footer { text-align: center; font-size: 10px; color: #777; margin-top: 6px; }
@media print { body { margin: 0; width: 80mm; } }
</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>
<hr>
<div class="section-title">SANGRIAS</div>
${mkDet(data.sangrias, r => parseFloat(r.retirada)||0)}
${fmtRow('TOTAL SANGRIAS', vSangrias)}
<div class="section-title">DESPESAS</div>
${mkDet(data.despesas, r => parseFloat(r.valor)||0)}
${fmtRow('TOTAL DESPESAS', vDespesas)}
<div class="section-title">VALES</div>
${mkDet(data.vales, r => parseFloat(r.valor)||0)}
${fmtRow('TOTAL VALES', vVales)}
<div class="section-title">A RECEBER</div>
${mkDet(data.receber, r => parseFloat(r.valor)||0)}
${fmtRow('TOTAL A RECEBER', vReceber)}
<div class="section-title">PIX</div>
<div class="section-title" style="font-size:11px;font-weight:normal">— Cartão —</div>
${fmtRow('PIX CARTAO', vPixCartao)}
<div class="section-title" style="font-size:11px;font-weight:normal">— CNPJ —</div>
${mkDet(data.pixcnpj, r => parseFloat(r.valor)||0)}
${fmtRow('TOTAL PIX CNPJ', vPixCnpj)}
<div class="section-title">CARTOES</div>
${fmtRow('CREDITO', vCredito)}
${fmtRow('DEBITO', vDebito)}
${fmtRow('VOUCHER', vAliment)}
<div class="section-title">CANCELAMENTOS</div>
${mkDet(data.cancelamentos, r => parseFloat(r.valor)||0)}
${fmtRow('TOTAL CANCELAMENTOS', vCancel)}
<hr>
${fmtRow('SALDO ESPERADO', data.saldo_esperado || 0)}
${fmtRow('FECHAMENTO', vFechamento)}
<div class="grand-total" style="color:${diffColor}">DIFERENCA: ${diffStr}</div>
<div class="footer">Gerado em ${new Date().toLocaleString('pt-BR')}</div>
</body></html>`;
}
function openPrintPreview(data) {
// Gera HTML e injeta no body para window.print()
const html = buildPrintHTML(data);
const style = document.createElement('style');
style.id = '_print_style';
style.textContent = `
@media print {
body { width: 80mm; margin: 0 auto; font-family: 'Courier New', monospace; font-size: 13px; }
.no-print, .print-actions { display: none !important; }
hr { border-top: 1px dashed #000 !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; margin-left: 8px; }
.print-actions button:hover { background: #1d4ed8; }
.print-actions .btn-back { background: #6b7280; }
.print-actions .btn-back:hover { background: #4b5563; }
`;
const actions = document.createElement('div');
actions.className = 'print-actions no-print';
actions.innerHTML =
'<button class="btn-back" id="_btn_back_print">← Voltar ao App</button>' +
'<button onclick="window.print()">🖨️ Imprimir (Ctrl+P)</button>';
// Guarda originais
const originalBody = document.body.innerHTML;
const originalOnAfterPrint = window.onafterprint;
// Botão voltar restaura sem reload
document.addEventListener('click', function onBackClick(e) {
if (e.target.id === '_btn_back_print') {
document.body.innerHTML = originalBody;
const oldStyle = document.getElementById('_print_style');
if (oldStyle) oldStyle.remove();
document.removeEventListener('click', onBackClick);
window.onafterprint = originalOnAfterPrint;
// Re-init app state — mínimo viable restore
initApp();
}
});
document.body.innerHTML = html;
document.head.appendChild(style);
document.body.insertBefore(actions, document.body.firstChild);
window.onafterprint = () => {
document.body.innerHTML = originalBody;
const oldStyle = document.getElementById('_print_style');
if (oldStyle) oldStyle.remove();
window.onafterprint = originalOnAfterPrint;
document.removeEventListener('click', onBackClick);
initApp();
};
}
function triggerWindowPrint() {
// Chama window.print() na janela pai (o próprio app)
window.print();
}
// ─── Recibo 80mm via Tauri (ESC/POS USB) ─────────────────────────────────
async function tauriRecibo() {
// Build flat totals for the Rust ReciboData struct
const cartCred = estado.cartoes.credito.reduce((s,r) => s+(r.valor||0), 0);
@@ -1239,7 +1405,15 @@ async function tauriRecibo() {
observacoes: estado.observacoes,
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() {
@@ -1303,19 +1477,30 @@ document.getElementById('confirm-close').addEventListener('click', closeConfirmM
document.getElementById('confirm-cancel').addEventListener('click', 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 () => {
closeConfirmModal();
const btn = document.getElementById('btn-recibo');
btn.disabled = true;
try {
await tauriRecibo();
alert('✅ Impressão enviada!');
} catch(e) {
alert('Erro ao imprimir: ' + (e.message || e));
} finally {
btn.disabled = false;
}
// Reconstrói os mesmos dados do tauriRecibo e abre preview
const cartCred = estado.cartoes.credito.reduce((s,r) => s+(r.valor||0), 0);
const cartDeb = estado.cartoes.debito.reduce((s,r) => s+(r.valor||0), 0);
const cartAli = estado.cartoes.alimentacao.reduce((s,r) => s+(r.valor||0), 0);
const cartPix = estado.cartoes.pix.reduce((s,r) => s+(r.valor||0), 0);
const data = {
loja: estado.loja, data: estado.data, operador: estado.operador, turno: estado.turno,
saldo_troco: parseNum(val('sys-troco')) || 0,
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
@@ -1328,10 +1513,17 @@ document.getElementById('confirm-send').addEventListener('click', async () => {
const r = await tauriEnviar();
estado.sync_status = 'synced';
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) {
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 {
btn.disabled = false;
btn.textContent = '💾 Enviar Fechamento';
@@ -1347,10 +1539,13 @@ document.getElementById('btn-recibo').addEventListener('click', async () => {
const btn = document.getElementById('btn-recibo');
btn.disabled = true;
try {
// Tenta ESC/POS primeiro; se falhar, abre preview no navegador
await tauriRecibo();
alert('✅ Impressão enviada!');
// Se chegou aqui sem erro, ESC/POS funcionou
} 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 {
btn.disabled = false;
}