Compare commits

...

8 Commits

Author SHA1 Message Date
root 7e57d30863 v1.2.7: conserta 4 bugs — impressao, enviar, dia anterior, localStorage
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 13:38:00 +00:00
root 7a93a30c22 v1.2.6: init() restaura operador+turno; tar inclui WebView2Loader.dll
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 13:11:43 +00:00
root 230304e878 v1.2.5 — fixes: diferenca=fechamento-esperado; btn-enviar func; mapas tabela 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
MAPA SUPABASE:
Tabela: fechamentos_web
  uuid | loja | data | operador | turno
  saldo_troco | saldo_esperado | fechamento | diferenca
  sync_status | observacoes | dados (JSON com tudo)
  atualizado_em

Tabela: despesas (via JSON em dados->despesas)
Tabela: sangrias (via JSON em dados->sangrias)
Tabela: vales (via JSON em dados->vales)
Tabela: receber (via JSON em dados->receber)
Tabela: pixcnpj (via JSON em dados->pixcnpj)
Tabela: cancelamentos (via JSON em dados->cancelamentos)
Tabela: cartoes (dados->cartoes: credito/debito/alimentacao/pix arrays)

Tabela: listas_personalizadas (operadores/gerentes/despesas_custom/clientes)
  loja | operadores[] | gerentes[] | despesas_custom[] | clientes[]

FUNCOES RPC:
  upsert_fechamento() — upsert fechamentos_web por uuid
  upsert_listas() — upsert listas_personalizadas por loja

BOTÃO DIA ANTERIOR: usa sb_listar_recentes → tabela fechamentos_web
  Seleciona por numero (1=N Mais Recente, 2=2o Mais Recente...)
  ou automatico: data-1dia
2026-06-24 01:32:18 +00:00
root d185f82bd5 Revert "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
This reverts commit 2253d45d00.
2026-06-24 01:18:57 +00:00
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
root 208eede526 v1.2.0 — campos texto livre vs datalist; diferença abaixo saldo esperado + badge OK/sobra; btn-anterior com datas do Supabase; ESC/POS copy fallback; modal com PIX+diferença+print; debug 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-23 16:46:53 +00:00
10 changed files with 396 additions and 101 deletions
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
View File
Binary file not shown.
+36 -13
View File
@@ -122,7 +122,6 @@ impl ReciboData {
// Corta papel
out.extend(escpos::FEED_CUT);
out.extend(escpos::CUT);
out
}
@@ -227,21 +226,45 @@ impl PrinterManager {
#[cfg(windows)]
fn windows_write(&self, path: &str, data: &[u8]) -> Result<usize, String> {
use std::fs::OpenOptions;
use std::io::Write;
let file = OpenOptions::new()
.write(true)
.create(false)
.open(path)
.map_err(|e| format!("Erro ao abrir {}: {}", path, e))?;
// Tenta via File primeiro (mais simples)
{
let file = std::fs::OpenOptions::new()
.write(true)
.create(false)
.open(path);
if let Ok(mut f) = file {
let r = f.write_all(data);
let _ = f.flush();
if r.is_ok() {
return Ok(data.len());
}
}
}
let mut f = file;
f.write_all(data)
.map_err(|e| format!("Erro ao escrever: {}", e))?;
f.flush()
.map_err(|e| format!("Erro ao fazer flush: {}", e))?;
Ok(data.len())
// Fallback: usa API Windows direta via std::process::Command + > arquivo
// Isso绕过 problemas de locking do OpenOptions
let tmp = std::env::temp_dir().join("escpos_tmp.bin");
std::fs::write(&tmp, data)
.map_err(|e| format!("Falha ao criar arquivo temporario: {}", e))?;
let status = std::process::Command::new("cmd")
.args(["/C", &format!("copy /B {} {} /Y > nul 2>&1", tmp.display(), path)])
.status();
let _ = std::fs::remove_file(&tmp);
if status.map(|s| s.success()).unwrap_or(false) {
log::info!("Escritos {} bytes para {}", data.len(), path);
Ok(data.len())
} else {
Err(format!(
"Falha ao enviar dados para {}. Verifique se a impressora USB está \
conectada e ligada. Tente configurar manualmente em Config > Impressora.",
path
))
}
}
}
+360 -88
View File
@@ -405,6 +405,12 @@
<label>Informe o saldo esperado (contado em caixa)</label>
<input type="text" id="f-saldo-esperado" placeholder="0,00" />
</div>
<!-- DIFERENÇA — dentro do Saldo Esperado, logo abaixo -->
<div class="dif-box" style="margin-top:8px">
<span class="dif-label">Diferença</span>
<span class="dif-value zero" id="diferenca">R$ 0,00</span>
<span id="dif-badge" style="font-size:11px;font-weight:700;padding:2px 8px;border-radius:10px;background:#e8f5e9;color:#2e7d32;display:none">OK</span>
</div>
</div>
<!-- FECHAMENTO (VALORES CONTADOS) -->
@@ -421,14 +427,6 @@
</div>
</div>
<!-- DIFERENÇA -->
<div class="section">
<div class="dif-box">
<span class="dif-label">Diferença</span>
<span class="dif-value zero" id="diferenca">R$ 0,00</span>
</div>
</div>
<!-- OBS -->
<div class="section" style="flex:1;display:flex;flex-direction:column">
<div class="section-header">📝 Observações</div>
@@ -643,19 +641,23 @@ function makeRow(tblId, rowIndex, rowData) {
if (col === 'valor' || col === 'retirada') td.className = 'num';
const inp = document.createElement('input');
inp.type = 'text';
// For numeric cols that hold money, format with 2 decimals
// For text cols (hora, desc, obs, nome, gerente, motivo, numero) keep as-is
const numericCols = ['valor', 'retirada'];
inp.value = rowData[col] !== undefined
? (rowData[col] === 0 ? '' : fmtNum(rowData[col]))
? (numericCols.includes(col) ? (rowData[col] === 0 ? '' : fmtNum(rowData[col])) : (rowData[col] || ''))
: '';
inp.placeholder = col === 'valor' || col === 'retirada' ? '0,00' : (col === 'hora' ? 'HH:MM' : '');
inp.dataset.tbl = tblId;
inp.dataset.row = rowIndex;
inp.dataset.col = col;
// datalist only for fields that should offer suggestions from config
if (col === 'gerente') inp.setAttribute('list', 'dl-gerente');
if (col === 'desc') inp.setAttribute('list', 'dl-despesa');
// tbl-receber: clientes free-text (sem datalist)
// tbl-vales: usa dl-vales
if (col === 'nome' && tblId === 'tbl-vales') inp.setAttribute('list', 'dl-vales');
if (col === 'nome' && tblId === 'tbl-pix') inp.setAttribute('list', 'dl-generico');
// tbl-pix nome = free text (no datalist)
// tbl-receber nome = free text (no datalist)
// tbl-cancelamentos numero + motivo = free text (no datalist)
inp.addEventListener('input', onTableInput);
inp.addEventListener('blur', onCellBlur);
inp.addEventListener('keydown', onCellKeydown);
@@ -693,6 +695,7 @@ function onTableInput(e) {
if (formatted !== e.target.value) e.target.value = formatted;
field[row][col] = formatted;
} else {
// text fields: nome, desc, obs, gerente, motivo, numero — keep as string
field[row][col] = e.target.value;
}
updateTotals();
@@ -706,6 +709,7 @@ function onCellBlur(e) {
e.target.value = isNaN(v) || v === 0 ? '' : fmtNum(v);
updateTotals();
}
// For text cols (hora, nome, desc, obs, gerente, motivo, numero) — keep as-is
}
function onCellKeydown(e) {
@@ -739,8 +743,9 @@ function focusInput(tblId, rowIndex, col) {
function addRowAndFocus(tblId, focusCol) {
const cfg = TABLES[tblId];
const field = getField(tblId);
const numericCols = ['valor', 'retirada'];
const empty = {};
cfg.cols.forEach(c => { empty[c] = c === 'valor' || c === 'retirada' ? 0 : ''; });
cfg.cols.forEach(c => { empty[c] = numericCols.includes(c) ? 0 : ''; });
field.push(empty);
renderTable(tblId);
focusInput(tblId, field.length - 1, focusCol);
@@ -849,16 +854,33 @@ function updateTotals() {
const saldoCaixa = sysTotal;
const diff = saldoCaixa - parseNum(val('f-saldo-esperado'));
const difEl = document.getElementById('diferenca');
const badgeEl = document.getElementById('dif-badge');
if (difEl) {
difEl.textContent = fmt(diff);
if (diff === 0) {
difEl.className = 'dif-value zero';
if (badgeEl) { badgeEl.textContent = '✓ OK'; badgeEl.style.background='#e8f5e9'; badgeEl.style.color='#2e7d32'; badgeEl.style.display='inline'; }
} else if (diff < 0 && diff >= -5) {
difEl.className = 'dif-value neg'; // OK até 5
difEl.className = 'dif-value neg'; // OK até 5 de tolerância
if (badgeEl) { badgeEl.textContent = '✓ OK'; badgeEl.style.background='#e8f5e9'; badgeEl.style.color='#2e7d32'; badgeEl.style.display='inline'; }
} else if (diff > 0 && diff <= 3) {
difEl.className = 'dif-value zero'; // OK + até 3
if (badgeEl) { badgeEl.textContent = '✓ OK'; badgeEl.style.background='#e8f5e9'; badgeEl.style.color='#2e7d32'; badgeEl.style.display='inline'; }
} else {
difEl.className = 'dif-value pos'; // Sobra > +3
// Sobra mais de R$3 ou falta mais de R$5
difEl.className = diff > 0 ? 'dif-value pos' : 'dif-value neg';
if (badgeEl) {
if (diff > 0) {
badgeEl.textContent = '⚠ SOBRANDO';
badgeEl.style.background='#fff3e0';
badgeEl.style.color='#e65100';
} else {
badgeEl.textContent = '⚠ FALTANDO';
badgeEl.style.background='#ffebee';
badgeEl.style.color='#c0272d';
}
badgeEl.style.display='inline';
}
}
}
}
@@ -1000,7 +1022,9 @@ function init() {
const today = new Date().toISOString().split('T')[0];
setVal('f-data', today);
estado.data = today;
setVal('f-loja', estado.loja);
setVal('f-loja', estado.loja || 'União');
setVal('f-operador', estado.operador || '');
setVal('f-turno', estado.turno || '');
syncHeader();
PRE_FILL_TABLES.forEach(ensureOneRow);
Object.keys(TABLES).forEach(renderTable);
@@ -1023,7 +1047,6 @@ function buildEstadoForBackend() {
saldo_troco: parseNum(val('sys-troco')), // troco do sistema
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'))),
despesas: estado.despesas.map(r => ({ desc: r.desc || '', obs: r.obs || '', valor: r.valor || 0 })),
sangrias: estado.sangrias.map(r => ({ hora: r.hora || '', retirada: r.retirada || 0, gerente: r.gerente || '' })),
pixcnpj: estado.pixcnpj.map(r => ({ nome: r.nome || '', valor: r.valor || 0 })),
@@ -1061,8 +1084,44 @@ function loadState() {
const raw = localStorage.getItem(LS_KEY);
if (!raw) return false;
const saved = JSON.parse(raw);
// Merge carefully (cartoes may be in old format)
Object.assign(estado, saved);
// Restore simple fields
estado.uuid = saved.uuid || estado.uuid;
estado.id_local = saved.id_local !== undefined ? saved.id_local : null;
estado.loja = saved.loja || estado.loja;
estado.data = saved.data || estado.data;
estado.operador = saved.operador || estado.operador;
estado.turno = saved.turno || estado.turno;
estado.saldo_troco = saved.saldo_troco || 0;
estado.saldo_esperado = saved.saldo_esperado || 0;
estado.fechamento = saved.fechamento || 0;
estado.observacoes = saved.observacoes || '';
estado.sync_status = saved.sync_status || 'local';
// Restore tables (always as arrays of objects)
estado.despesas = Array.isArray(saved.despesas) ? saved.despesas : [];
estado.sangrias = Array.isArray(saved.sangrias) ? saved.sangrias : [];
estado.pixcnpj = Array.isArray(saved.pixcnpj) ? saved.pixcnpj : [];
estado.vales = Array.isArray(saved.vales) ? saved.vales : [];
estado.receber = Array.isArray(saved.receber) ? saved.receber : [];
estado.cancelamentos = Array.isArray(saved.cancelamentos) ? saved.cancelamentos : [];
// Restore cartoes — each entry must be { valor: number }
if (saved.cartoes) {
estado.cartoes.credito = Array.isArray(saved.cartoes.credito)
? saved.cartoes.credito.map(v => ({ valor: typeof v === 'object' && v !== null ? (v.valor || 0) : (parseNum(v) || 0) }))
: [];
estado.cartoes.debito = Array.isArray(saved.cartoes.debito)
? saved.cartoes.debito.map(v => ({ valor: typeof v === 'object' && v !== null ? (v.valor || 0) : (parseNum(v) || 0) }))
: [];
estado.cartoes.alimentacao = Array.isArray(saved.cartoes.alimentacao)
? saved.cartoes.alimentacao.map(v => ({ valor: typeof v === 'object' && v !== null ? (v.valor || 0) : (parseNum(v) || 0) }))
: [];
estado.cartoes.pix = Array.isArray(saved.cartoes.pix)
? saved.cartoes.pix.map(v => ({ valor: typeof v === 'object' && v !== null ? (v.valor || 0) : (parseNum(v) || 0) }))
: [];
}
// Restore fechamento_contado
if (saved.fechamento_contado) {
estado.fechamento_contado = saved.fechamento_contado;
}
setVal('f-data', estado.data || '');
setVal('f-operador', estado.operador || '');
setVal('f-turno', estado.turno || '');
@@ -1070,9 +1129,10 @@ function loadState() {
setVal('f-obs', estado.observacoes || '');
setVal('f-saldo-esperado', estado.saldo_esperado === 0 ? '' : fmtNum(estado.saldo_esperado));
setVal('sys-troco', estado.saldo_troco === 0 ? '' : fmtNum(estado.saldo_troco));
setVal('f-fech-dinheiro', estado.fechamento_contado && estado.fechamento_contado.dinheiro ? fmtNum(estado.fechamento_contado.dinheiro) : '');
setVal('f-fech-cartoes', estado.fechamento_contado && estado.fechamento_contado.cartoes ? fmtNum(estado.fechamento_contado.cartoes) : '');
setVal('f-fech-receber', estado.fechamento_contado && estado.fechamento_contado.receber ? fmtNum(estado.fechamento_contado.receber) : '');
const fc = estado.fechamento_contado || {};
setVal('f-fech-dinheiro', fc.dinheiro ? fmtNum(fc.dinheiro) : '');
setVal('f-fech-cartoes', fc.cartoes ? fmtNum(fc.cartoes) : '');
setVal('f-fech-receber', fc.receber ? fmtNum(fc.receber) : '');
syncHeader();
PRE_FILL_TABLES.forEach(ensureOneRow);
Object.keys(TABLES).forEach(renderTable);
@@ -1153,17 +1213,161 @@ async function tauriEnviar() {
const data = buildEstadoForBackend();
// 1. Salva local
await window.__TAURI__.core.invoke('salvar_fechamento', { estado: data }).catch(e => console.warn('local save err:', e));
// 2. Envia Supabase
// 2. Envia Supabase — debug completo
console.log('=== Enviando para Supabase ===');
console.log('URL:', 'https://supabase.ofrangao.com.br/rest/v1/fechamentos');
console.log('Payload:', JSON.stringify(data, null, 2));
try {
const result = await window.__TAURI__.core.invoke('sb_salvar_fechamento', { estado: data });
console.log('Supabase salvo:', result);
console.log('Supabase resposta:', JSON.stringify(result));
return result;
} catch(e) {
console.error('Erro sb_salvar_fechamento:', e);
console.error('Erro sb_salvar_fechamento:', JSON.stringify(e));
// Tenta debug: qual era o payload exato?
console.error('Payload que falhou:', JSON.stringify(data));
throw e;
}
}
// ─── Print Preview Window ──────────────────────────────────────────────────
function buildPrintHTML(data) {
const fmt = v => 'R$ ' + (v || 0).toFixed(2).replace('.', ',');
const diferenca = (data.fechamento || 0) - (data.saldo_esperado || 0);
const diffStr = diferenca >= 0 ? `+${fmt(diferenca)}` : fmt(diferenca);
const diffColor = diferenca === 0 ? '#2e7d32' : (diferenca > 0 ? '#e65100' : '#c0272d');
let rows = '';
// Cancelamentos (sum the array first — single cancelamentos is a float total, array is from confirm-print)
const cancelTotal = Array.isArray(data.cancelamentos)
? data.cancelamentos.reduce((s, c) => s + (parseNum(c.valor || 0) || 0), 0)
: (data.cancelamentos || 0);
// PIX total (flat number from tauriRecibo; pixcnpj is also flat total)
const pixTotal = data.pix || data.voucher || 0;
const pixCnpjTotal = typeof data.pixcnpj === 'number' ? data.pixcnpj : 0;
// 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 (flat total, not array)
if (pixCnpjTotal > 0) {
rows += `<tr><td>PIX CNPJ</td><td>${fmt(pixCnpjTotal)}</td><td></td></tr>`;
}
// Cancelamentos
if (cancelTotal > 0) {
rows += `<tr><td>Cancelamentos</td><td>${fmt(cancelTotal)}</td><td></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||0)}</span></div>
<div class="row"><span>Débito:</span><span>${fmt(data.debito||0)}</span></div>
<div class="row"><span>Alimentação:</span><span>${fmt(data.alimentacao||0)}</span></div>
<div class="row"><span>PIX Cartão:</span><span>${fmt(pixTotal)}</span></div>
<div class="row"><span>Cancelamentos:</span><span>${fmt(cancelTotal)}</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() {
// Build flat totals for the Rust ReciboData struct
const cartCred = estado.cartoes.credito.reduce((s,r) => s+(r.valor||0), 0);
@@ -1179,7 +1383,6 @@ async function tauriRecibo() {
const fechamentoCounted = parseNum(val('f-fech-dinheiro')) + parseNum(val('f-fech-cartoes')) + parseNum(val('f-fech-receber'));
const saldoEsp = parseNum(val('f-saldo-esperado'));
const sysTroco = parseNum(val('sys-troco'));
const data = {
uuid: estado.uuid,
id_local: estado.id_local,
@@ -1190,15 +1393,8 @@ async function tauriRecibo() {
saldo_troco: sysTroco,
saldo_esperado: saldoEsp,
fechamento: fechamentoCounted,
diferenca: sysTroco - saldoEsp,
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||'' })),
cartoes: { credito: cartCred, debito: cartDeb, alimentacao: cartAli, pix: cartPix },
// Flat totals for ReciboData struct (Rust)
diferenca: fechamentoCounted - saldoEsp,
// Flat totals for ReciboData struct (Rust) — must be numbers
credito: cartCred,
debito: cartDeb,
alimentacao: cartAli,
@@ -1209,10 +1405,26 @@ async function tauriRecibo() {
vales: valesTotal,
areceber: receberTotal,
cancelamentos: cancelTotal,
// Full arrays for backend/storage
despesas_arr: estado.despesas.map(r => ({ desc: r.desc||'', obs: r.obs||'', valor: r.valor||0 })),
sangrias_arr: estado.sangrias.map(r => ({ hora: r.hora||'', retirada: parseNum(r.retirada)||0, gerente: r.gerente||'' })),
pixcnpj_arr: estado.pixcnpj.map(r => ({ nome: r.nome||'', valor: r.valor||0 })),
vales_arr: estado.vales.map(r => ({ nome: r.nome||'', obs: r.obs||'', valor: r.valor||0 })),
receber_arr: estado.receber.map(r => ({ nome: r.nome||'', valor: r.valor||0 })),
cancelamentos_arr: estado.cancelamentos.map(r => ({ numero: r.numero||'', valor: r.valor||0, motivo: r.motivo||'' })),
cartoes: { credito: cartCred, debito: cartDeb, alimentacao: cartAli, pix: cartPix },
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() {
@@ -1243,8 +1455,11 @@ function openConfirmModal() {
const vales = estado.vales.reduce((s,r) => s+(parseNum(r.valor||0)||0), 0);
const receber = estado.receber.reduce((s,r) => s+(parseNum(r.valor||0)||0), 0);
const cancelamentos = estado.cancelamentos.reduce((s,r) => s+(parseNum(r.valor||0)||0), 0);
const pixcnpj = estado.pixcnpj.reduce((s,r) => s+(parseNum(r.valor||0)||0), 0);
const fechamento = parseNum(val('f-fech-dinheiro')) + parseNum(val('f-fech-cartoes')) + parseNum(val('f-fech-receber'));
const saldoEsp = parseNum(val('f-saldo-esperado'));
const diferenca = (parseNum(val('sys-troco')) || 0) - saldoEsp;
const diffClass = diferenca === 0 ? '#2e7d32' : (diferenca > 0 ? '#e65100' : '#c0272d');
document.getElementById('confirm-summary').innerHTML =
`<div style="margin-bottom:6px"><strong>📅 Data:</strong> ${estado.data} &nbsp; <strong>👤 Operador:</strong> ${operador} &nbsp; <strong>⏰ Turno:</strong> ${turno} &nbsp; <strong>🏪 Loja:</strong> ${loja}</div>
<hr style="border:none;border-top:1px dashed #b3d9f7;margin:6px 0">
@@ -1252,13 +1467,15 @@ function openConfirmModal() {
<div>📝 Despesas: <strong>${fmt(despesas)}</strong></div>
<div>💰 Vales: <strong>${fmt(vales)}</strong></div>
<div>👥 À Receber: <strong>${fmt(receber)}</strong></div>
<div>💳 Crédito: <strong style="color:#c0272d">${fmt(cartCred)}</strong></div>
<div>🔵 PIX CNPJ: <strong>${fmt(pixcnpj)}</strong></div>
<div>💳 Crédito: <strong>${fmt(cartCred)}</strong></div>
<div>💳 Débito: <strong>${fmt(cartDeb)}</strong></div>
<div>💳 Alimentação: <strong>${fmt(cartAli)}</strong></div>
<div>❌ Cancelamentos: <strong>${fmt(cancelamentos)}</strong></div>
<div>📋 Fechamento: <strong>${fmt(fechamento)}</strong></div>
<hr style="border:none;border-top:1px dashed #b3d9f7;margin:6px 0">
<div>🎯 Saldo Esperado: <strong>${fmt(saldoEsp)}</strong></div>
<div style="margin-top:4px">📐 Diferença: <strong style="color:${diffClass}">${fmt(diferenca)}</strong></div>
<div style="margin-top:4px;font-size:10px;color:#888">ID: ${estado.loja}_${estado.data}_${estado.operador}</div>`;
document.getElementById('modal-confirm').classList.add('open');
}
@@ -1271,19 +1488,34 @@ 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;
}
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 cancelTotal = estado.cancelamentos.reduce((s,r) => s+(parseNum(r.valor||0)||0), 0);
const fechamentoCounted = parseNum(val('f-fech-dinheiro')) + parseNum(val('f-fech-cartoes')) + parseNum(val('f-fech-receber'));
const saldoEsp = parseNum(val('f-saldo-esperado'));
const data = {
loja: estado.loja, data: estado.data, operador: estado.operador, turno: estado.turno,
saldo_troco: parseNum(val('sys-troco')) || 0,
saldo_esperado: saldoEsp,
fechamento: fechamentoCounted,
diferenca: fechamentoCounted - saldoEsp,
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,
pix: cartPix, // flat float for buildPrintHTML
cartoes: { credito: estado.cartoes.credito, debito: estado.cartoes.debito, alimentacao: estado.cartoes.alimentacao, pix: estado.cartoes.pix },
observacoes: estado.observacoes,
};
openPrintPreview(data);
});
// Confirm send
@@ -1296,10 +1528,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';
@@ -1315,10 +1554,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;
}
@@ -1331,47 +1573,82 @@ document.getElementById('btn-limpar').addEventListener('click', () => {
});
document.getElementById('btn-anterior').addEventListener('click', async () => {
const d = new Date(estado.data); d.setDate(d.getDate() - 1);
const prevDate = d.toISOString().split('T')[0];
const loja = val('f-loja') || 'União';
// Carrega lista de fechamentos recentes do Supabase com uuid
let recentList = [];
try {
recentList = await window.__TAURI__.core.invoke('sb_listar_recentes', { loja, limite: 20 });
} catch(e) {
console.warn('sb_listar_recentes falhou:', e);
}
// Extrai datas únicas ordenadas + mapeamento data→id
const dateIdMap = {};
const dateList = [];
if (recentList && recentList.length > 0) {
recentList.forEach(item => {
if (item.data && !dateIdMap[item.data]) {
dateIdMap[item.data] = item.id;
dateList.push(item.data);
}
});
dateList.sort().reverse();
}
let selectedDate = null;
let selectedId = null;
if (dateList.length > 0) {
const msg = dateList.map((d, i) => `${i + 1}) ${d}`).join('\n');
const escolha = prompt(`Selecione o número do dia anterior:\n${msg}\n\n(Deixe em branco e clique Cancelar para ver só o dia anterior automático)`);
if (escolha !== null && escolha.trim() !== '') {
const idx = parseInt(escolha) - 1;
if (idx >= 0 && idx < dateList.length) {
selectedDate = dateList[idx];
selectedId = dateIdMap[selectedDate];
}
}
}
// Se não escolheu data, usa dia anterior automático (sem id — carrega por data)
if (!selectedDate) {
const d = new Date(estado.data); d.setDate(d.getDate() - 1);
selectedDate = d.toISOString().split('T')[0];
}
await tauriSaveLocal();
setVal('f-data', prevDate);
estado.data = prevDate;
setVal('f-data', selectedDate);
estado.data = selectedDate;
estado.uuid = crypto.randomUUID ? crypto.randomUUID() : Date.now().toString();
estado.id_local = null;
syncHeader();
// Try Supabase first (source of truth for submitted closings)
// Tenta Supabase primeiro (com id se disponível), senão local
let rascunho = null;
try {
rascunho = await window.__TAURI__.core.invoke('sb_carregar_rascunho', {
loja: val('f-loja'),
data: prevDate
});
} catch(e) {
console.warn('sb_carregar_rascunho falhou, tentando local:', e);
if (selectedId) {
// Usa sb_buscar_por_id para buscar por id numérico do Supabase
try {
rascunho = await window.__TAURI__.core.invoke('sb_buscar_por_id', { id: selectedId });
} catch(e) { console.warn('sb_buscar_por_id falhou:', e); }
}
// Fallback: local SQLite
if (!rascunho) {
try {
rascunho = await window.__TAURI__.core.invoke('carregar_rascunho', {
loja: val('f-loja'),
data: prevDate
});
} catch(e) {
console.warn('carregar_rascunho local falhou:', e);
}
rascunho = await window.__TAURI__.core.invoke('sb_carregar_rascunho', { loja, data: selectedDate });
} catch(e) { console.warn('sb_carregar_rascunho falhou:', e); }
}
if (!rascunho) {
try {
rascunho = await window.__TAURI__.core.invoke('carregar_rascunho', { loja, data: selectedDate });
} catch(e) { console.warn('carregar_rascunho local falhou:', e); }
}
if (rascunho) {
// Meta fields
if (rascunho.operador) { setVal('f-operador', rascunho.operador); estado.operador = rascunho.operador; }
if (rascunho.turno) { setVal('f-turno', rascunho.turno); estado.turno = rascunho.turno; }
if (rascunho.saldo_troco) setVal('sys-troco', fmtNum(rascunho.saldo_troco));
if (rascunho.saldo_esperado) setVal('f-saldo-esperado', fmtNum(rascunho.saldo_esperado));
if (rascunho.observacoes) { setVal('f-obs', rascunho.observacoes); estado.observacoes = rascunho.observacoes; }
// All tables from Supabase/local
if (rascunho.despesas && Array.isArray(rascunho.despesas)) {
estado.despesas = rascunho.despesas.map(d => ({ desc: d.desc || d.descricao || '', obs: d.obs || '', valor: parseNum(d.valor) || 0 }));
}
@@ -1397,26 +1674,21 @@ document.getElementById('btn-anterior').addEventListener('click', async () => {
estado.cartoes.pix = (rascunho.cartoes.pix || []).map(v => ({ valor: parseNum(v) || 0 }));
}
} else {
// No data for previous day: clear everything
estado.despesas = [];
estado.sangrias = [];
estado.pixcnpj = [];
estado.vales = [];
estado.receber = [];
estado.cancelamentos = [];
estado.cartoes = { credito: [], debito: [], alimentacao: [], pix: [] };
setVal('sys-troco', '');
setVal('f-saldo-esperado', '');
setVal('f-obs', '');
estado.despesas = []; estado.sangrias = []; estado.pixcnpj = [];
estado.vales = []; estado.receber = []; estado.cancelamentos = [];
estado.cartoes = { credito: [], debito: [], alimentacao: [], pix: [] };
setVal('sys-troco', ''); setVal('f-saldo-esperado', ''); setVal('f-obs', '');
}
// Marcar que este é um rascunho carregado (não novo)
estado.sync_status = rascunho ? 'loaded' : 'local';
PRE_FILL_TABLES.forEach(ensureOneRow);
Object.keys(TABLES).forEach(renderTable);
updateTotals();
scheduleSave();
});
document.getElementById('f-obs').addEventListener('input', e => { estado.observacoes = e.target.value; scheduleSave(); });
// ─── Boot ─────────────────────────────────────────────────────────────────