Compare commits
13 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7e57d30863 | |||
| 7a93a30c22 | |||
| 230304e878 | |||
| d185f82bd5 | |||
| 2253d45d00 | |||
| 60e15dbed3 | |||
| 53767df80c | |||
| 208eede526 | |||
| fa1e973f8c | |||
| d623d11e23 | |||
| f13b059ce7 | |||
| 8660ac4755 | |||
| 72c534a813 |
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.
Executable
BIN
Binary file not shown.
@@ -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,
|
||||||
|
|||||||
@@ -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.");
|
||||||
|
}
|
||||||
|
|||||||
@@ -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
@@ -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} <strong>👤 Operador:</strong> ${operador} <strong>⏰ Turno:</strong> ${turno} <strong>🏪 Loja:</strong> ${loja}</div>
|
`<div style="margin-bottom:6px"><strong>📅 Data:</strong> ${estado.data} <strong>👤 Operador:</strong> ${operador} <strong>⏰ Turno:</strong> ${turno} <strong>🏪 Loja:</strong> ${loja}</div>
|
||||||
<hr style="border:none;border-top:1px dashed #b3d9f7;margin:6px 0">
|
<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 ─────────────────────────────────────────────────────────────────
|
||||||
|
|||||||
Reference in New Issue
Block a user