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

This commit is contained in:
root
2026-06-23 16:46:53 +00:00
parent fa1e973f8c
commit 208eede526
2 changed files with 132 additions and 66 deletions
+33 -9
View File
@@ -227,21 +227,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)
{
let file = std::fs::OpenOptions::new()
.write(true) .write(true)
.create(false) .create(false)
.open(path) .open(path);
.map_err(|e| format!("Erro ao abrir {}: {}", path, e))?; 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))?;
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()) 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
))
}
} }
} }
+94 -52
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';
}
} }
} }
} }
@@ -1153,13 +1175,18 @@ 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;
} }
} }
@@ -1243,8 +1270,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 +1282,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');
} }
@@ -1331,47 +1363,62 @@ document.getElementById('btn-limpar').addEventListener('click', () => {
}); });
document.getElementById('btn-anterior').addEventListener('click', async () => { document.getElementById('btn-anterior').addEventListener('click', async () => {
const loja = val('f-loja') || 'União';
// 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); const d = new Date(estado.data); d.setDate(d.getDate() - 1);
const prevDate = d.toISOString().split('T')[0]; 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 +1444,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.pixcnpj = [];
estado.vales = [];
estado.receber = [];
estado.cancelamentos = [];
estado.cartoes = { credito: [], debito: [], alimentacao: [], pix: [] }; estado.cartoes = { credito: [], debito: [], alimentacao: [], pix: [] };
setVal('sys-troco', ''); setVal('sys-troco', ''); setVal('f-saldo-esperado', ''); setVal('f-obs', '');
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 ─────────────────────────────────────────────────────────────────