diff --git a/src-tauri/src/plugins/printer.rs b/src-tauri/src/plugins/printer.rs index 2cc860c..5f5985b 100644 --- a/src-tauri/src/plugins/printer.rs +++ b/src-tauri/src/plugins/printer.rs @@ -227,21 +227,45 @@ impl PrinterManager { #[cfg(windows)] fn windows_write(&self, path: &str, data: &[u8]) -> Result { - 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 + )) + } } } diff --git a/src/index.html b/src/index.html index c95d06d..0eb4e95 100644 --- a/src/index.html +++ b/src/index.html @@ -405,6 +405,12 @@ + +
+ Diferença + R$ 0,00 + +
@@ -421,14 +427,6 @@ - -
-
- Diferença - R$ 0,00 -
-
-
📝 Observações
@@ -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'; + } } } } @@ -1153,13 +1175,18 @@ 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; } } @@ -1243,8 +1270,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 = `
📅 Data: ${estado.data}   👤 Operador: ${operador}   ⏰ Turno: ${turno}   🏪 Loja: ${loja}

@@ -1252,13 +1282,15 @@ function openConfirmModal() {
📝 Despesas: ${fmt(despesas)}
💰 Vales: ${fmt(vales)}
👥 À Receber: ${fmt(receber)}
-
💳 Crédito: ${fmt(cartCred)}
+
🔵 PIX CNPJ: ${fmt(pixcnpj)}
+
💳 Crédito: ${fmt(cartCred)}
💳 Débito: ${fmt(cartDeb)}
💳 Alimentação: ${fmt(cartAli)}
❌ Cancelamentos: ${fmt(cancelamentos)}
📋 Fechamento: ${fmt(fechamento)}

🎯 Saldo Esperado: ${fmt(saldoEsp)}
+
📐 Diferença: ${fmt(diferenca)}
ID: ${estado.loja}_${estado.data}_${estado.operador}
`; document.getElementById('modal-confirm').classList.add('open'); } @@ -1331,47 +1363,62 @@ 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 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(); - 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 +1444,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 ─────────────────────────────────────────────────────────────────