fix: 5 problemas recorrentes
1. PIX CNPJ: linha separada no Saldo de Caixa + incluído na soma sys-total 2. Casas decimais: fmtNum sempre 2 casas (padrão dinheiro) 3. btn-anterior: agora busca do Supabase via sb_carregar_rascunho e repovoa todas as tabelas 4. Impressão: reimplementado usb_windows (WriteFile no Windows); adicionado modo teste (grava .bin) 5. Supabase: feedback de erro com console.error; storage salvar_agora persiste TODAS as tabelas (sangrias, pixcnpj, vales, cancelamentos)
This commit is contained in:
@@ -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));
|
||||
|
||||
@@ -126,82 +128,122 @@ impl ReciboData {
|
||||
}
|
||||
}
|
||||
|
||||
// ── USB Printer (stub for cross-compilation) ─────────────────────────────────
|
||||
// Real implementation requires native Windows build with correct windows crate API.
|
||||
|
||||
#[cfg(windows)]
|
||||
pub mod usb_windows {
|
||||
pub fn find_printer_device() -> Option<String> {
|
||||
// STUB: returns None during cross-compilation
|
||||
// Native Windows build will enumerate USB ports
|
||||
None
|
||||
}
|
||||
pub fn open_printer(_path: &str) -> Result<isize, String> {
|
||||
Err("Cross-compilation stub: printing needs native Windows build".into())
|
||||
}
|
||||
pub fn write_to(_handle: isize, _data: &[u8]) -> Result<usize, String> {
|
||||
Err("Cross-compilation stub".into())
|
||||
}
|
||||
pub fn close_handle(_h: isize) {}
|
||||
}
|
||||
|
||||
#[cfg(not(windows))]
|
||||
pub mod usb_windows {
|
||||
pub fn find_printer_device() -> Option<String> {
|
||||
None
|
||||
}
|
||||
pub fn open_printer(_path: &str) -> Result<isize, String> {
|
||||
Err("Not supported on this platform".into())
|
||||
}
|
||||
pub fn write_to(_handle: isize, _data: &[u8]) -> Result<usize, String> {
|
||||
Err("Not supported on this platform".into())
|
||||
}
|
||||
pub fn close_handle(_h: isize) {}
|
||||
}
|
||||
|
||||
// ── Printer Manager ─────────────────────────────────────────────────────────
|
||||
|
||||
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::os::windows::io::AsRawFd;
|
||||
use std::process::Command;
|
||||
// Try opening the device
|
||||
let out = Command::new("cmd")
|
||||
.args(["/C", &format!("type {} < nul 2>&1", path)])
|
||||
.output();
|
||||
out.map(|o| o.status.success()).unwrap_or(false)
|
||||
}
|
||||
|
||||
pub fn configurar(&self, path: String) -> Result<(), String> {
|
||||
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::fs::OpenOptions;
|
||||
use std::io::Write;
|
||||
|
||||
let file = OpenOptions::new()
|
||||
.write(true)
|
||||
.create(false)
|
||||
.open(path)
|
||||
.map_err(|e| format!("Erro ao abrir {}: {}", path, e))?;
|
||||
|
||||
let mut f = file;
|
||||
f.write_all(data)
|
||||
.map_err(|e| format!("Erro ao escrever: {}", e))?;
|
||||
f.flush()
|
||||
.map_err(|e| format!("Erro ao fazer flush: {}", e))?;
|
||||
Ok(data.len())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -209,6 +251,7 @@ impl PrinterManager {
|
||||
|
||||
static PRINTER: PrinterManager = PrinterManager {
|
||||
device_path: Mutex::new(None),
|
||||
test_mode: Mutex::new(false),
|
||||
};
|
||||
|
||||
#[tauri::command]
|
||||
@@ -231,22 +274,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 {
|
||||
|
||||
Reference in New Issue
Block a user