Compare commits

...

7 Commits

Author SHA1 Message Date
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
9 changed files with 317 additions and 86 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.
BIN
View File
Binary file not shown.
+36 -13
View File
@@ -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
} }
@@ -227,21 +226,45 @@ impl PrinterManager {
#[cfg(windows)] #[cfg(windows)]
fn windows_write(&self, path: &str, data: &[u8]) -> Result<usize, String> { fn windows_write(&self, path: &str, data: &[u8]) -> Result<usize, String> {
use std::fs::OpenOptions;
use std::io::Write; use std::io::Write;
let file = OpenOptions::new() // Tenta via File primeiro (mais simples)
.write(true) {
.create(false) let file = std::fs::OpenOptions::new()
.open(path) .write(true)
.map_err(|e| format!("Erro ao abrir {}: {}", path, e))?; .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; // Fallback: usa API Windows direta via std::process::Command + > arquivo
f.write_all(data) // Isso绕过 problemas de locking do OpenOptions
.map_err(|e| format!("Erro ao escrever: {}", e))?; let tmp = std::env::temp_dir().join("escpos_tmp.bin");
f.flush() std::fs::write(&tmp, data)
.map_err(|e| format!("Erro ao fazer flush: {}", e))?; .map_err(|e| format!("Falha ao criar arquivo temporario: {}", e))?;
Ok(data.len())
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
))
}
} }
} }
+281 -73
View File
@@ -405,6 +405,12 @@
<label>Informe o saldo esperado (contado em caixa)</label> <label>Informe o saldo esperado (contado em caixa)</label>
<input type="text" id="f-saldo-esperado" placeholder="0,00" /> <input type="text" id="f-saldo-esperado" placeholder="0,00" />
</div> </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> </div>
<!-- FECHAMENTO (VALORES CONTADOS) --> <!-- FECHAMENTO (VALORES CONTADOS) -->
@@ -421,14 +427,6 @@
</div> </div>
</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 --> <!-- OBS -->
<div class="section" style="flex:1;display:flex;flex-direction:column"> <div class="section" style="flex:1;display:flex;flex-direction:column">
<div class="section-header">📝 Observações</div> <div class="section-header">📝 Observações</div>
@@ -643,19 +641,23 @@ function makeRow(tblId, rowIndex, rowData) {
if (col === 'valor' || col === 'retirada') td.className = 'num'; if (col === 'valor' || col === 'retirada') td.className = 'num';
const inp = document.createElement('input'); const inp = document.createElement('input');
inp.type = 'text'; 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 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.placeholder = col === 'valor' || col === 'retirada' ? '0,00' : (col === 'hora' ? 'HH:MM' : '');
inp.dataset.tbl = tblId; inp.dataset.tbl = tblId;
inp.dataset.row = rowIndex; inp.dataset.row = rowIndex;
inp.dataset.col = col; inp.dataset.col = col;
// datalist only for fields that should offer suggestions from config
if (col === 'gerente') inp.setAttribute('list', 'dl-gerente'); if (col === 'gerente') inp.setAttribute('list', 'dl-gerente');
if (col === 'desc') inp.setAttribute('list', 'dl-despesa'); 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-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('input', onTableInput);
inp.addEventListener('blur', onCellBlur); inp.addEventListener('blur', onCellBlur);
inp.addEventListener('keydown', onCellKeydown); inp.addEventListener('keydown', onCellKeydown);
@@ -693,6 +695,7 @@ function onTableInput(e) {
if (formatted !== e.target.value) e.target.value = formatted; if (formatted !== e.target.value) e.target.value = formatted;
field[row][col] = formatted; field[row][col] = formatted;
} else { } else {
// text fields: nome, desc, obs, gerente, motivo, numero — keep as string
field[row][col] = e.target.value; field[row][col] = e.target.value;
} }
updateTotals(); updateTotals();
@@ -706,6 +709,7 @@ function onCellBlur(e) {
e.target.value = isNaN(v) || v === 0 ? '' : fmtNum(v); e.target.value = isNaN(v) || v === 0 ? '' : fmtNum(v);
updateTotals(); updateTotals();
} }
// For text cols (hora, nome, desc, obs, gerente, motivo, numero) — keep as-is
} }
function onCellKeydown(e) { function onCellKeydown(e) {
@@ -739,8 +743,9 @@ function focusInput(tblId, rowIndex, col) {
function addRowAndFocus(tblId, focusCol) { function addRowAndFocus(tblId, focusCol) {
const cfg = TABLES[tblId]; const cfg = TABLES[tblId];
const field = getField(tblId); const field = getField(tblId);
const numericCols = ['valor', 'retirada'];
const empty = {}; 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); field.push(empty);
renderTable(tblId); renderTable(tblId);
focusInput(tblId, field.length - 1, focusCol); focusInput(tblId, field.length - 1, focusCol);
@@ -849,16 +854,33 @@ function updateTotals() {
const saldoCaixa = sysTotal; const saldoCaixa = sysTotal;
const diff = saldoCaixa - parseNum(val('f-saldo-esperado')); const diff = saldoCaixa - parseNum(val('f-saldo-esperado'));
const difEl = document.getElementById('diferenca'); const difEl = document.getElementById('diferenca');
const badgeEl = document.getElementById('dif-badge');
if (difEl) { if (difEl) {
difEl.textContent = fmt(diff); difEl.textContent = fmt(diff);
if (diff === 0) { if (diff === 0) {
difEl.className = 'dif-value zero'; 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) { } 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) { } else if (diff > 0 && diff <= 3) {
difEl.className = 'dif-value zero'; // OK + até 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 { } 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]; const today = new Date().toISOString().split('T')[0];
setVal('f-data', today); setVal('f-data', today);
estado.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(); syncHeader();
PRE_FILL_TABLES.forEach(ensureOneRow); PRE_FILL_TABLES.forEach(ensureOneRow);
Object.keys(TABLES).forEach(renderTable); Object.keys(TABLES).forEach(renderTable);
@@ -1153,17 +1177,154 @@ async function tauriEnviar() {
const data = buildEstadoForBackend(); const data = buildEstadoForBackend();
// 1. Salva local // 1. Salva local
await window.__TAURI__.core.invoke('salvar_fechamento', { estado: data }).catch(e => console.warn('local save err:', e)); 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 { try {
const result = await window.__TAURI__.core.invoke('sb_salvar_fechamento', { estado: data }); 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; return result;
} catch(e) { } 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; 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 = '';
// 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);
@@ -1179,7 +1340,7 @@ async function tauriRecibo() {
const fechamentoCounted = parseNum(val('f-fech-dinheiro')) + parseNum(val('f-fech-cartoes')) + parseNum(val('f-fech-receber')); 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 saldoEsp = parseNum(val('f-saldo-esperado'));
const sysTroco = parseNum(val('sys-troco')); const sysTroco = parseNum(val('sys-troco'));
const fechamentoCounted = parseNum(val('f-fech-dinheiro')) + parseNum(val('f-fech-cartoes')) + parseNum(val('f-fech-receber'));
const data = { const data = {
uuid: estado.uuid, uuid: estado.uuid,
id_local: estado.id_local, id_local: estado.id_local,
@@ -1190,7 +1351,7 @@ async function tauriRecibo() {
saldo_troco: sysTroco, saldo_troco: sysTroco,
saldo_esperado: saldoEsp, saldo_esperado: saldoEsp,
fechamento: fechamentoCounted, fechamento: fechamentoCounted,
diferenca: sysTroco - saldoEsp, diferenca: fechamentoCounted - saldoEsp, // CORRIGIDO: diferenca = fechamento - esperado
despesas: estado.despesas.map(r => ({ desc: r.desc||'', obs: r.obs||'', valor: r.valor||0 })), 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||'' })), 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 })), pixcnpj: estado.pixcnpj.map(r => ({ nome: r.nome||'', valor: r.valor||0 })),
@@ -1212,7 +1373,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() {
@@ -1243,8 +1412,11 @@ function openConfirmModal() {
const vales = estado.vales.reduce((s,r) => s+(parseNum(r.valor||0)||0), 0); 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 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 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 fechamento = parseNum(val('f-fech-dinheiro')) + parseNum(val('f-fech-cartoes')) + parseNum(val('f-fech-receber'));
const saldoEsp = parseNum(val('f-saldo-esperado')); 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 = 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> `<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"> <hr style="border:none;border-top:1px dashed #b3d9f7;margin:6px 0">
@@ -1252,13 +1424,15 @@ function openConfirmModal() {
<div>📝 Despesas: <strong>${fmt(despesas)}</strong></div> <div>📝 Despesas: <strong>${fmt(despesas)}</strong></div>
<div>💰 Vales: <strong>${fmt(vales)}</strong></div> <div>💰 Vales: <strong>${fmt(vales)}</strong></div>
<div>👥 À Receber: <strong>${fmt(receber)}</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>💳 Débito: <strong>${fmt(cartDeb)}</strong></div>
<div>💳 Alimentação: <strong>${fmt(cartAli)}</strong></div> <div>💳 Alimentação: <strong>${fmt(cartAli)}</strong></div>
<div>❌ Cancelamentos: <strong>${fmt(cancelamentos)}</strong></div> <div>❌ Cancelamentos: <strong>${fmt(cancelamentos)}</strong></div>
<div>📋 Fechamento: <strong>${fmt(fechamento)}</strong></div> <div>📋 Fechamento: <strong>${fmt(fechamento)}</strong></div>
<hr style="border:none;border-top:1px dashed #b3d9f7;margin:6px 0"> <hr style="border:none;border-top:1px dashed #b3d9f7;margin:6px 0">
<div>🎯 Saldo Esperado: <strong>${fmt(saldoEsp)}</strong></div> <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>`; <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'); document.getElementById('modal-confirm').classList.add('open');
} }
@@ -1271,19 +1445,33 @@ 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'); const cartCred = estado.cartoes.credito.reduce((s,r) => s+(r.valor||0), 0);
btn.disabled = true; const cartDeb = estado.cartoes.debito.reduce((s,r) => s+(r.valor||0), 0);
try { const cartAli = estado.cartoes.alimentacao.reduce((s,r) => s+(r.valor||0), 0);
await tauriRecibo(); const cartPix = estado.cartoes.pix.reduce((s,r) => s+(r.valor||0), 0);
alert('✅ Impressão enviada!'); const cancelTotal = estado.cancelamentos.reduce((s,r) => s+(parseNum(r.valor||0)||0), 0);
} catch(e) { const fechamentoCounted = parseNum(val('f-fech-dinheiro')) + parseNum(val('f-fech-cartoes')) + parseNum(val('f-fech-receber'));
alert('Erro ao imprimir: ' + (e.message || e)); const saldoEsp = parseNum(val('f-saldo-esperado'));
} finally { const data = {
btn.disabled = false; 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,
cartoes: { credito: estado.cartoes.credito, debito: estado.cartoes.debito, alimentacao: estado.cartoes.alimentacao, pix: estado.cartoes.pix },
observacoes: estado.observacoes,
};
openPrintPreview(data);
}); });
// Confirm send // Confirm send
@@ -1296,10 +1484,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';
@@ -1315,10 +1510,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;
} }
@@ -1331,47 +1529,62 @@ document.getElementById('btn-limpar').addEventListener('click', () => {
}); });
document.getElementById('btn-anterior').addEventListener('click', async () => { document.getElementById('btn-anterior').addEventListener('click', async () => {
const d = new Date(estado.data); d.setDate(d.getDate() - 1); const loja = val('f-loja') || 'União';
const prevDate = d.toISOString().split('T')[0];
// Carrega lista de datas recentes do Supabase
let datas = [];
try {
const lista = await window.__TAURI__.core.invoke('sb_listar_recentes', { loja, limite: 20 });
if (lista && lista.length > 0) {
// Extrai datas únicas e já ordenadas
datas = [...new Set(lista.map(l => l.data).filter(Boolean))].sort().reverse();
}
} catch(e) {
console.warn('sb_listar_recentes falhou:', e);
}
let selectedDate = null;
if (datas.length > 0) {
// Mostra seletor com as datas disponíveis
const msg = datas.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 < datas.length) selectedDate = datas[idx];
}
}
// Se não escolheu data, usa dia anterior automático
if (!selectedDate) {
const d = new Date(estado.data); d.setDate(d.getDate() - 1);
selectedDate = d.toISOString().split('T')[0];
}
await tauriSaveLocal(); await tauriSaveLocal();
setVal('f-data', prevDate); setVal('f-data', selectedDate);
estado.data = prevDate; estado.data = selectedDate;
estado.uuid = crypto.randomUUID ? crypto.randomUUID() : Date.now().toString(); estado.uuid = crypto.randomUUID ? crypto.randomUUID() : Date.now().toString();
estado.id_local = null; estado.id_local = null;
syncHeader(); syncHeader();
// Try Supabase first (source of truth for submitted closings) // Tenta Supabase primeiro, senão local
let rascunho = null; let rascunho = null;
try { try {
rascunho = await window.__TAURI__.core.invoke('sb_carregar_rascunho', { rascunho = await window.__TAURI__.core.invoke('sb_carregar_rascunho', { loja, data: selectedDate });
loja: val('f-loja'), } catch(e) { console.warn('sb_carregar_rascunho falhou:', e); }
data: prevDate
});
} catch(e) {
console.warn('sb_carregar_rascunho falhou, tentando local:', e);
}
// Fallback: local SQLite
if (!rascunho) { if (!rascunho) {
try { try {
rascunho = await window.__TAURI__.core.invoke('carregar_rascunho', { rascunho = await window.__TAURI__.core.invoke('carregar_rascunho', { loja, data: selectedDate });
loja: val('f-loja'), } catch(e) { console.warn('carregar_rascunho local falhou:', e); }
data: prevDate
});
} catch(e) {
console.warn('carregar_rascunho local falhou:', e);
}
} }
if (rascunho) { if (rascunho) {
// Meta fields
if (rascunho.operador) { setVal('f-operador', rascunho.operador); estado.operador = rascunho.operador; } 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.turno) { setVal('f-turno', rascunho.turno); estado.turno = rascunho.turno; }
if (rascunho.saldo_troco) setVal('sys-troco', fmtNum(rascunho.saldo_troco)); 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.saldo_esperado) setVal('f-saldo-esperado', fmtNum(rascunho.saldo_esperado));
if (rascunho.observacoes) { setVal('f-obs', rascunho.observacoes); estado.observacoes = rascunho.observacoes; } if (rascunho.observacoes) { setVal('f-obs', rascunho.observacoes); estado.observacoes = rascunho.observacoes; }
// All tables from Supabase/local
if (rascunho.despesas && Array.isArray(rascunho.despesas)) { 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 })); estado.despesas = rascunho.despesas.map(d => ({ desc: d.desc || d.descricao || '', obs: d.obs || '', valor: parseNum(d.valor) || 0 }));
} }
@@ -1397,26 +1610,21 @@ document.getElementById('btn-anterior').addEventListener('click', async () => {
estado.cartoes.pix = (rascunho.cartoes.pix || []).map(v => ({ valor: parseNum(v) || 0 })); estado.cartoes.pix = (rascunho.cartoes.pix || []).map(v => ({ valor: parseNum(v) || 0 }));
} }
} else { } else {
// No data for previous day: clear everything estado.despesas = []; estado.sangrias = []; estado.pixcnpj = [];
estado.despesas = []; estado.vales = []; estado.receber = []; estado.cancelamentos = [];
estado.sangrias = []; estado.cartoes = { credito: [], debito: [], alimentacao: [], pix: [] };
estado.pixcnpj = []; setVal('sys-troco', ''); setVal('f-saldo-esperado', ''); setVal('f-obs', '');
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); PRE_FILL_TABLES.forEach(ensureOneRow);
Object.keys(TABLES).forEach(renderTable); Object.keys(TABLES).forEach(renderTable);
updateTotals(); updateTotals();
scheduleSave(); scheduleSave();
}); });
document.getElementById('f-obs').addEventListener('input', e => { estado.observacoes = e.target.value; scheduleSave(); }); document.getElementById('f-obs').addEventListener('input', e => { estado.observacoes = e.target.value; scheduleSave(); });
// ─── Boot ───────────────────────────────────────────────────────────────── // ─── Boot ─────────────────────────────────────────────────────────────────