Compare commits

...

13 Commits

Author SHA1 Message Date
root 7e57d30863 v1.2.7: conserta 4 bugs — impressao, enviar, dia anterior, localStorage
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:38:00 +00:00
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
root fa1e973f8c fix: remove AsRawFd import (unix-only, não existe no Windows)
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:13:06 +00:00
root d623d11e23 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)
2026-06-23 14:22:06 +00:00
root f13b059ce7 fix: header sticky; makeRow formats valor cells with fmtNum; fmtNum shows .00 only when needed
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 12:56:57 +00:00
root 8660ac4755 fix: fmtNum shows .00 only when needed; Diferença=SaldoCaixa-SaldoEsperado; sys-total already includes pixCnpj
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 12:55:30 +00:00
root 72c534a813 fix: parseNum receives numbers not strings (was treating JS numbers' dot as thousand sep); updateTotals/confirm-summary use parseNum consistently
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 01:51:37 +00:00
12 changed files with 679 additions and 152 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.
Binary file not shown.
BIN
View File
Binary file not shown.
+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,
+126 -53
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));
@@ -120,88 +122,149 @@ impl ReciboData {
// Corta papel // Corta papel
out.extend(escpos::FEED_CUT); out.extend(escpos::FEED_CUT);
out.extend(escpos::CUT);
out out
} }
} }
// ── 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::process::Command;
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::io::Write;
// 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());
}
}
}
// 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
))
}
} }
} }
@@ -209,6 +272,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 +295,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 {
+477 -83
View File
@@ -32,6 +32,7 @@
color: #fff; padding: 8px 18px; color: #fff; padding: 8px 18px;
display: flex; align-items: center; justify-content: space-between; display: flex; align-items: center; justify-content: space-between;
flex-shrink: 0; gap: 14px; flex-shrink: 0; gap: 14px;
position: sticky; top: 0; z-index: 100;
} }
.header-left { display: flex; align-items: center; gap: 14px; flex: 1; } .header-left { display: flex; align-items: center; gap: 14px; flex: 1; }
.header-title { font-size: 15px; font-weight: 700; white-space: nowrap; } .header-title { font-size: 15px; font-weight: 700; white-space: nowrap; }
@@ -391,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>
@@ -403,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) -->
@@ -419,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>
@@ -577,28 +577,32 @@ let estado = {
// ─── Helpers ────────────────────────────────────────────────────────────── // ─── Helpers ──────────────────────────────────────────────────────────────
const fmt = v => 'R$ ' + (parseFloat(v) || 0).toLocaleString('pt-BR', { minimumFractionDigits: 2 }); const fmt = v => 'R$ ' + (parseFloat(v) || 0).toLocaleString('pt-BR', { minimumFractionDigits: 2 });
const parseNum = v => { const parseNum = v => {
const s = String(v || '0'); // Already a number? Return directly (don't treat '.' as thousand separator)
// Handle empty or whitespace if (typeof v === 'number') return isNaN(v) ? 0 : v;
if (!s.trim()) return 0; const s = String(v || '0').trim();
if (!s) return 0;
// Brazilian format: 1.234,56 → 1234.56 // Brazilian format: 1.234,56 → 1234.56
// Strategy: remove thousand dots, replace decimal comma with dot // Strategy: if string has comma, treat as BRL (decimal comma)
// "10,55" → "1055,00" (wrong) → fix: split on comma first // If no comma but has dot, treat as US (decimal dot)
// "1.055,00" → parts=["1.055","00"] → intPart="1055", decPart="00" → 1055.00
const lastComma = s.lastIndexOf(','); const lastComma = s.lastIndexOf(',');
if (lastComma === -1) { const lastDot = s.lastIndexOf('.');
// No comma: just remove thousand separators (dots) const hasCommaDecimal = lastComma > lastDot;
return parseFloat(s.replace(/,/g, '').replace(/\./g, '')) || 0; if (hasCommaDecimal) {
const intPart = s.slice(0, lastComma).replace(/\./g, '').replace(/,/g, '');
const decPart = s.slice(lastComma + 1);
return parseFloat(intPart + '.' + decPart) || 0;
} }
const intPart = s.slice(0, lastComma).replace(/\./g, '').replace(/,/g, ''); // No comma: US-style number, just strip any dots (thousand sep)
const decPart = s.slice(lastComma + 1); return parseFloat(s.replace(/,/g, '').replace(/\./g, '')) || 0;
return parseFloat(intPart + '.' + decPart) || 0;
}; };
const setText = (id, t) => { const e = document.getElementById(id); if (e) e.textContent = t; }; const setText = (id, t) => { const e = document.getElementById(id); if (e) e.textContent = t; };
const setVal = (id, v) => { const e = document.getElementById(id); if (e) e.value = v; }; const setVal = (id, v) => { const e = document.getElementById(id); if (e) e.value = v; };
const val = id => { const e = document.getElementById(id); return e ? e.value : ''; }; 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);
return isNaN(n) ? '' : n.toLocaleString('pt-BR', { minimumFractionDigits: 2 }); if (isNaN(n)) return '';
// Always 2 decimal places for money
return n.toLocaleString('pt-BR', { minimumFractionDigits: 2, maximumFractionDigits: 2 });
}; };
// Format time: 4 digits → HH:MM // Format time: 4 digits → HH:MM
@@ -637,17 +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';
inp.value = rowData[col] !== undefined ? (rowData[col] === 0 ? '' : rowData[col]) : ''; // 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
? (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);
@@ -685,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();
@@ -698,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) {
@@ -731,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);
@@ -773,7 +786,7 @@ document.querySelectorAll('.add-btn[data-tbl]').forEach(btn => {
// ─── Totals ──────────────────────────────────────────────────────────────── // ─── Totals ────────────────────────────────────────────────────────────────
function updateTotals() { function updateTotals() {
// ── Sangrias ── // ── Sangrias ──
const sangriasTotal = estado.sangrias.reduce((s, r) => s + (parseFloat(r['retirada'] || 0) || 0), 0); const sangriasTotal = estado.sangrias.reduce((s, r) => s + (parseNum(r['retirada'] || 0) || 0), 0);
setText('total-sangrias', fmt(sangriasTotal)); setText('total-sangrias', fmt(sangriasTotal));
// ── Tabelas de valor ── // ── Tabelas de valor ──
@@ -782,7 +795,7 @@ function updateTotals() {
'tbl-receber':'receber', 'tbl-cancelamentos':'cancelamentos', 'tbl-receber':'receber', 'tbl-cancelamentos':'cancelamentos',
}; };
Object.entries(map).forEach(([tblId, fieldName]) => { Object.entries(map).forEach(([tblId, fieldName]) => {
const total = estado[fieldName].reduce((s, r) => s + (parseFloat(r['valor'] || 0) || 0), 0); const total = estado[fieldName].reduce((s, r) => s + (parseNum(r['valor'] || 0) || 0), 0);
setText(tblId.replace('tbl-','total-'), fmt(total)); setText(tblId.replace('tbl-','total-'), fmt(total));
}); });
@@ -807,17 +820,19 @@ 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
const pixCnpjTotal = estado.pixcnpj.reduce((s, r) => s + (parseFloat(r['valor'] || 0) || 0), 0); document.getElementById('sys-pix').value = cartPix > 0 ? fmtNum(cartPix) : '';
document.getElementById('sys-pix').value = (cartPix + pixCnpjTotal) > 0 ? fmtNum(cartPix + pixCnpjTotal) : ''; // PIX CNPJ = total PIX CNPJ
const pixCnpjTotal = estado.pixcnpj.reduce((s, r) => s + (parseNum(r['valor'] || 0) || 0), 0);
document.getElementById('sys-pixcnpj').value = pixCnpjTotal > 0 ? fmtNum(pixCnpjTotal) : '';
// Vales = total vales // Vales = total vales
const valesTotal = estado.vales.reduce((s, r) => s + (parseFloat(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) : '';
// À Receber = total receber // À Receber = total receber
const receberTotal = estado.receber.reduce((s, r) => s + (parseFloat(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));
@@ -835,19 +850,37 @@ function updateTotals() {
const fr = receberTotal; const fr = receberTotal;
setText('fech-total', fmt(fd + fc + fr)); setText('fech-total', fmt(fd + fc + fr));
// Diferença = Saldo Esperado Fechamento // Diferença = Saldo Caixa Saldo Esperado
const diff = parseNum(val('f-saldo-esperado')) - (fd + fc + fr); const saldoCaixa = sysTotal;
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';
}
} }
} }
} }
@@ -989,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);
@@ -1012,7 +1047,6 @@ function buildEstadoForBackend() {
saldo_troco: parseNum(val('sys-troco')), // troco do sistema saldo_troco: parseNum(val('sys-troco')), // troco do sistema
saldo_esperado: parseNum(val('f-saldo-esperado')), saldo_esperado: parseNum(val('f-saldo-esperado')),
fechamento: parseNum(val('f-fech-dinheiro')) + parseNum(val('f-fech-cartoes')) + parseNum(val('f-fech-receber')), 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'))),
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: r.retirada || 0, gerente: r.gerente || '' })), sangrias: estado.sangrias.map(r => ({ hora: r.hora || '', retirada: 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 })),
@@ -1050,8 +1084,44 @@ function loadState() {
const raw = localStorage.getItem(LS_KEY); const raw = localStorage.getItem(LS_KEY);
if (!raw) return false; if (!raw) return false;
const saved = JSON.parse(raw); const saved = JSON.parse(raw);
// Merge carefully (cartoes may be in old format) // Restore simple fields
Object.assign(estado, saved); estado.uuid = saved.uuid || estado.uuid;
estado.id_local = saved.id_local !== undefined ? saved.id_local : null;
estado.loja = saved.loja || estado.loja;
estado.data = saved.data || estado.data;
estado.operador = saved.operador || estado.operador;
estado.turno = saved.turno || estado.turno;
estado.saldo_troco = saved.saldo_troco || 0;
estado.saldo_esperado = saved.saldo_esperado || 0;
estado.fechamento = saved.fechamento || 0;
estado.observacoes = saved.observacoes || '';
estado.sync_status = saved.sync_status || 'local';
// Restore tables (always as arrays of objects)
estado.despesas = Array.isArray(saved.despesas) ? saved.despesas : [];
estado.sangrias = Array.isArray(saved.sangrias) ? saved.sangrias : [];
estado.pixcnpj = Array.isArray(saved.pixcnpj) ? saved.pixcnpj : [];
estado.vales = Array.isArray(saved.vales) ? saved.vales : [];
estado.receber = Array.isArray(saved.receber) ? saved.receber : [];
estado.cancelamentos = Array.isArray(saved.cancelamentos) ? saved.cancelamentos : [];
// Restore cartoes — each entry must be { valor: number }
if (saved.cartoes) {
estado.cartoes.credito = Array.isArray(saved.cartoes.credito)
? saved.cartoes.credito.map(v => ({ valor: typeof v === 'object' && v !== null ? (v.valor || 0) : (parseNum(v) || 0) }))
: [];
estado.cartoes.debito = Array.isArray(saved.cartoes.debito)
? saved.cartoes.debito.map(v => ({ valor: typeof v === 'object' && v !== null ? (v.valor || 0) : (parseNum(v) || 0) }))
: [];
estado.cartoes.alimentacao = Array.isArray(saved.cartoes.alimentacao)
? saved.cartoes.alimentacao.map(v => ({ valor: typeof v === 'object' && v !== null ? (v.valor || 0) : (parseNum(v) || 0) }))
: [];
estado.cartoes.pix = Array.isArray(saved.cartoes.pix)
? saved.cartoes.pix.map(v => ({ valor: typeof v === 'object' && v !== null ? (v.valor || 0) : (parseNum(v) || 0) }))
: [];
}
// Restore fechamento_contado
if (saved.fechamento_contado) {
estado.fechamento_contado = saved.fechamento_contado;
}
setVal('f-data', estado.data || ''); setVal('f-data', estado.data || '');
setVal('f-operador', estado.operador || ''); setVal('f-operador', estado.operador || '');
setVal('f-turno', estado.turno || ''); setVal('f-turno', estado.turno || '');
@@ -1059,9 +1129,10 @@ function loadState() {
setVal('f-obs', estado.observacoes || ''); setVal('f-obs', estado.observacoes || '');
setVal('f-saldo-esperado', estado.saldo_esperado === 0 ? '' : fmtNum(estado.saldo_esperado)); setVal('f-saldo-esperado', estado.saldo_esperado === 0 ? '' : fmtNum(estado.saldo_esperado));
setVal('sys-troco', estado.saldo_troco === 0 ? '' : fmtNum(estado.saldo_troco)); setVal('sys-troco', estado.saldo_troco === 0 ? '' : fmtNum(estado.saldo_troco));
setVal('f-fech-dinheiro', estado.fechamento_contado && estado.fechamento_contado.dinheiro ? fmtNum(estado.fechamento_contado.dinheiro) : ''); const fc = estado.fechamento_contado || {};
setVal('f-fech-cartoes', estado.fechamento_contado && estado.fechamento_contado.cartoes ? fmtNum(estado.fechamento_contado.cartoes) : ''); setVal('f-fech-dinheiro', fc.dinheiro ? fmtNum(fc.dinheiro) : '');
setVal('f-fech-receber', estado.fechamento_contado && estado.fechamento_contado.receber ? fmtNum(estado.fechamento_contado.receber) : ''); setVal('f-fech-cartoes', fc.cartoes ? fmtNum(fc.cartoes) : '');
setVal('f-fech-receber', fc.receber ? fmtNum(fc.receber) : '');
syncHeader(); syncHeader();
PRE_FILL_TABLES.forEach(ensureOneRow); PRE_FILL_TABLES.forEach(ensureOneRow);
Object.keys(TABLES).forEach(renderTable); Object.keys(TABLES).forEach(renderTable);
@@ -1142,14 +1213,218 @@ 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
const result = await window.__TAURI__.core.invoke('sb_salvar_fechamento', { estado: data }); console.log('=== Enviando para Supabase ===');
return result; 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 resposta:', JSON.stringify(result));
return result;
} catch(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 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 = '';
// Cancelamentos (sum the array first — single cancelamentos is a float total, array is from confirm-print)
const cancelTotal = Array.isArray(data.cancelamentos)
? data.cancelamentos.reduce((s, c) => s + (parseNum(c.valor || 0) || 0), 0)
: (data.cancelamentos || 0);
// PIX total (flat number from tauriRecibo; pixcnpj is also flat total)
const pixTotal = data.pix || data.voucher || 0;
const pixCnpjTotal = typeof data.pixcnpj === 'number' ? data.pixcnpj : 0;
// 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 (flat total, not array)
if (pixCnpjTotal > 0) {
rows += `<tr><td>PIX CNPJ</td><td>${fmt(pixCnpjTotal)}</td><td></td></tr>`;
}
// Cancelamentos
if (cancelTotal > 0) {
rows += `<tr><td>Cancelamentos</td><td>${fmt(cancelTotal)}</td><td></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||0)}</span></div>
<div class="row"><span>Débito:</span><span>${fmt(data.debito||0)}</span></div>
<div class="row"><span>Alimentação:</span><span>${fmt(data.alimentacao||0)}</span></div>
<div class="row"><span>PIX Cartão:</span><span>${fmt(pixTotal)}</span></div>
<div class="row"><span>Cancelamentos:</span><span>${fmt(cancelTotal)}</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() {
const data = buildEstadoForBackend(); // Build flat totals for the Rust ReciboData struct
return await window.__TAURI__.core.invoke('imprimir_recibo', { estado: JSON.stringify(data) }); 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: fechamentoCounted - saldoEsp,
// Flat totals for ReciboData struct (Rust) — must be numbers
credito: cartCred,
debito: cartDeb,
alimentacao: cartAli,
voucher: cartPix,
pixcnpj: pixCnpjTotal,
sangrias: sangriasTotal,
despesas: despesasTotal,
vales: valesTotal,
areceber: receberTotal,
cancelamentos: cancelTotal,
// Full arrays for backend/storage
despesas_arr: estado.despesas.map(r => ({ desc: r.desc||'', obs: r.obs||'', valor: r.valor||0 })),
sangrias_arr: estado.sangrias.map(r => ({ hora: r.hora||'', retirada: parseNum(r.retirada)||0, gerente: r.gerente||'' })),
pixcnpj_arr: estado.pixcnpj.map(r => ({ nome: r.nome||'', valor: r.valor||0 })),
vales_arr: estado.vales.map(r => ({ nome: r.nome||'', obs: r.obs||'', valor: r.valor||0 })),
receber_arr: estado.receber.map(r => ({ nome: r.nome||'', valor: r.valor||0 })),
cancelamentos_arr: estado.cancelamentos.map(r => ({ numero: r.numero||'', valor: r.valor||0, motivo: r.motivo||'' })),
cartoes: { credito: cartCred, debito: cartDeb, alimentacao: cartAli, pix: cartPix },
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() { async function tauriCarregarRascunho() {
@@ -1175,13 +1450,16 @@ function openConfirmModal() {
const cartDeb = estado.cartoes.debito.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 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 cartPix = estado.cartoes.pix.reduce((s,r) => s+(r.valor||0), 0);
const sangrias = estado.sangrias.reduce((s,r) => s+(parseFloat(r.retirada||0)||0), 0); const sangrias = estado.sangrias.reduce((s,r) => s+(parseNum(r.retirada||0)||0), 0);
const despesas = estado.despesas.reduce((s,r) => s+(r.valor||0), 0); const despesas = estado.despesas.reduce((s,r) => s+(parseNum(r.valor||0)||0), 0);
const vales = estado.vales.reduce((s,r) => s+(r.valor||0), 0); const vales = estado.vales.reduce((s,r) => s+(parseNum(r.valor||0)||0), 0);
const receber = estado.receber.reduce((s,r) => s+(r.valor||0), 0); const receber = estado.receber.reduce((s,r) => s+(parseNum(r.valor||0)||0), 0);
const cancelamentos = estado.cancelamentos.reduce((s,r) => s+(r.valor||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">
@@ -1189,13 +1467,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');
} }
@@ -1208,19 +1488,34 @@ 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,
pix: cartPix, // flat float for buildPrintHTML
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
@@ -1233,10 +1528,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';
@@ -1252,10 +1554,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;
} }
@@ -1268,33 +1573,122 @@ 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 fechamentos recentes do Supabase com uuid
let recentList = [];
try {
recentList = await window.__TAURI__.core.invoke('sb_listar_recentes', { loja, limite: 20 });
} catch(e) {
console.warn('sb_listar_recentes falhou:', e);
}
// Extrai datas únicas ordenadas + mapeamento data→id
const dateIdMap = {};
const dateList = [];
if (recentList && recentList.length > 0) {
recentList.forEach(item => {
if (item.data && !dateIdMap[item.data]) {
dateIdMap[item.data] = item.id;
dateList.push(item.data);
}
});
dateList.sort().reverse();
}
let selectedDate = null;
let selectedId = null;
if (dateList.length > 0) {
const msg = dateList.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 < dateList.length) {
selectedDate = dateList[idx];
selectedId = dateIdMap[selectedDate];
}
}
}
// Se não escolheu data, usa dia anterior automático (sem id — carrega por data)
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();
const rascunho = await tauriCarregarRascunho();
// Tenta Supabase primeiro (com id se disponível), senão local
let rascunho = null;
if (selectedId) {
// Usa sb_buscar_por_id para buscar por id numérico do Supabase
try {
rascunho = await window.__TAURI__.core.invoke('sb_buscar_por_id', { id: selectedId });
} catch(e) { console.warn('sb_buscar_por_id falhou:', e); }
}
if (!rascunho) {
try {
rascunho = await window.__TAURI__.core.invoke('sb_carregar_rascunho', { loja, data: selectedDate });
} catch(e) { console.warn('sb_carregar_rascunho falhou:', e); }
}
if (!rascunho) {
try {
rascunho = await window.__TAURI__.core.invoke('carregar_rascunho', { loja, data: selectedDate });
} catch(e) { console.warn('carregar_rascunho local falhou:', e); }
}
if (rascunho) { if (rascunho) {
if (rascunho.operador) setVal('f-operador', rascunho.operador); if (rascunho.operador) { setVal('f-operador', rascunho.operador); estado.operador = rascunho.operador; }
if (rascunho.turno) setVal('f-turno', rascunho.turno); if (rascunho.turno) { setVal('f-turno', rascunho.turno); estado.turno = rascunho.turno; }
estado.operador = rascunho.operador || '';
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.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 {
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); 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 ─────────────────────────────────────────────────────────────────