Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 60e15dbed3 | |||
| 53767df80c | |||
| 208eede526 | |||
| fa1e973f8c | |||
| d623d11e23 | |||
| f13b059ce7 | |||
| 8660ac4755 | |||
| 72c534a813 |
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 {
|
||||||
|
|||||||
+401
-76
@@ -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';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1142,14 +1175,211 @@ 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 diff = (data.saldo_troco - data.saldo_esperado);
|
||||||
|
const diffStr = diff >= 0 ? `+${fmt(diff)}` : fmt(diff);
|
||||||
|
const diffColor = diff === 0 ? '#2e7d32' : (diff > 0 ? '#e65100' : '#c0272d');
|
||||||
|
|
||||||
|
let rows = '';
|
||||||
|
// 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
|
||||||
|
if (data.pixcnpj && data.pixcnpj.length > 0) {
|
||||||
|
data.pixcnpj.forEach(p => { if (p.nome || p.valor) rows += `<tr><td>PIX: ${p.nome||''}</td><td>${fmt(p.valor)}</td><td></td></tr>`; });
|
||||||
|
}
|
||||||
|
// Cancelamentos
|
||||||
|
if (data.cancelamentos && data.cancelamentos.length > 0) {
|
||||||
|
data.cancelamentos.forEach(c => { if (c.numero || c.valor) rows += `<tr><td>${c.numero||''}</td><td>${fmt(c.valor)}</td><td>${c.motivo||''}</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)}</span></div>
|
||||||
|
<div class="row"><span>Débito:</span><span>${fmt(data.debito)}</span></div>
|
||||||
|
<div class="row"><span>Alimentação:</span><span>${fmt(data.alimentacao)}</span></div>
|
||||||
|
<div class="row"><span>PIX Cartão:</span><span>${fmt(data.cartoes?.pix||0)}</span></div>
|
||||||
|
<div class="row"><span>Cancelamentos:</span><span>${fmt(data.cancelamentos)}</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: sysTroco - saldoEsp,
|
||||||
|
despesas: estado.despesas.map(r => ({ desc: r.desc||'', obs: r.obs||'', valor: r.valor||0 })),
|
||||||
|
sangrias: estado.sangrias.map(r => ({ hora: r.hora||'', retirada: parseNum(r.retirada)||0, gerente: r.gerente||'' })),
|
||||||
|
pixcnpj: estado.pixcnpj.map(r => ({ nome: r.nome||'', valor: r.valor||0 })),
|
||||||
|
vales: estado.vales.map(r => ({ nome: r.nome||'', obs: r.obs||'', valor: r.valor||0 })),
|
||||||
|
receber: estado.receber.map(r => ({ nome: r.nome||'', valor: r.valor||0 })),
|
||||||
|
cancelamentos: estado.cancelamentos.map(r => ({ numero: r.numero||'', valor: r.valor||0, motivo: r.motivo||'' })),
|
||||||
|
cartoes: { credito: cartCred, debito: cartDeb, alimentacao: cartAli, pix: cartPix },
|
||||||
|
// Flat totals for ReciboData struct (Rust)
|
||||||
|
credito: cartCred,
|
||||||
|
debito: cartDeb,
|
||||||
|
alimentacao: cartAli,
|
||||||
|
voucher: cartPix,
|
||||||
|
pixcnpj: pixCnpjTotal,
|
||||||
|
sangrias: sangriasTotal,
|
||||||
|
despesas: despesasTotal,
|
||||||
|
vales: valesTotal,
|
||||||
|
areceber: receberTotal,
|
||||||
|
cancelamentos: cancelTotal,
|
||||||
|
observacoes: estado.observacoes,
|
||||||
|
sync_status: estado.sync_status,
|
||||||
|
};
|
||||||
|
|
||||||
|
// 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 +1405,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 +1422,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 +1443,30 @@ 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');
|
// Reconstrói os mesmos dados do tauriRecibo e abre preview
|
||||||
btn.disabled = true;
|
const cartCred = estado.cartoes.credito.reduce((s,r) => s+(r.valor||0), 0);
|
||||||
try {
|
const cartDeb = estado.cartoes.debito.reduce((s,r) => s+(r.valor||0), 0);
|
||||||
await tauriRecibo();
|
const cartAli = estado.cartoes.alimentacao.reduce((s,r) => s+(r.valor||0), 0);
|
||||||
alert('✅ Impressão enviada!');
|
const cartPix = estado.cartoes.pix.reduce((s,r) => s+(r.valor||0), 0);
|
||||||
} catch(e) {
|
const data = {
|
||||||
alert('Erro ao imprimir: ' + (e.message || e));
|
loja: estado.loja, data: estado.data, operador: estado.operador, turno: estado.turno,
|
||||||
} finally {
|
saldo_troco: parseNum(val('sys-troco')) || 0,
|
||||||
btn.disabled = false;
|
saldo_esperado: parseNum(val('f-saldo-esperado')) || 0,
|
||||||
}
|
fechamento: parseNum(val('f-fech-dinheiro')) + parseNum(val('f-fech-cartoes')) + parseNum(val('f-fech-receber')),
|
||||||
|
despesas: estado.despesas.map(r => ({ desc: r.desc||'', obs: r.obs||'', valor: r.valor||0 })),
|
||||||
|
sangrias: estado.sangrias.map(r => ({ hora: r.hora||'', retirada: parseNum(r.retirada)||0, gerente: r.gerente||'' })),
|
||||||
|
pixcnpj: estado.pixcnpj.map(r => ({ nome: r.nome||'', valor: r.valor||0 })),
|
||||||
|
vales: estado.vales.map(r => ({ nome: r.nome||'', obs: r.obs||'', valor: r.valor||0 })),
|
||||||
|
receber: estado.receber.map(r => ({ nome: r.nome||'', valor: r.valor||0 })),
|
||||||
|
cancelamentos: estado.cancelamentos.map(r => ({ numero: r.numero||'', valor: r.valor||0, motivo: r.motivo||'' })),
|
||||||
|
credito: cartCred, debito: cartDeb, alimentacao: cartAli,
|
||||||
|
cartoes: { pix: cartPix },
|
||||||
|
observacoes: estado.observacoes,
|
||||||
|
};
|
||||||
|
openPrintPreview(data);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Confirm send
|
// Confirm send
|
||||||
@@ -1233,10 +1479,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 +1505,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 +1524,102 @@ 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 datas recentes do Supabase
|
||||||
|
let datas = [];
|
||||||
|
try {
|
||||||
|
const lista = await window.__TAURI__.core.invoke('sb_listar_recentes', { loja, limite: 20 });
|
||||||
|
if (lista && lista.length > 0) {
|
||||||
|
// Extrai datas únicas e já ordenadas
|
||||||
|
datas = [...new Set(lista.map(l => l.data).filter(Boolean))].sort().reverse();
|
||||||
|
}
|
||||||
|
} catch(e) {
|
||||||
|
console.warn('sb_listar_recentes falhou:', e);
|
||||||
|
}
|
||||||
|
|
||||||
|
let selectedDate = null;
|
||||||
|
if (datas.length > 0) {
|
||||||
|
// Mostra seletor com as datas disponíveis
|
||||||
|
const msg = datas.map((d, i) => `${i + 1}) ${d}`).join('\n');
|
||||||
|
const escolha = prompt(`Selecione o número do dia anterior:\n${msg}\n\n(Deixe em branco e clique Cancelar para ver só o dia anterior automático)`);
|
||||||
|
if (escolha !== null && escolha.trim() !== '') {
|
||||||
|
const idx = parseInt(escolha) - 1;
|
||||||
|
if (idx >= 0 && idx < datas.length) selectedDate = datas[idx];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Se não escolheu data, usa dia anterior automático
|
||||||
|
if (!selectedDate) {
|
||||||
|
const d = new Date(estado.data); d.setDate(d.getDate() - 1);
|
||||||
|
selectedDate = d.toISOString().split('T')[0];
|
||||||
|
}
|
||||||
|
|
||||||
await tauriSaveLocal();
|
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, senão local
|
||||||
|
let rascunho = null;
|
||||||
|
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