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::imprimir_recibo,
|
||||
plugins::printer::teste_impressora,
|
||||
plugins::printer::ativar_modo_teste_impressao,
|
||||
// App
|
||||
plugins::app::get_loja,
|
||||
plugins::app::set_loja,
|
||||
|
||||
@@ -48,6 +48,7 @@ pub struct ReciboData {
|
||||
pub debito: f64,
|
||||
pub alimentacao: f64,
|
||||
pub voucher: f64,
|
||||
pub pixcnpj: f64,
|
||||
pub cancelamentos: f64,
|
||||
}
|
||||
|
||||
@@ -84,18 +85,19 @@ impl ReciboData {
|
||||
out.extend(escpos::divider());
|
||||
|
||||
// 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("Despesas:", &self.fmt_money(self.despesas)));
|
||||
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("PIX CNPJ:", &self.fmt_money(self.pixcnpj)));
|
||||
out.extend(escpos::divider());
|
||||
|
||||
// Cartões
|
||||
out.extend(escpos::line_bold("CARTÕES"));
|
||||
out.extend(escpos::kv("Crédito:", &self.fmt_money(self.credito)));
|
||||
out.extend(escpos::kv("Débito:", &self.fmt_money(self.debito)));
|
||||
out.extend(escpos::kv("Alimentação:", &self.fmt_money(self.alimentacao)));
|
||||
out.extend(escpos::kv("Credito:", &self.fmt_money(self.credito)));
|
||||
out.extend(escpos::kv("Debito:", &self.fmt_money(self.debito)));
|
||||
out.extend(escpos::kv("Alimentacao:", &self.fmt_money(self.alimentacao)));
|
||||
out.extend(escpos::kv("Voucher:", &self.fmt_money(self.voucher)));
|
||||
out.extend(escpos::kv("Cancelamentos:", &self.fmt_money(self.cancelamentos)));
|
||||
out.extend(escpos::divider());
|
||||
@@ -106,7 +108,7 @@ impl ReciboData {
|
||||
} else {
|
||||
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::blank_lines(2));
|
||||
|
||||
@@ -120,88 +122,149 @@ impl ReciboData {
|
||||
|
||||
// Corta papel
|
||||
out.extend(escpos::FEED_CUT);
|
||||
out.extend(escpos::CUT);
|
||||
|
||||
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 ─────────────────────────────────────────────────────────
|
||||
|
||||
pub struct PrinterManager {
|
||||
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 {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
device_path: Mutex::new(None),
|
||||
test_mode: Mutex::new(false),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PrinterManager {
|
||||
/// Detecta impressora USB conectada
|
||||
pub fn detectar(&self) -> Result<String, String> {
|
||||
usb_windows::find_printer_device()
|
||||
.ok_or_else(|| "Nenhuma impressora USB encontrada".into())
|
||||
.map(|p| {
|
||||
*self.device_path.lock().unwrap() = Some(p.clone());
|
||||
p
|
||||
})
|
||||
#[cfg(windows)]
|
||||
{
|
||||
// Tenta encontrar porta USB virtual via WMI/registry
|
||||
// Common TM-T20 USB port names on Windows
|
||||
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> {
|
||||
if path.is_empty() {
|
||||
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(())
|
||||
}
|
||||
|
||||
/// 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> {
|
||||
// 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
|
||||
.device_path
|
||||
.lock()
|
||||
.unwrap()
|
||||
.clone()
|
||||
.ok_or_else(|| "Impressora nao configurada".to_string())?;
|
||||
let handle = usb_windows::open_printer(&path)?;
|
||||
let n = usb_windows::write_to(handle, data)?;
|
||||
usb_windows::close_handle(handle);
|
||||
Ok(n)
|
||||
.ok_or_else(|| "Impressora nao configurada. Use o botao de detectar ou configure manualmente.".to_string())?;
|
||||
|
||||
#[cfg(windows)]
|
||||
{
|
||||
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 {
|
||||
device_path: Mutex::new(None),
|
||||
test_mode: Mutex::new(false),
|
||||
};
|
||||
|
||||
#[tauri::command]
|
||||
@@ -231,22 +295,31 @@ pub fn imprimir_recibo(data: String) -> Result<usize, String> {
|
||||
let r: ReciboData =
|
||||
serde_json::from_str(&data).map_err(|e| format!("Erro ao analisar dados: {}", e))?;
|
||||
let bytes = r.to_escpos();
|
||||
log::info!("Imprimindo recibo: {} bytes ESC/POS", bytes.len());
|
||||
PRINTER.imprimir_bytes(&bytes)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn teste_impressora() -> Result<usize, String> {
|
||||
// Simple test pattern
|
||||
let mut test = Vec::new();
|
||||
test.extend(escpos::INIT);
|
||||
test.extend(escpos::blank_lines(1));
|
||||
test.extend(escpos::title("TESTE DE IMPRESSORA"));
|
||||
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::FEED_CUT);
|
||||
test.extend(escpos::CUT);
|
||||
log::info!("Teste impressora: {} bytes", test.len());
|
||||
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)
|
||||
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 fechamento = estado.get("fechamento").and_then(|v| v.as_f64()).unwrap_or(0.0);
|
||||
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 despesas/receber arrays
|
||||
let despesas: Vec<serde_json::Value> = estado.get("despesas")
|
||||
// Constrói TableData a partir de todas as tabelas do frontend
|
||||
let despesas: Vec<serde_json::Value> = estado
|
||||
.get("despesas")
|
||||
.and_then(|v| v.as_array())
|
||||
.map(|arr| arr.to_vec())
|
||||
.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())
|
||||
.map(|arr| arr.to_vec())
|
||||
.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 {
|
||||
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()),
|
||||
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()
|
||||
}
|
||||
};
|
||||
|
||||
let tabelas = TableData {
|
||||
despesas: despesas.iter().map(to_table_row).collect(),
|
||||
areceber: receber.iter().map(to_table_row).collect(),
|
||||
..Default::default()
|
||||
despesas: despesas.iter().map(|v| to_table_row(v, "despesas")).collect(),
|
||||
sangrias: sangrias.iter().map(|v| to_table_row(v, "sangrias")).collect(),
|
||||
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 {
|
||||
credito: vec![TableRow { valor: cartoes_json.and_then(|c| c.get("credito")).and_then(|v| v.as_f64()), ..Default::default() }],
|
||||
debito: vec![TableRow { valor: cartoes_json.and_then(|c| c.get("debito")).and_then(|v| v.as_f64()), ..Default::default() }],
|
||||
alimentacao: vec![TableRow { valor: cartoes_json.and_then(|c| c.get("alimentacao")).and_then(|v| v.as_f64()), ..Default::default() }],
|
||||
voucher: vec![TableRow { valor: cartoes_json.and_then(|c| c.get("voucher")).and_then(|v| v.as_f64()), ..Default::default() }],
|
||||
credito: cartoes_json
|
||||
.and_then(|c| c.get("credito"))
|
||||
.and_then(|v| v.as_array())
|
||||
.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 {
|
||||
|
||||
+477
-83
@@ -32,6 +32,7 @@
|
||||
color: #fff; padding: 8px 18px;
|
||||
display: flex; align-items: center; justify-content: space-between;
|
||||
flex-shrink: 0; gap: 14px;
|
||||
position: sticky; top: 0; z-index: 100;
|
||||
}
|
||||
.header-left { display: flex; align-items: center; gap: 14px; flex: 1; }
|
||||
.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">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">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>
|
||||
</div>
|
||||
@@ -403,6 +405,12 @@
|
||||
<label>Informe o saldo esperado (contado em caixa)</label>
|
||||
<input type="text" id="f-saldo-esperado" placeholder="0,00" />
|
||||
</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>
|
||||
|
||||
<!-- FECHAMENTO (VALORES CONTADOS) -->
|
||||
@@ -419,14 +427,6 @@
|
||||
</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 -->
|
||||
<div class="section" style="flex:1;display:flex;flex-direction:column">
|
||||
<div class="section-header">📝 Observações</div>
|
||||
@@ -577,28 +577,32 @@ let estado = {
|
||||
// ─── Helpers ──────────────────────────────────────────────────────────────
|
||||
const fmt = v => 'R$ ' + (parseFloat(v) || 0).toLocaleString('pt-BR', { minimumFractionDigits: 2 });
|
||||
const parseNum = v => {
|
||||
const s = String(v || '0');
|
||||
// Handle empty or whitespace
|
||||
if (!s.trim()) return 0;
|
||||
// Already a number? Return directly (don't treat '.' as thousand separator)
|
||||
if (typeof v === 'number') return isNaN(v) ? 0 : v;
|
||||
const s = String(v || '0').trim();
|
||||
if (!s) return 0;
|
||||
// Brazilian format: 1.234,56 → 1234.56
|
||||
// Strategy: remove thousand dots, replace decimal comma with dot
|
||||
// "10,55" → "1055,00" (wrong) → fix: split on comma first
|
||||
// "1.055,00" → parts=["1.055","00"] → intPart="1055", decPart="00" → 1055.00
|
||||
// Strategy: if string has comma, treat as BRL (decimal comma)
|
||||
// If no comma but has dot, treat as US (decimal dot)
|
||||
const lastComma = s.lastIndexOf(',');
|
||||
if (lastComma === -1) {
|
||||
// No comma: just remove thousand separators (dots)
|
||||
return parseFloat(s.replace(/,/g, '').replace(/\./g, '')) || 0;
|
||||
const lastDot = s.lastIndexOf('.');
|
||||
const hasCommaDecimal = lastComma > lastDot;
|
||||
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, '');
|
||||
const decPart = s.slice(lastComma + 1);
|
||||
return parseFloat(intPart + '.' + decPart) || 0;
|
||||
// No comma: US-style number, just strip any dots (thousand sep)
|
||||
return parseFloat(s.replace(/,/g, '').replace(/\./g, '')) || 0;
|
||||
};
|
||||
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 val = id => { const e = document.getElementById(id); return e ? e.value : ''; };
|
||||
const fmtNum = 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
|
||||
@@ -637,17 +641,23 @@ function makeRow(tblId, rowIndex, rowData) {
|
||||
if (col === 'valor' || col === 'retirada') td.className = 'num';
|
||||
const inp = document.createElement('input');
|
||||
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.dataset.tbl = tblId;
|
||||
inp.dataset.row = rowIndex;
|
||||
inp.dataset.col = col;
|
||||
// datalist only for fields that should offer suggestions from config
|
||||
if (col === 'gerente') inp.setAttribute('list', 'dl-gerente');
|
||||
if (col === 'desc') inp.setAttribute('list', 'dl-despesa');
|
||||
// tbl-receber: clientes free-text (sem datalist)
|
||||
// tbl-vales: usa dl-vales
|
||||
if (col === 'nome' && tblId === 'tbl-vales') inp.setAttribute('list', 'dl-vales');
|
||||
if (col === 'nome' && tblId === 'tbl-pix') inp.setAttribute('list', 'dl-generico');
|
||||
// tbl-pix nome = free text (no datalist)
|
||||
// tbl-receber nome = free text (no datalist)
|
||||
// tbl-cancelamentos numero + motivo = free text (no datalist)
|
||||
inp.addEventListener('input', onTableInput);
|
||||
inp.addEventListener('blur', onCellBlur);
|
||||
inp.addEventListener('keydown', onCellKeydown);
|
||||
@@ -685,6 +695,7 @@ function onTableInput(e) {
|
||||
if (formatted !== e.target.value) e.target.value = formatted;
|
||||
field[row][col] = formatted;
|
||||
} else {
|
||||
// text fields: nome, desc, obs, gerente, motivo, numero — keep as string
|
||||
field[row][col] = e.target.value;
|
||||
}
|
||||
updateTotals();
|
||||
@@ -698,6 +709,7 @@ function onCellBlur(e) {
|
||||
e.target.value = isNaN(v) || v === 0 ? '' : fmtNum(v);
|
||||
updateTotals();
|
||||
}
|
||||
// For text cols (hora, nome, desc, obs, gerente, motivo, numero) — keep as-is
|
||||
}
|
||||
|
||||
function onCellKeydown(e) {
|
||||
@@ -731,8 +743,9 @@ function focusInput(tblId, rowIndex, col) {
|
||||
function addRowAndFocus(tblId, focusCol) {
|
||||
const cfg = TABLES[tblId];
|
||||
const field = getField(tblId);
|
||||
const numericCols = ['valor', 'retirada'];
|
||||
const empty = {};
|
||||
cfg.cols.forEach(c => { empty[c] = c === 'valor' || c === 'retirada' ? 0 : ''; });
|
||||
cfg.cols.forEach(c => { empty[c] = numericCols.includes(c) ? 0 : ''; });
|
||||
field.push(empty);
|
||||
renderTable(tblId);
|
||||
focusInput(tblId, field.length - 1, focusCol);
|
||||
@@ -773,7 +786,7 @@ document.querySelectorAll('.add-btn[data-tbl]').forEach(btn => {
|
||||
// ─── Totals ────────────────────────────────────────────────────────────────
|
||||
function updateTotals() {
|
||||
// ── 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));
|
||||
|
||||
// ── Tabelas de valor ──
|
||||
@@ -782,7 +795,7 @@ function updateTotals() {
|
||||
'tbl-receber':'receber', 'tbl-cancelamentos':'cancelamentos',
|
||||
};
|
||||
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));
|
||||
});
|
||||
|
||||
@@ -807,17 +820,19 @@ function updateTotals() {
|
||||
document.getElementById('sys-debito').value = cartDeb > 0 ? fmtNum(cartDeb) : '';
|
||||
// Alimentação = total cartão alimentação
|
||||
document.getElementById('sys-alimentacao').value = cartAli > 0 ? fmtNum(cartAli) : '';
|
||||
// PIX = total cartão PIX + total PIX CNPJ
|
||||
const pixCnpjTotal = estado.pixcnpj.reduce((s, r) => s + (parseFloat(r['valor'] || 0) || 0), 0);
|
||||
document.getElementById('sys-pix').value = (cartPix + pixCnpjTotal) > 0 ? fmtNum(cartPix + pixCnpjTotal) : '';
|
||||
// PIX Cartão = total cartão PIX
|
||||
document.getElementById('sys-pix').value = cartPix > 0 ? fmtNum(cartPix) : '';
|
||||
// PIX CNPJ = total PIX CNPJ
|
||||
const pixCnpjTotal = estado.pixcnpj.reduce((s, r) => s + (parseNum(r['valor'] || 0) || 0), 0);
|
||||
document.getElementById('sys-pixcnpj').value = pixCnpjTotal > 0 ? fmtNum(pixCnpjTotal) : '';
|
||||
// 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) : '';
|
||||
// À 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) : '';
|
||||
|
||||
// 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;
|
||||
setText('sys-total', fmt(sysTotal));
|
||||
|
||||
@@ -835,19 +850,37 @@ function updateTotals() {
|
||||
const fr = receberTotal;
|
||||
setText('fech-total', fmt(fd + fc + fr));
|
||||
|
||||
// Diferença = Saldo Esperado − Fechamento
|
||||
const diff = parseNum(val('f-saldo-esperado')) - (fd + fc + fr);
|
||||
// Diferença = Saldo Caixa − Saldo Esperado
|
||||
const saldoCaixa = sysTotal;
|
||||
const diff = saldoCaixa - parseNum(val('f-saldo-esperado'));
|
||||
const difEl = document.getElementById('diferenca');
|
||||
const badgeEl = document.getElementById('dif-badge');
|
||||
if (difEl) {
|
||||
difEl.textContent = fmt(diff);
|
||||
if (diff === 0) {
|
||||
difEl.className = 'dif-value zero';
|
||||
if (badgeEl) { badgeEl.textContent = '✓ OK'; badgeEl.style.background='#e8f5e9'; badgeEl.style.color='#2e7d32'; badgeEl.style.display='inline'; }
|
||||
} else if (diff < 0 && diff >= -5) {
|
||||
difEl.className = 'dif-value neg'; // OK − até −5
|
||||
difEl.className = 'dif-value neg'; // OK − até −5 de tolerância
|
||||
if (badgeEl) { badgeEl.textContent = '✓ OK'; badgeEl.style.background='#e8f5e9'; badgeEl.style.color='#2e7d32'; badgeEl.style.display='inline'; }
|
||||
} else if (diff > 0 && diff <= 3) {
|
||||
difEl.className = 'dif-value zero'; // OK + até 3
|
||||
if (badgeEl) { badgeEl.textContent = '✓ OK'; badgeEl.style.background='#e8f5e9'; badgeEl.style.color='#2e7d32'; badgeEl.style.display='inline'; }
|
||||
} else {
|
||||
difEl.className = 'dif-value pos'; // Sobra > +3
|
||||
// Sobra mais de R$3 ou falta mais de R$5
|
||||
difEl.className = diff > 0 ? 'dif-value pos' : 'dif-value neg';
|
||||
if (badgeEl) {
|
||||
if (diff > 0) {
|
||||
badgeEl.textContent = '⚠ SOBRANDO';
|
||||
badgeEl.style.background='#fff3e0';
|
||||
badgeEl.style.color='#e65100';
|
||||
} else {
|
||||
badgeEl.textContent = '⚠ FALTANDO';
|
||||
badgeEl.style.background='#ffebee';
|
||||
badgeEl.style.color='#c0272d';
|
||||
}
|
||||
badgeEl.style.display='inline';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -989,7 +1022,9 @@ function init() {
|
||||
const today = new Date().toISOString().split('T')[0];
|
||||
setVal('f-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();
|
||||
PRE_FILL_TABLES.forEach(ensureOneRow);
|
||||
Object.keys(TABLES).forEach(renderTable);
|
||||
@@ -1012,7 +1047,6 @@ function buildEstadoForBackend() {
|
||||
saldo_troco: parseNum(val('sys-troco')), // troco do sistema
|
||||
saldo_esperado: parseNum(val('f-saldo-esperado')),
|
||||
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 })),
|
||||
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 })),
|
||||
@@ -1050,8 +1084,44 @@ function loadState() {
|
||||
const raw = localStorage.getItem(LS_KEY);
|
||||
if (!raw) return false;
|
||||
const saved = JSON.parse(raw);
|
||||
// Merge carefully (cartoes may be in old format)
|
||||
Object.assign(estado, saved);
|
||||
// Restore simple fields
|
||||
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-operador', estado.operador || '');
|
||||
setVal('f-turno', estado.turno || '');
|
||||
@@ -1059,9 +1129,10 @@ function loadState() {
|
||||
setVal('f-obs', estado.observacoes || '');
|
||||
setVal('f-saldo-esperado', estado.saldo_esperado === 0 ? '' : fmtNum(estado.saldo_esperado));
|
||||
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) : '');
|
||||
setVal('f-fech-cartoes', estado.fechamento_contado && estado.fechamento_contado.cartoes ? fmtNum(estado.fechamento_contado.cartoes) : '');
|
||||
setVal('f-fech-receber', estado.fechamento_contado && estado.fechamento_contado.receber ? fmtNum(estado.fechamento_contado.receber) : '');
|
||||
const fc = estado.fechamento_contado || {};
|
||||
setVal('f-fech-dinheiro', fc.dinheiro ? fmtNum(fc.dinheiro) : '');
|
||||
setVal('f-fech-cartoes', fc.cartoes ? fmtNum(fc.cartoes) : '');
|
||||
setVal('f-fech-receber', fc.receber ? fmtNum(fc.receber) : '');
|
||||
syncHeader();
|
||||
PRE_FILL_TABLES.forEach(ensureOneRow);
|
||||
Object.keys(TABLES).forEach(renderTable);
|
||||
@@ -1142,14 +1213,218 @@ async function tauriEnviar() {
|
||||
const data = buildEstadoForBackend();
|
||||
// 1. Salva local
|
||||
await window.__TAURI__.core.invoke('salvar_fechamento', { estado: data }).catch(e => console.warn('local save err:', e));
|
||||
// 2. Envia Supabase
|
||||
const result = await window.__TAURI__.core.invoke('sb_salvar_fechamento', { estado: data });
|
||||
return result;
|
||||
// 2. Envia Supabase — debug completo
|
||||
console.log('=== Enviando para Supabase ===');
|
||||
console.log('URL:', 'https://supabase.ofrangao.com.br/rest/v1/fechamentos');
|
||||
console.log('Payload:', JSON.stringify(data, null, 2));
|
||||
try {
|
||||
const result = await window.__TAURI__.core.invoke('sb_salvar_fechamento', { estado: data });
|
||||
console.log('Supabase 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() {
|
||||
const data = buildEstadoForBackend();
|
||||
return await window.__TAURI__.core.invoke('imprimir_recibo', { estado: JSON.stringify(data) });
|
||||
// Build flat totals for the Rust ReciboData struct
|
||||
const cartCred = estado.cartoes.credito.reduce((s,r) => s+(r.valor||0), 0);
|
||||
const cartDeb = estado.cartoes.debito.reduce((s,r) => s+(r.valor||0), 0);
|
||||
const cartAli = estado.cartoes.alimentacao.reduce((s,r) => s+(r.valor||0), 0);
|
||||
const cartPix = estado.cartoes.pix.reduce((s,r) => s+(r.valor||0), 0);
|
||||
const pixCnpjTotal = estado.pixcnpj.reduce((s,r) => s+(parseNum(r.valor||0)||0), 0);
|
||||
const sangriasTotal = estado.sangrias.reduce((s,r) => s+(parseNum(r.retirada||0)||0), 0);
|
||||
const despesasTotal = estado.despesas.reduce((s,r) => s+(parseNum(r.valor||0)||0), 0);
|
||||
const valesTotal = estado.vales.reduce((s,r) => s+(parseNum(r.valor||0)||0), 0);
|
||||
const receberTotal = estado.receber.reduce((s,r) => s+(parseNum(r.valor||0)||0), 0);
|
||||
const cancelTotal = estado.cancelamentos.reduce((s,r) => s+(parseNum(r.valor||0)||0), 0);
|
||||
const fechamentoCounted = parseNum(val('f-fech-dinheiro')) + parseNum(val('f-fech-cartoes')) + parseNum(val('f-fech-receber'));
|
||||
const saldoEsp = parseNum(val('f-saldo-esperado'));
|
||||
const sysTroco = parseNum(val('sys-troco'));
|
||||
const data = {
|
||||
uuid: estado.uuid,
|
||||
id_local: estado.id_local,
|
||||
loja: estado.loja,
|
||||
data: estado.data,
|
||||
operador: estado.operador,
|
||||
turno: estado.turno,
|
||||
saldo_troco: sysTroco,
|
||||
saldo_esperado: saldoEsp,
|
||||
fechamento: fechamentoCounted,
|
||||
diferenca: 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() {
|
||||
@@ -1175,13 +1450,16 @@ function openConfirmModal() {
|
||||
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 sangrias = estado.sangrias.reduce((s,r) => s+(parseFloat(r.retirada||0)||0), 0);
|
||||
const despesas = estado.despesas.reduce((s,r) => s+(r.valor||0), 0);
|
||||
const vales = estado.vales.reduce((s,r) => s+(r.valor||0), 0);
|
||||
const receber = estado.receber.reduce((s,r) => s+(r.valor||0), 0);
|
||||
const cancelamentos = estado.cancelamentos.reduce((s,r) => s+(r.valor||0), 0);
|
||||
const sangrias = estado.sangrias.reduce((s,r) => s+(parseNum(r.retirada||0)||0), 0);
|
||||
const despesas = estado.despesas.reduce((s,r) => s+(parseNum(r.valor||0)||0), 0);
|
||||
const vales = estado.vales.reduce((s,r) => s+(parseNum(r.valor||0)||0), 0);
|
||||
const receber = estado.receber.reduce((s,r) => s+(parseNum(r.valor||0)||0), 0);
|
||||
const cancelamentos = estado.cancelamentos.reduce((s,r) => s+(parseNum(r.valor||0)||0), 0);
|
||||
const pixcnpj = estado.pixcnpj.reduce((s,r) => s+(parseNum(r.valor||0)||0), 0);
|
||||
const fechamento = parseNum(val('f-fech-dinheiro')) + parseNum(val('f-fech-cartoes')) + parseNum(val('f-fech-receber'));
|
||||
const saldoEsp = parseNum(val('f-saldo-esperado'));
|
||||
const diferenca = (parseNum(val('sys-troco')) || 0) - saldoEsp;
|
||||
const diffClass = diferenca === 0 ? '#2e7d32' : (diferenca > 0 ? '#e65100' : '#c0272d');
|
||||
document.getElementById('confirm-summary').innerHTML =
|
||||
`<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">
|
||||
@@ -1189,13 +1467,15 @@ function openConfirmModal() {
|
||||
<div>📝 Despesas: <strong>${fmt(despesas)}</strong></div>
|
||||
<div>💰 Vales: <strong>${fmt(vales)}</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>💳 Alimentação: <strong>${fmt(cartAli)}</strong></div>
|
||||
<div>❌ Cancelamentos: <strong>${fmt(cancelamentos)}</strong></div>
|
||||
<div>📋 Fechamento: <strong>${fmt(fechamento)}</strong></div>
|
||||
<hr style="border:none;border-top:1px dashed #b3d9f7;margin:6px 0">
|
||||
<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>`;
|
||||
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('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 () => {
|
||||
closeConfirmModal();
|
||||
const btn = document.getElementById('btn-recibo');
|
||||
btn.disabled = true;
|
||||
try {
|
||||
await tauriRecibo();
|
||||
alert('✅ Impressão enviada!');
|
||||
} catch(e) {
|
||||
alert('Erro ao imprimir: ' + (e.message || e));
|
||||
} finally {
|
||||
btn.disabled = false;
|
||||
}
|
||||
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 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 data = {
|
||||
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
|
||||
@@ -1233,10 +1528,17 @@ document.getElementById('confirm-send').addEventListener('click', async () => {
|
||||
const r = await tauriEnviar();
|
||||
estado.sync_status = 'synced';
|
||||
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) {
|
||||
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 {
|
||||
btn.disabled = false;
|
||||
btn.textContent = '💾 Enviar Fechamento';
|
||||
@@ -1252,10 +1554,13 @@ document.getElementById('btn-recibo').addEventListener('click', async () => {
|
||||
const btn = document.getElementById('btn-recibo');
|
||||
btn.disabled = true;
|
||||
try {
|
||||
// Tenta ESC/POS primeiro; se falhar, abre preview no navegador
|
||||
await tauriRecibo();
|
||||
alert('✅ Impressão enviada!');
|
||||
// Se chegou aqui sem erro, ESC/POS funcionou
|
||||
} 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 {
|
||||
btn.disabled = false;
|
||||
}
|
||||
@@ -1268,33 +1573,122 @@ document.getElementById('btn-limpar').addEventListener('click', () => {
|
||||
});
|
||||
|
||||
document.getElementById('btn-anterior').addEventListener('click', async () => {
|
||||
const d = new Date(estado.data); d.setDate(d.getDate() - 1);
|
||||
const prevDate = d.toISOString().split('T')[0];
|
||||
const loja = val('f-loja') || 'União';
|
||||
|
||||
// Carrega lista de 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();
|
||||
setVal('f-data', prevDate);
|
||||
estado.data = prevDate;
|
||||
setVal('f-data', selectedDate);
|
||||
estado.data = selectedDate;
|
||||
estado.uuid = crypto.randomUUID ? crypto.randomUUID() : Date.now().toString();
|
||||
estado.id_local = null;
|
||||
syncHeader();
|
||||
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.operador) setVal('f-operador', rascunho.operador);
|
||||
if (rascunho.turno) setVal('f-turno', rascunho.turno);
|
||||
estado.operador = rascunho.operador || '';
|
||||
estado.turno = rascunho.turno || '';
|
||||
if (rascunho.operador) { setVal('f-operador', rascunho.operador); estado.operador = rascunho.operador; }
|
||||
if (rascunho.turno) { setVal('f-turno', rascunho.turno); estado.turno = rascunho.turno; }
|
||||
if (rascunho.saldo_troco) setVal('sys-troco', fmtNum(rascunho.saldo_troco));
|
||||
if (rascunho.saldo_esperado) setVal('f-saldo-esperado', fmtNum(rascunho.saldo_esperado));
|
||||
if (rascunho.observacoes) { setVal('f-obs', rascunho.observacoes); estado.observacoes = rascunho.observacoes; }
|
||||
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);
|
||||
Object.keys(TABLES).forEach(renderTable);
|
||||
updateTotals();
|
||||
scheduleSave();
|
||||
});
|
||||
|
||||
|
||||
document.getElementById('f-obs').addEventListener('input', e => { estado.observacoes = e.target.value; scheduleSave(); });
|
||||
|
||||
// ─── Boot ─────────────────────────────────────────────────────────────────
|
||||
|
||||
Reference in New Issue
Block a user