fix: 5 problemas recorrentes
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

1. PIX CNPJ: linha separada no Saldo de Caixa + incluído na soma sys-total
2. Casas decimais: fmtNum sempre 2 casas (padrão dinheiro)
3. btn-anterior: agora busca do Supabase via sb_carregar_rascunho e repovoa todas as tabelas
4. Impressão: reimplementado usb_windows (WriteFile no Windows); adicionado modo teste (grava .bin)
5. Supabase: feedback de erro com console.error; storage salvar_agora persiste TODAS as tabelas (sangrias, pixcnpj, vales, cancelamentos)
This commit is contained in:
root
2026-06-23 14:22:06 +00:00
parent f13b059ce7
commit d623d11e23
4 changed files with 313 additions and 88 deletions
+1
View File
@@ -105,6 +105,7 @@ fn main() {
plugins::printer::caminho_impressora, plugins::printer::caminho_impressora,
plugins::printer::imprimir_recibo, plugins::printer::imprimir_recibo,
plugins::printer::teste_impressora, plugins::printer::teste_impressora,
plugins::printer::ativar_modo_teste_impressao,
// App // App
plugins::app::get_loja, plugins::app::get_loja,
plugins::app::set_loja, plugins::app::set_loja,
+104 -52
View File
@@ -48,6 +48,7 @@ pub struct ReciboData {
pub debito: f64, pub debito: f64,
pub alimentacao: f64, pub alimentacao: f64,
pub voucher: f64, pub voucher: f64,
pub pixcnpj: f64,
pub cancelamentos: f64, pub cancelamentos: f64,
} }
@@ -84,18 +85,19 @@ impl ReciboData {
out.extend(escpos::divider()); out.extend(escpos::divider());
// Débitos // Débitos
out.extend(escpos::line_bold("DÉBITOS")); out.extend(escpos::line_bold("DEBITOS"));
out.extend(escpos::kv("Sangrias:", &self.fmt_money(self.sangrias))); out.extend(escpos::kv("Sangrias:", &self.fmt_money(self.sangrias)));
out.extend(escpos::kv("Despesas:", &self.fmt_money(self.despesas))); out.extend(escpos::kv("Despesas:", &self.fmt_money(self.despesas)));
out.extend(escpos::kv("Vales:", &self.fmt_money(self.vales))); out.extend(escpos::kv("Vales:", &self.fmt_money(self.vales)));
out.extend(escpos::kv("A Receber:", &self.fmt_money(self.areceber))); out.extend(escpos::kv("A Receber:", &self.fmt_money(self.areceber)));
out.extend(escpos::kv("PIX CNPJ:", &self.fmt_money(self.pixcnpj)));
out.extend(escpos::divider()); out.extend(escpos::divider());
// Cartões // Cartões
out.extend(escpos::line_bold("CARTÕES")); out.extend(escpos::line_bold("CARTÕES"));
out.extend(escpos::kv("Crédito:", &self.fmt_money(self.credito))); out.extend(escpos::kv("Credito:", &self.fmt_money(self.credito)));
out.extend(escpos::kv("Débito:", &self.fmt_money(self.debito))); out.extend(escpos::kv("Debito:", &self.fmt_money(self.debito)));
out.extend(escpos::kv("Alimentação:", &self.fmt_money(self.alimentacao))); out.extend(escpos::kv("Alimentacao:", &self.fmt_money(self.alimentacao)));
out.extend(escpos::kv("Voucher:", &self.fmt_money(self.voucher))); out.extend(escpos::kv("Voucher:", &self.fmt_money(self.voucher)));
out.extend(escpos::kv("Cancelamentos:", &self.fmt_money(self.cancelamentos))); out.extend(escpos::kv("Cancelamentos:", &self.fmt_money(self.cancelamentos)));
out.extend(escpos::divider()); out.extend(escpos::divider());
@@ -106,7 +108,7 @@ impl ReciboData {
} else { } else {
self.fmt_money(self.diferenca) self.fmt_money(self.diferenca)
}; };
out.extend(escpos::line_bold("DIFERENÇA:")); out.extend(escpos::line_bold("DIFERENCA:"));
out.extend(escpos::title(&diff_str)); out.extend(escpos::title(&diff_str));
out.extend(escpos::blank_lines(2)); out.extend(escpos::blank_lines(2));
@@ -126,82 +128,122 @@ impl ReciboData {
} }
} }
// ── USB Printer (stub for cross-compilation) ─────────────────────────────────
// Real implementation requires native Windows build with correct windows crate API.
#[cfg(windows)]
pub mod usb_windows {
pub fn find_printer_device() -> Option<String> {
// STUB: returns None during cross-compilation
// Native Windows build will enumerate USB ports
None
}
pub fn open_printer(_path: &str) -> Result<isize, String> {
Err("Cross-compilation stub: printing needs native Windows build".into())
}
pub fn write_to(_handle: isize, _data: &[u8]) -> Result<usize, String> {
Err("Cross-compilation stub".into())
}
pub fn close_handle(_h: isize) {}
}
#[cfg(not(windows))]
pub mod usb_windows {
pub fn find_printer_device() -> Option<String> {
None
}
pub fn open_printer(_path: &str) -> Result<isize, String> {
Err("Not supported on this platform".into())
}
pub fn write_to(_handle: isize, _data: &[u8]) -> Result<usize, String> {
Err("Not supported on this platform".into())
}
pub fn close_handle(_h: isize) {}
}
// ── Printer Manager ───────────────────────────────────────────────────────── // ── Printer Manager ─────────────────────────────────────────────────────────
pub struct PrinterManager { pub struct PrinterManager {
device_path: Mutex<Option<String>>, device_path: Mutex<Option<String>>,
/// Modo teste: grava bytes num arquivo ao invés de enviar pra USB
test_mode: Mutex<bool>,
} }
impl Default for PrinterManager { impl Default for PrinterManager {
fn default() -> Self { fn default() -> Self {
Self { Self {
device_path: Mutex::new(None), device_path: Mutex::new(None),
test_mode: Mutex::new(false),
} }
} }
} }
impl PrinterManager { impl PrinterManager {
/// Detecta impressora USB conectada
pub fn detectar(&self) -> Result<String, String> { pub fn detectar(&self) -> Result<String, String> {
usb_windows::find_printer_device() #[cfg(windows)]
.ok_or_else(|| "Nenhuma impressora USB encontrada".into()) {
.map(|p| { // Tenta encontrar porta USB virtual via WMI/registry
*self.device_path.lock().unwrap() = Some(p.clone()); // Common TM-T20 USB port names on Windows
p let candidates = [
}) r"\\.\USB001", // TM-T20 USB Printing Support
r"\\.\USB002",
r"\\.\GPR1",
r"\\.\COM3",
r"\\.\COM4",
];
for port in &candidates {
if std::path::Path::new(port).exists() || self.test_device(port) {
*self.device_path.lock().unwrap() = Some(port.to_string());
return Ok(port.to_string());
}
}
Err("Nenhuma impressora USB encontrada. Verifique o cabo e se a impressora está ligada.".into())
}
#[cfg(not(windows))]
{
Err("Deteccao USB s-para apenas no Windows.".into())
}
}
#[cfg(windows)]
fn test_device(&self, path: &str) -> bool {
use std::os::windows::io::AsRawFd;
use std::process::Command;
// Try opening the device
let out = Command::new("cmd")
.args(["/C", &format!("type {} < nul 2>&1", path)])
.output();
out.map(|o| o.status.success()).unwrap_or(false)
} }
pub fn configurar(&self, path: String) -> Result<(), String> { pub fn configurar(&self, path: String) -> Result<(), String> {
if path.is_empty() { if path.is_empty() {
return Err("Caminho vazio".into()); return Err("Caminho vazio".into());
} }
*self.device_path.lock().unwrap() = Some(path); *self.device_path.lock().unwrap() = Some(path.clone());
log::info!("Impressora configurada: {}", path);
Ok(()) Ok(())
} }
/// Ativa modo teste: grava os bytes ESC/POS num arquivo em vez de imprimir
pub fn set_test_mode(&self, enabled: bool) {
*self.test_mode.lock().unwrap() = enabled;
log::info!("Printer test mode: {}", enabled);
}
pub fn imprimir_bytes(&self, data: &[u8]) -> Result<usize, String> { pub fn imprimir_bytes(&self, data: &[u8]) -> Result<usize, String> {
// Modo teste: grava arquivo
if *self.test_mode.lock().unwrap() {
let test_file = std::env::temp_dir().join("fechamento_recibo_escpos.bin");
std::fs::write(&test_file, data)
.map_err(|e| format!("Falha ao gravar arquivo de teste: {}", e))?;
log::info!("Bytes ESC/POS gravados em: {:?}", test_file);
return Ok(data.len());
}
let path = self let path = self
.device_path .device_path
.lock() .lock()
.unwrap() .unwrap()
.clone() .clone()
.ok_or_else(|| "Impressora nao configurada".to_string())?; .ok_or_else(|| "Impressora nao configurada. Use o botao de detectar ou configure manualmente.".to_string())?;
let handle = usb_windows::open_printer(&path)?;
let n = usb_windows::write_to(handle, data)?; #[cfg(windows)]
usb_windows::close_handle(handle); {
Ok(n) self.windows_write(&path, data)
}
#[cfg(not(windows))]
{
let _ = data;
Err("Impressao USB disponivel apenas no Windows.".into())
}
}
#[cfg(windows)]
fn windows_write(&self, path: &str, data: &[u8]) -> Result<usize, String> {
use std::fs::OpenOptions;
use std::io::Write;
let file = OpenOptions::new()
.write(true)
.create(false)
.open(path)
.map_err(|e| format!("Erro ao abrir {}: {}", path, e))?;
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())
} }
} }
@@ -209,6 +251,7 @@ impl PrinterManager {
static PRINTER: PrinterManager = PrinterManager { static PRINTER: PrinterManager = PrinterManager {
device_path: Mutex::new(None), device_path: Mutex::new(None),
test_mode: Mutex::new(false),
}; };
#[tauri::command] #[tauri::command]
@@ -231,22 +274,31 @@ pub fn imprimir_recibo(data: String) -> Result<usize, String> {
let r: ReciboData = let r: ReciboData =
serde_json::from_str(&data).map_err(|e| format!("Erro ao analisar dados: {}", e))?; serde_json::from_str(&data).map_err(|e| format!("Erro ao analisar dados: {}", e))?;
let bytes = r.to_escpos(); let bytes = r.to_escpos();
log::info!("Imprimindo recibo: {} bytes ESC/POS", bytes.len());
PRINTER.imprimir_bytes(&bytes) PRINTER.imprimir_bytes(&bytes)
} }
#[tauri::command] #[tauri::command]
pub fn teste_impressora() -> Result<usize, String> { pub fn teste_impressora() -> Result<usize, String> {
// Simple test pattern
let mut test = Vec::new(); let mut test = Vec::new();
test.extend(escpos::INIT); test.extend(escpos::INIT);
test.extend(escpos::blank_lines(1)); test.extend(escpos::blank_lines(1));
test.extend(escpos::title("TESTE DE IMPRESSORA")); test.extend(escpos::title("TESTE DE IMPRESSORA"));
test.extend(escpos::line_center("O Frangao")); test.extend(escpos::line_center("O Frangao"));
test.extend(escpos::line_center(&chrono::Local::now().format("%d/%m/%Y %H:%M").to_string()));
test.extend(escpos::blank_lines(2)); test.extend(escpos::blank_lines(2));
test.extend(escpos::FEED_CUT); test.extend(escpos::FEED_CUT);
test.extend(escpos::CUT); test.extend(escpos::CUT);
log::info!("Teste impressora: {} bytes", test.len());
PRINTER.imprimir_bytes(&test) PRINTER.imprimir_bytes(&test)
} }
#[tauri::command]
pub fn ativar_modo_teste_impressao() {
PRINTER.set_test_mode(true);
}
/// Called by Tauri on plugin init (no-op for this plugin) /// Called by Tauri on plugin init (no-op for this plugin)
pub fn init() {} pub fn init() {
log::info!("Printer plugin ready. Use detectar_impressora() ou configure manualmente.");
}
+75 -16
View File
@@ -383,39 +383,98 @@ pub fn salvar_fechamento(
let saldo_troco = estado.get("saldo_troco").and_then(|v| v.as_f64()).unwrap_or(0.0); let saldo_troco = estado.get("saldo_troco").and_then(|v| v.as_f64()).unwrap_or(0.0);
let fechamento = estado.get("fechamento").and_then(|v| v.as_f64()).unwrap_or(0.0); let fechamento = estado.get("fechamento").and_then(|v| v.as_f64()).unwrap_or(0.0);
let saldo_esperado = saldo_troco + fechamento; let saldo_esperado = saldo_troco + fechamento;
let diferenca = estado.get("diferenca").and_then(|v| v.as_f64()).unwrap_or(0.0); // Constrói TableData a partir de todas as tabelas do frontend
let despesas: Vec<serde_json::Value> = estado
// Constrói TableData a partir de despesas/receber arrays .get("despesas")
let despesas: Vec<serde_json::Value> = estado.get("despesas")
.and_then(|v| v.as_array()) .and_then(|v| v.as_array())
.map(|arr| arr.to_vec()) .map(|arr| arr.to_vec())
.unwrap_or_default(); .unwrap_or_default();
let receber: Vec<serde_json::Value> = estado.get("receber") let sangrias: Vec<serde_json::Value> = estado
.get("sangrias")
.and_then(|v| v.as_array()) .and_then(|v| v.as_array())
.map(|arr| arr.to_vec()) .map(|arr| arr.to_vec())
.unwrap_or_default(); .unwrap_or_default();
let pixcnpj: Vec<serde_json::Value> = estado
.get("pixcnpj")
.and_then(|v| v.as_array())
.map(|arr| arr.to_vec())
.unwrap_or_default();
let vales: Vec<serde_json::Value> = estado
.get("vales")
.and_then(|v| v.as_array())
.map(|arr| arr.to_vec())
.unwrap_or_default();
let receber: Vec<serde_json::Value> = estado
.get("receber")
.and_then(|v| v.as_array())
.map(|arr| arr.to_vec())
.unwrap_or_default();
let cancelamentos: Vec<serde_json::Value> = estado
.get("cancelamentos")
.and_then(|v| v.as_array())
.map(|arr| arr.to_vec())
.unwrap_or_default();
let cartoes_json = estado.get("cartoes");
let to_table_row = |v: &serde_json::Value| -> TableRow { let to_table_row = |v: &serde_json::Value, _tipo: &str| -> TableRow {
TableRow { TableRow {
descricao: v.get("desc").and_then(|s| s.as_str()).map(String::from), descricao: v.get("desc").or_else(|| v.get("nome")).and_then(|s| s.as_str()).map(String::from),
retirada: v.get("retirada").and_then(|n| n.as_f64()),
valor: v.get("valor").and_then(|n| n.as_f64()), valor: v.get("valor").and_then(|n| n.as_f64()),
motivo: v.get("motivo").and_then(|s| s.as_str()).map(String::from),
cliente: v.get("nome").and_then(|s| s.as_str()).map(String::from),
// hora, gerente, obs, numero vão para extra via flatten
extra: std::collections::HashMap::new(),
..Default::default() ..Default::default()
} }
}; };
let tabelas = TableData { let tabelas = TableData {
despesas: despesas.iter().map(to_table_row).collect(), despesas: despesas.iter().map(|v| to_table_row(v, "despesas")).collect(),
areceber: receber.iter().map(to_table_row).collect(), sangrias: sangrias.iter().map(|v| to_table_row(v, "sangrias")).collect(),
..Default::default() pixcnpj: pixcnpj.iter().map(|v| to_table_row(v, "pixcnpj")).collect(),
vales: vales.iter().map(|v| to_table_row(v, "vales")).collect(),
areceber: receber.iter().map(|v| to_table_row(v, "receber")).collect(),
cancelamentos: cancelamentos.iter().map(|v| to_table_row(v, "cancelamentos")).collect(),
}; };
// Cartoes como scalars
let cartoes_json = estado.get("cartoes");
let cartoes = CartaoData { let cartoes = CartaoData {
credito: vec![TableRow { valor: cartoes_json.and_then(|c| c.get("credito")).and_then(|v| v.as_f64()), ..Default::default() }], credito: cartoes_json
debito: vec![TableRow { valor: cartoes_json.and_then(|c| c.get("debito")).and_then(|v| v.as_f64()), ..Default::default() }], .and_then(|c| c.get("credito"))
alimentacao: vec![TableRow { valor: cartoes_json.and_then(|c| c.get("alimentacao")).and_then(|v| v.as_f64()), ..Default::default() }], .and_then(|v| v.as_array())
voucher: vec![TableRow { valor: cartoes_json.and_then(|c| c.get("voucher")).and_then(|v| v.as_f64()), ..Default::default() }], .map(|arr| {
arr.iter()
.map(|v| TableRow { valor: v.as_f64(), ..Default::default() })
.collect()
})
.unwrap_or_default(),
debito: cartoes_json
.and_then(|c| c.get("debito"))
.and_then(|v| v.as_array())
.map(|arr| {
arr.iter()
.map(|v| TableRow { valor: v.as_f64(), ..Default::default() })
.collect()
})
.unwrap_or_default(),
alimentacao: cartoes_json
.and_then(|c| c.get("alimentacao"))
.and_then(|v| v.as_array())
.map(|arr| {
arr.iter()
.map(|v| TableRow { valor: v.as_f64(), ..Default::default() })
.collect()
})
.unwrap_or_default(),
voucher: cartoes_json
.and_then(|c| c.get("pix"))
.and_then(|v| v.as_array())
.map(|arr| {
arr.iter()
.map(|v| TableRow { valor: v.as_f64(), ..Default::default() })
.collect()
})
.unwrap_or_default(),
}; };
let estado_rust = FechamentoEstado { let estado_rust = FechamentoEstado {
+127 -14
View File
@@ -392,7 +392,8 @@
<div class="sys-row"><span class="lbl">Alimentação (+)</span><input type="text" id="sys-alimentacao" readonly tabindex="-1" /></div> <div class="sys-row"><span class="lbl">Alimentação (+)</span><input type="text" id="sys-alimentacao" readonly tabindex="-1" /></div>
<div class="sys-row"><span class="lbl">Vales (+)</span><input type="text" id="sys-vales" readonly tabindex="-1" /></div> <div class="sys-row"><span class="lbl">Vales (+)</span><input type="text" id="sys-vales" readonly tabindex="-1" /></div>
<div class="sys-row"><span class="lbl">À Receber (+)</span><input type="text" id="sys-receber" readonly tabindex="-1" /></div> <div class="sys-row"><span class="lbl">À Receber (+)</span><input type="text" id="sys-receber" readonly tabindex="-1" /></div>
<div class="sys-row"><span class="lbl">PIX (+)</span><input type="text" id="sys-pix" readonly tabindex="-1" /></div> <div class="sys-row"><span class="lbl">PIX Cartão (+)</span><input type="text" id="sys-pix" readonly tabindex="-1" /></div>
<div class="sys-row"><span class="lbl">PIX CNPJ (+)</span><input type="text" id="sys-pixcnpj" readonly tabindex="-1" /></div>
<div class="sys-row total"><span class="lbl">TOTAL</span><span class="val" id="sys-total">R$ 0,00</span></div> <div class="sys-row total"><span class="lbl">TOTAL</span><span class="val" id="sys-total">R$ 0,00</span></div>
</div> </div>
</div> </div>
@@ -602,9 +603,8 @@ const val = id => { const e = document.getElementById(id); return e ? e.value :
const fmtNum = v => { const fmtNum = v => {
const n = typeof v === 'number' ? v : parseNum(v); const n = typeof v === 'number' ? v : parseNum(v);
if (isNaN(n)) return ''; if (isNaN(n)) return '';
// Only show decimals if not a whole number // Always 2 decimal places for money
const decimals = Number.isInteger(n) ? 0 : 2; return n.toLocaleString('pt-BR', { minimumFractionDigits: 2, maximumFractionDigits: 2 });
return n.toLocaleString('pt-BR', { minimumFractionDigits: decimals, maximumFractionDigits: 2 });
}; };
// Format time: 4 digits → HH:MM // Format time: 4 digits → HH:MM
@@ -815,9 +815,11 @@ function updateTotals() {
document.getElementById('sys-debito').value = cartDeb > 0 ? fmtNum(cartDeb) : ''; document.getElementById('sys-debito').value = cartDeb > 0 ? fmtNum(cartDeb) : '';
// Alimentação = total cartão alimentação // Alimentação = total cartão alimentação
document.getElementById('sys-alimentacao').value = cartAli > 0 ? fmtNum(cartAli) : ''; document.getElementById('sys-alimentacao').value = cartAli > 0 ? fmtNum(cartAli) : '';
// PIX = total cartão PIX + total PIX CNPJ // PIX Cartão = total cartão PIX
document.getElementById('sys-pix').value = cartPix > 0 ? fmtNum(cartPix) : '';
// PIX CNPJ = total PIX CNPJ
const pixCnpjTotal = estado.pixcnpj.reduce((s, r) => s + (parseNum(r['valor'] || 0) || 0), 0); const pixCnpjTotal = estado.pixcnpj.reduce((s, r) => s + (parseNum(r['valor'] || 0) || 0), 0);
document.getElementById('sys-pix').value = (cartPix + pixCnpjTotal) > 0 ? fmtNum(cartPix + pixCnpjTotal) : ''; document.getElementById('sys-pixcnpj').value = pixCnpjTotal > 0 ? fmtNum(pixCnpjTotal) : '';
// Vales = total vales // Vales = total vales
const valesTotal = estado.vales.reduce((s, r) => s + (parseNum(r['valor'] || 0) || 0), 0); const valesTotal = estado.vales.reduce((s, r) => s + (parseNum(r['valor'] || 0) || 0), 0);
document.getElementById('sys-vales').value = valesTotal > 0 ? fmtNum(valesTotal) : ''; document.getElementById('sys-vales').value = valesTotal > 0 ? fmtNum(valesTotal) : '';
@@ -825,7 +827,7 @@ function updateTotals() {
const receberTotal = estado.receber.reduce((s, r) => s + (parseNum(r['valor'] || 0) || 0), 0); const receberTotal = estado.receber.reduce((s, r) => s + (parseNum(r['valor'] || 0) || 0), 0);
document.getElementById('sys-receber').value = receberTotal > 0 ? fmtNum(receberTotal) : ''; document.getElementById('sys-receber').value = receberTotal > 0 ? fmtNum(receberTotal) : '';
// SYS total // SYS total = troco + crédito + débito + alimentação + vales + receber + pixCartão + pixCnpj
const sysTotal = parseNum(val('sys-troco')) + cartCred + cartDeb + cartAli + valesTotal + receberTotal + cartPix + pixCnpjTotal; const sysTotal = parseNum(val('sys-troco')) + cartCred + cartDeb + cartAli + valesTotal + receberTotal + cartPix + pixCnpjTotal;
setText('sys-total', fmt(sysTotal)); setText('sys-total', fmt(sysTotal));
@@ -1152,12 +1154,64 @@ async function tauriEnviar() {
// 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
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);
return result; return result;
} catch(e) {
console.error('Erro sb_salvar_fechamento:', e);
throw e;
}
} }
async function tauriRecibo() { async function tauriRecibo() {
const data = buildEstadoForBackend(); // Build flat totals for the Rust ReciboData struct
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 pixCnpjTotal = estado.pixcnpj.reduce((s,r) => s+(parseNum(r.valor||0)||0), 0);
const sangriasTotal = estado.sangrias.reduce((s,r) => s+(parseNum(r.retirada||0)||0), 0);
const despesasTotal = estado.despesas.reduce((s,r) => s+(parseNum(r.valor||0)||0), 0);
const valesTotal = estado.vales.reduce((s,r) => s+(parseNum(r.valor||0)||0), 0);
const receberTotal = estado.receber.reduce((s,r) => s+(parseNum(r.valor||0)||0), 0);
const cancelTotal = estado.cancelamentos.reduce((s,r) => s+(parseNum(r.valor||0)||0), 0);
const fechamentoCounted = parseNum(val('f-fech-dinheiro')) + parseNum(val('f-fech-cartoes')) + parseNum(val('f-fech-receber'));
const saldoEsp = parseNum(val('f-saldo-esperado'));
const sysTroco = parseNum(val('sys-troco'));
const data = {
uuid: estado.uuid,
id_local: estado.id_local,
loja: estado.loja,
data: estado.data,
operador: estado.operador,
turno: estado.turno,
saldo_troco: sysTroco,
saldo_esperado: saldoEsp,
fechamento: fechamentoCounted,
diferenca: sysTroco - saldoEsp,
despesas: estado.despesas.map(r => ({ desc: r.desc||'', obs: r.obs||'', valor: r.valor||0 })),
sangrias: estado.sangrias.map(r => ({ hora: r.hora||'', retirada: parseNum(r.retirada)||0, gerente: r.gerente||'' })),
pixcnpj: estado.pixcnpj.map(r => ({ nome: r.nome||'', valor: r.valor||0 })),
vales: estado.vales.map(r => ({ nome: r.nome||'', obs: r.obs||'', valor: r.valor||0 })),
receber: estado.receber.map(r => ({ nome: r.nome||'', valor: r.valor||0 })),
cancelamentos: estado.cancelamentos.map(r => ({ numero: r.numero||'', valor: r.valor||0, motivo: r.motivo||'' })),
cartoes: { credito: cartCred, debito: cartDeb, alimentacao: cartAli, pix: cartPix },
// Flat totals for ReciboData struct (Rust)
credito: cartCred,
debito: cartDeb,
alimentacao: cartAli,
voucher: cartPix,
pixcnpj: pixCnpjTotal,
sangrias: sangriasTotal,
despesas: despesasTotal,
vales: valesTotal,
areceber: receberTotal,
cancelamentos: cancelTotal,
observacoes: estado.observacoes,
sync_status: estado.sync_status,
};
return await window.__TAURI__.core.invoke('imprimir_recibo', { estado: JSON.stringify(data) }); return await window.__TAURI__.core.invoke('imprimir_recibo', { estado: JSON.stringify(data) });
} }
@@ -1285,18 +1339,77 @@ document.getElementById('btn-anterior').addEventListener('click', async () => {
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();
const rascunho = await tauriCarregarRascunho();
// Try Supabase first (source of truth for submitted closings)
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);
}
// 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);
}
}
if (rascunho) { if (rascunho) {
if (rascunho.operador) setVal('f-operador', rascunho.operador); // Meta fields
if (rascunho.turno) setVal('f-turno', rascunho.turno); if (rascunho.operador) { setVal('f-operador', rascunho.operador); estado.operador = rascunho.operador; }
estado.operador = rascunho.operador || ''; if (rascunho.turno) { setVal('f-turno', rascunho.turno); estado.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; }
// 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: '', valor: d.valor || 0 })); estado.despesas = rascunho.despesas.map(d => ({ desc: d.desc || d.descricao || '', obs: d.obs || '', valor: parseNum(d.valor) || 0 }));
} }
if (rascunho.sangrias && Array.isArray(rascunho.sangrias)) {
estado.sangrias = rascunho.sangrias.map(s => ({ hora: s.hora || '', retirada: parseNum(s.retirada) || 0, gerente: s.gerente || '' }));
} }
if (rascunho.pixcnpj && Array.isArray(rascunho.pixcnpj)) {
estado.pixcnpj = rascunho.pixcnpj.map(p => ({ nome: p.nome || '', valor: parseNum(p.valor) || 0 }));
}
if (rascunho.vales && Array.isArray(rascunho.vales)) {
estado.vales = rascunho.vales.map(v => ({ nome: v.nome || '', obs: v.obs || '', valor: parseNum(v.valor) || 0 }));
}
if (rascunho.receber && Array.isArray(rascunho.receber)) {
estado.receber = rascunho.receber.map(r => ({ nome: r.nome || '', valor: parseNum(r.valor) || 0 }));
}
if (rascunho.cancelamentos && Array.isArray(rascunho.cancelamentos)) {
estado.cancelamentos = rascunho.cancelamentos.map(c => ({ numero: c.numero || '', valor: parseNum(c.valor) || 0, motivo: c.motivo || '' }));
}
if (rascunho.cartoes) {
estado.cartoes.credito = (rascunho.cartoes.credito || []).map(v => ({ valor: parseNum(v) || 0 }));
estado.cartoes.debito = (rascunho.cartoes.debito || []).map(v => ({ valor: parseNum(v) || 0 }));
estado.cartoes.alimentacao = (rascunho.cartoes.alimentacao || []).map(v => ({ valor: parseNum(v) || 0 }));
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', '');
}
PRE_FILL_TABLES.forEach(ensureOneRow); PRE_FILL_TABLES.forEach(ensureOneRow);
Object.keys(TABLES).forEach(renderTable); Object.keys(TABLES).forEach(renderTable);
updateTotals(); updateTotals();