Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 2253d45d00 | |||
| 60e15dbed3 | |||
| 53767df80c | |||
| 208eede526 |
Binary file not shown.
Binary file not shown.
Binary file not shown.
Executable
BIN
Binary file not shown.
@@ -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()
|
||||
// Tenta via File primeiro (mais simples)
|
||||
{
|
||||
let file = std::fs::OpenOptions::new()
|
||||
.write(true)
|
||||
.create(false)
|
||||
.open(path)
|
||||
.map_err(|e| format!("Erro ao abrir {}: {}", path, e))?;
|
||||
.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))?;
|
||||
// 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
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+310
-73
@@ -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';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1012,7 +1034,7 @@ const LS_KEY = 'fc_v7';
|
||||
const CFG_KEY = 'fc_cfg';
|
||||
|
||||
function buildEstadoForBackend() {
|
||||
// Monta o objeto exactly as o backend espera (storage.rs → salvar_fechamento)
|
||||
// Monta o objeto que vai para o Supabase — inclui TUDO, todos os detalhes
|
||||
return {
|
||||
uuid: estado.uuid,
|
||||
id_local: estado.id_local,
|
||||
@@ -1020,7 +1042,7 @@ function buildEstadoForBackend() {
|
||||
data: estado.data,
|
||||
operador: estado.operador,
|
||||
turno: estado.turno,
|
||||
saldo_troco: parseNum(val('sys-troco')), // troco do sistema
|
||||
saldo_troco: parseNum(val('sys-troco')),
|
||||
saldo_esperado: parseNum(val('f-saldo-esperado')),
|
||||
fechamento: parseNum(val('f-fech-dinheiro')) + parseNum(val('f-fech-cartoes')) + parseNum(val('f-fech-receber')),
|
||||
diferenca: parseNum(val('f-saldo-esperado')) - (parseNum(val('f-fech-dinheiro')) + parseNum(val('f-fech-cartoes')) + parseNum(val('f-fech-receber'))),
|
||||
@@ -1030,11 +1052,12 @@ function buildEstadoForBackend() {
|
||||
vales: estado.vales.map(r => ({ nome: r.nome || '', obs: r.obs || '', valor: r.valor || 0 })),
|
||||
receber: estado.receber.map(r => ({ nome: r.nome || '', valor: r.valor || 0 })),
|
||||
cancelamentos: estado.cancelamentos.map(r => ({ numero: r.numero || '', valor: r.valor || 0, motivo: r.motivo || '' })),
|
||||
// Cartões com detalhes completos (não só totais)
|
||||
cartoes: {
|
||||
credito: estado.cartoes.credito.map(r => r.valor || 0),
|
||||
debito: estado.cartoes.debito.map(r => r.valor || 0),
|
||||
alimentacao: estado.cartoes.alimentacao.map(r => r.valor || 0),
|
||||
pix: estado.cartoes.pix.map(r => r.valor || 0),
|
||||
credito: estado.cartoes.credito.map(r => ({ valor: r.valor || 0, obs: r.obs || '' })),
|
||||
debito: estado.cartoes.debito.map(r => ({ valor: r.valor || 0, obs: r.obs || '' })),
|
||||
alimentacao: estado.cartoes.alimentacao.map(r => ({ valor: r.valor || 0, obs: r.obs || '' })),
|
||||
pix: estado.cartoes.pix.map(r => ({ valor: r.valor || 0, obs: r.obs || '' })),
|
||||
},
|
||||
observacoes: estado.observacoes,
|
||||
sync_status: estado.sync_status,
|
||||
@@ -1153,17 +1176,187 @@ 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 vSangrias = (data.sangrias||[]).reduce((s,r) => s+(parseFloat(r.retirada)||0), 0);
|
||||
const vDespesas = (data.despesas||[]).reduce((s,r) => s+(parseFloat(r.valor)||0), 0);
|
||||
const vVales = (data.vales||[]).reduce((s,r) => s+(parseFloat(r.valor)||0), 0);
|
||||
const vReceber = (data.receber||[]).reduce((s,r) => s+(parseFloat(r.valor)||0), 0);
|
||||
const vPixCnpj = (data.pixcnpj||[]).reduce((s,r) => s+(parseFloat(r.valor)||0), 0);
|
||||
const vCredito = data.cartoes?.credito?.reduce?.((s,r) => s+(parseFloat(r)||0), 0) || data.credito || 0;
|
||||
const vDebito = data.cartoes?.debito?.reduce?.((s,r) => s+(parseFloat(r)||0), 0) || data.debito || 0;
|
||||
const vAliment = data.cartoes?.alimentacao?.reduce?.((s,r) => s+(parseFloat(r)||0), 0) || data.alimentacao || 0;
|
||||
const vPixCartao = data.cartoes?.pix?.reduce?.((s,r) => s+(parseFloat(r)||0), 0) || 0;
|
||||
const vCancel = (data.cancelamentos||[]).reduce((s,r) => s+(parseFloat(r.valor)||0), 0);
|
||||
// Diferença = Fechamento Total − Saldo Esperado
|
||||
const vFechamento = (data.fechamento || 0);
|
||||
const diferenca = vFechamento - (data.saldo_esperado || 0);
|
||||
const diffStr = diferenca >= 0 ? `+${fmt(diferenca)}` : fmt(diferenca);
|
||||
const diffColor = diferenca === 0 ? '#2e7d32' : (diferenca > 0 ? '#e65100' : '#c0272d');
|
||||
|
||||
const fmtRow = (label, valor) =>
|
||||
`<div class="row"><span>${label}</span><span>${fmt(valor)}</span></div>`;
|
||||
const fmtDetail = (label, valor, obs) =>
|
||||
`<div class="row sub"><span>${label}</span><span>${fmt(valor)}</span></div>`;
|
||||
|
||||
const mkDet = (arr, fmtFn) => {
|
||||
if (!arr || arr.length === 0) return '';
|
||||
return arr.map(r => {
|
||||
if (!r) return '';
|
||||
const v = fmtFn(r);
|
||||
const nome = r.nome || r.desc || r.hora || r.numero || '';
|
||||
const obs = r.obs || r.motivo || '';
|
||||
return fmtDetail(nome, v, obs);
|
||||
}).join('');
|
||||
};
|
||||
|
||||
return `<!DOCTYPE html>
|
||||
<html><head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Relatório de Fechamento</title>
|
||||
<style>
|
||||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
body { font-family: 'Courier New', monospace; font-size: 12px; width: 80mm; margin: 0 auto; padding: 6px; }
|
||||
.header { text-align: center; margin-bottom: 6px; }
|
||||
.header h1 { font-size: 16px; }
|
||||
.header h2 { font-size: 12px; font-weight: normal; }
|
||||
hr { border: none; border-top: 1px dashed #000; margin: 4px 0; }
|
||||
.row { display: flex; justify-content: space-between; padding: 1px 0; }
|
||||
.row.sub { padding-left: 8px; font-size: 11px; color: #444; }
|
||||
.row strong { font-weight: bold; }
|
||||
.section-title { font-weight: bold; font-size: 12px; border-top: 1px dashed #000; padding-top: 3px; margin-top: 5px; }
|
||||
.grand-total { font-size: 14px; font-weight: bold; text-align: center; padding: 5px; border: 2px solid #000; margin: 6px 0; }
|
||||
.footer { text-align: center; font-size: 10px; color: #777; margin-top: 6px; }
|
||||
@media print { body { margin: 0; width: 80mm; } }
|
||||
</style>
|
||||
</head><body>
|
||||
<div class="header">
|
||||
<h1>${data.loja || ''}</h1>
|
||||
<h2>Fechamento de Caixa</h2>
|
||||
</div>
|
||||
<hr>
|
||||
<div class="row"><span>DATA:</span><span>${data.data || ''}</span></div>
|
||||
<div class="row"><span>OPERADOR:</span><span>${data.operador || ''}</span></div>
|
||||
<div class="row"><span>TURNO:</span><span>${data.turno || ''}</span></div>
|
||||
|
||||
<hr>
|
||||
<div class="section-title">SANGRIAS</div>
|
||||
${mkDet(data.sangrias, r => parseFloat(r.retirada)||0)}
|
||||
${fmtRow('TOTAL SANGRIAS', vSangrias)}
|
||||
|
||||
<div class="section-title">DESPESAS</div>
|
||||
${mkDet(data.despesas, r => parseFloat(r.valor)||0)}
|
||||
${fmtRow('TOTAL DESPESAS', vDespesas)}
|
||||
|
||||
<div class="section-title">VALES</div>
|
||||
${mkDet(data.vales, r => parseFloat(r.valor)||0)}
|
||||
${fmtRow('TOTAL VALES', vVales)}
|
||||
|
||||
<div class="section-title">A RECEBER</div>
|
||||
${mkDet(data.receber, r => parseFloat(r.valor)||0)}
|
||||
${fmtRow('TOTAL A RECEBER', vReceber)}
|
||||
|
||||
<div class="section-title">PIX</div>
|
||||
<div class="section-title" style="font-size:11px;font-weight:normal">— Cartão —</div>
|
||||
${fmtRow('PIX CARTAO', vPixCartao)}
|
||||
<div class="section-title" style="font-size:11px;font-weight:normal">— CNPJ —</div>
|
||||
${mkDet(data.pixcnpj, r => parseFloat(r.valor)||0)}
|
||||
${fmtRow('TOTAL PIX CNPJ', vPixCnpj)}
|
||||
|
||||
<div class="section-title">CARTOES</div>
|
||||
${fmtRow('CREDITO', vCredito)}
|
||||
${fmtRow('DEBITO', vDebito)}
|
||||
${fmtRow('VOUCHER', vAliment)}
|
||||
|
||||
<div class="section-title">CANCELAMENTOS</div>
|
||||
${mkDet(data.cancelamentos, r => parseFloat(r.valor)||0)}
|
||||
${fmtRow('TOTAL CANCELAMENTOS', vCancel)}
|
||||
|
||||
<hr>
|
||||
${fmtRow('SALDO ESPERADO', data.saldo_esperado || 0)}
|
||||
${fmtRow('FECHAMENTO', vFechamento)}
|
||||
<div class="grand-total" style="color:${diffColor}">DIFERENCA: ${diffStr}</div>
|
||||
<div class="footer">Gerado em ${new Date().toLocaleString('pt-BR')}</div>
|
||||
</body></html>`;
|
||||
}
|
||||
|
||||
function openPrintPreview(data) {
|
||||
// Gera HTML e injeta no body para window.print()
|
||||
const html = buildPrintHTML(data);
|
||||
const style = document.createElement('style');
|
||||
style.id = '_print_style';
|
||||
style.textContent = `
|
||||
@media print {
|
||||
body { width: 80mm; margin: 0 auto; font-family: 'Courier New', monospace; font-size: 13px; }
|
||||
.no-print, .print-actions { display: none !important; }
|
||||
hr { border-top: 1px dashed #000 !important; }
|
||||
}
|
||||
.print-actions { position: fixed; top: 10px; right: 10px; z-index: 9999; }
|
||||
.print-actions button { padding: 8px 16px; cursor: pointer; background: #2563eb; color: #fff; border: none; border-radius: 4px; font-size: 14px; margin-left: 8px; }
|
||||
.print-actions button:hover { background: #1d4ed8; }
|
||||
.print-actions .btn-back { background: #6b7280; }
|
||||
.print-actions .btn-back:hover { background: #4b5563; }
|
||||
`;
|
||||
const actions = document.createElement('div');
|
||||
actions.className = 'print-actions no-print';
|
||||
actions.innerHTML =
|
||||
'<button class="btn-back" id="_btn_back_print">← Voltar ao App</button>' +
|
||||
'<button onclick="window.print()">🖨️ Imprimir (Ctrl+P)</button>';
|
||||
|
||||
// Guarda originais
|
||||
const originalBody = document.body.innerHTML;
|
||||
const originalOnAfterPrint = window.onafterprint;
|
||||
|
||||
// Botão voltar restaura sem reload
|
||||
document.addEventListener('click', function onBackClick(e) {
|
||||
if (e.target.id === '_btn_back_print') {
|
||||
document.body.innerHTML = originalBody;
|
||||
const oldStyle = document.getElementById('_print_style');
|
||||
if (oldStyle) oldStyle.remove();
|
||||
document.removeEventListener('click', onBackClick);
|
||||
window.onafterprint = originalOnAfterPrint;
|
||||
// Re-init app state — mínimo viable restore
|
||||
initApp();
|
||||
}
|
||||
});
|
||||
|
||||
document.body.innerHTML = html;
|
||||
document.head.appendChild(style);
|
||||
document.body.insertBefore(actions, document.body.firstChild);
|
||||
|
||||
window.onafterprint = () => {
|
||||
document.body.innerHTML = originalBody;
|
||||
const oldStyle = document.getElementById('_print_style');
|
||||
if (oldStyle) oldStyle.remove();
|
||||
window.onafterprint = originalOnAfterPrint;
|
||||
document.removeEventListener('click', onBackClick);
|
||||
initApp();
|
||||
};
|
||||
}
|
||||
|
||||
function triggerWindowPrint() {
|
||||
// Chama window.print() na janela pai (o próprio app)
|
||||
window.print();
|
||||
}
|
||||
|
||||
// ─── Recibo 80mm via Tauri (ESC/POS USB) ─────────────────────────────────
|
||||
async function tauriRecibo() {
|
||||
// Build flat totals for the Rust ReciboData struct
|
||||
const cartCred = estado.cartoes.credito.reduce((s,r) => s+(r.valor||0), 0);
|
||||
@@ -1212,7 +1405,15 @@ async function tauriRecibo() {
|
||||
observacoes: estado.observacoes,
|
||||
sync_status: estado.sync_status,
|
||||
};
|
||||
|
||||
// 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 +1444,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} <strong>👤 Operador:</strong> ${operador} <strong>⏰ Turno:</strong> ${turno} <strong>🏪 Loja:</strong> ${loja}</div>
|
||||
<hr style="border:none;border-top:1px dashed #b3d9f7;margin:6px 0">
|
||||
@@ -1252,13 +1456,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 +1477,30 @@ document.getElementById('confirm-close').addEventListener('click', closeConfirmM
|
||||
document.getElementById('confirm-cancel').addEventListener('click', closeConfirmModal);
|
||||
document.getElementById('modal-confirm').addEventListener('click', e => { if (e.target.id === 'modal-confirm') closeConfirmModal(); });
|
||||
|
||||
// Print from confirm modal
|
||||
// Print from confirm modal — abre preview no navegador
|
||||
document.getElementById('confirm-print').addEventListener('click', async () => {
|
||||
closeConfirmModal();
|
||||
const btn = document.getElementById('btn-recibo');
|
||||
btn.disabled = true;
|
||||
try {
|
||||
await tauriRecibo();
|
||||
alert('✅ Impressão enviada!');
|
||||
} catch(e) {
|
||||
alert('Erro ao imprimir: ' + (e.message || e));
|
||||
} finally {
|
||||
btn.disabled = false;
|
||||
}
|
||||
// Reconstrói os mesmos dados do tauriRecibo e abre preview
|
||||
const cartCred = estado.cartoes.credito.reduce((s,r) => s+(r.valor||0), 0);
|
||||
const cartDeb = estado.cartoes.debito.reduce((s,r) => s+(r.valor||0), 0);
|
||||
const cartAli = estado.cartoes.alimentacao.reduce((s,r) => s+(r.valor||0), 0);
|
||||
const cartPix = estado.cartoes.pix.reduce((s,r) => s+(r.valor||0), 0);
|
||||
const data = {
|
||||
loja: estado.loja, data: estado.data, operador: estado.operador, turno: estado.turno,
|
||||
saldo_troco: parseNum(val('sys-troco')) || 0,
|
||||
saldo_esperado: parseNum(val('f-saldo-esperado')) || 0,
|
||||
fechamento: parseNum(val('f-fech-dinheiro')) + parseNum(val('f-fech-cartoes')) + parseNum(val('f-fech-receber')),
|
||||
despesas: estado.despesas.map(r => ({ desc: r.desc||'', obs: r.obs||'', valor: r.valor||0 })),
|
||||
sangrias: estado.sangrias.map(r => ({ hora: r.hora||'', retirada: parseNum(r.retirada)||0, gerente: r.gerente||'' })),
|
||||
pixcnpj: estado.pixcnpj.map(r => ({ nome: r.nome||'', valor: r.valor||0 })),
|
||||
vales: estado.vales.map(r => ({ nome: r.nome||'', obs: r.obs||'', valor: r.valor||0 })),
|
||||
receber: estado.receber.map(r => ({ nome: r.nome||'', valor: r.valor||0 })),
|
||||
cancelamentos: estado.cancelamentos.map(r => ({ numero: r.numero||'', valor: r.valor||0, motivo: r.motivo||'' })),
|
||||
credito: cartCred, debito: cartDeb, alimentacao: cartAli,
|
||||
cartoes: { pix: cartPix },
|
||||
observacoes: estado.observacoes,
|
||||
};
|
||||
openPrintPreview(data);
|
||||
});
|
||||
|
||||
// Confirm send
|
||||
@@ -1296,10 +1513,17 @@ document.getElementById('confirm-send').addEventListener('click', async () => {
|
||||
const r = await tauriEnviar();
|
||||
estado.sync_status = 'synced';
|
||||
saveState();
|
||||
alert('✅ Enviado com sucesso!');
|
||||
alert('✅ Enviado com sucesso!\n\n📋 Tabela: fechamentos_web\n🌐 Servidor: supabase.ofrangao.com.br\n\nAbra o Supabase Studio para verificar.');
|
||||
} catch(e) {
|
||||
await tauriSaveLocal();
|
||||
alert('Erro ao enviar: ' + (e.message || e) + '\n(Dados salvos localmente)');
|
||||
const msg = e?.message || e?.toString?.() || String(e);
|
||||
alert(
|
||||
'⚠️ Erro ao enviar para o Supabase.\n\n' +
|
||||
'📋 Tabela: fechamentos_web\n' +
|
||||
'🌐 Servidor: supabase.ofrangao.com.br\n\n' +
|
||||
'O servidor pode estar offline. Dados salvos localmente.\n\n' +
|
||||
'Erro: ' + msg
|
||||
);
|
||||
} finally {
|
||||
btn.disabled = false;
|
||||
btn.textContent = '💾 Enviar Fechamento';
|
||||
@@ -1315,10 +1539,13 @@ document.getElementById('btn-recibo').addEventListener('click', async () => {
|
||||
const btn = document.getElementById('btn-recibo');
|
||||
btn.disabled = true;
|
||||
try {
|
||||
// Tenta ESC/POS primeiro; se falhar, abre preview no navegador
|
||||
await tauriRecibo();
|
||||
alert('✅ Impressão enviada!');
|
||||
// Se chegou aqui sem erro, ESC/POS funcionou
|
||||
} catch(e) {
|
||||
alert('Erro ao imprimir: ' + (e.message || e));
|
||||
// ESC/POS falhou — openPrintPreview já foi chamado dentro de tauriRecibo
|
||||
// Só mostra alerta se nem o preview abriu
|
||||
console.warn('Recibo:', e);
|
||||
} finally {
|
||||
btn.disabled = false;
|
||||
}
|
||||
@@ -1331,47 +1558,62 @@ document.getElementById('btn-limpar').addEventListener('click', () => {
|
||||
});
|
||||
|
||||
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 prevDate = d.toISOString().split('T')[0];
|
||||
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, 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);
|
||||
}
|
||||
rascunho = await window.__TAURI__.core.invoke('sb_carregar_rascunho', { loja, data: selectedDate });
|
||||
} catch(e) { console.warn('sb_carregar_rascunho 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('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 +1639,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.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', '');
|
||||
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 ─────────────────────────────────────────────────────────────────
|
||||
|
||||
Reference in New Issue
Block a user