From 9176b764fdf52808413603576e8899c1ce7f54d1 Mon Sep 17 00:00:00 2001 From: root Date: Wed, 24 Jun 2026 18:31:03 +0000 Subject: [PATCH] feat: alinha recibo ESC/POS com formato do screenshot MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ReciboData agora recebe arrays (sangrias_arr, despesas_arr, etc.) to_escpos() gera: SANGRIAS → ENTRADAS → DESPESAS → PIX CNPJ → VALES → A RECEBER → CARTÕES → CANCELAMENTOS → TROCO → OBSERVAÇÕES → TOTAL SALDO CAIXA → SALDO ESPERADO → DIFERENÇA Preview impressao: modal com iframe 360x600px + botao Fechar + botao Imprimir — usuario consegue voltar ao app --- src-tauri/src/plugins/printer.rs | 208 ++++++++++++++++++++++++++++--- src/index.html | 94 +++++++++----- 2 files changed, 253 insertions(+), 49 deletions(-) diff --git a/src-tauri/src/plugins/printer.rs b/src-tauri/src/plugins/printer.rs index 028c6b3..55dedaf 100644 --- a/src-tauri/src/plugins/printer.rs +++ b/src-tauri/src/plugins/printer.rs @@ -31,6 +31,50 @@ use crate::plugins::escpos; // ── Struct de dados do recibo ──────────────────────────────────────────────── +/// Linha de uma tabela no recibo (despesas, sangrias, vales, etc.) +#[derive(Debug, Serialize, Deserialize, Clone, Default)] +pub struct ReciboLinha { + #[serde(default)] + pub desc: Option, + #[serde(rename = "hora", default)] + pub hora: Option, + #[serde(rename = "nome", default)] + pub nome: Option, + #[serde(default)] + pub retirada: Option, + #[serde(default)] + pub valor: Option, + #[serde(default)] + pub obs: Option, + #[serde(default)] + pub gerente: Option, + #[serde(default)] + pub numero: Option, + #[serde(default)] + pub motivo: Option, +} + +impl ReciboLinha { + fn fmt_v(&self) -> String { + let v = self.retirada.or(self.valor).unwrap_or(0.0); + format!("R$ {:.2}", v).replace('.', ",") + } + fn label(&self) -> String { + self.desc + .clone() + .or(self.nome.clone()) + .or(self.hora.clone().map(|h| format!("{}h", h))) + .unwrap_or_default() + } + fn obs(&self) -> String { + self.gerente + .clone() + .or(self.obs.clone()) + .or(self.motivo.clone()) + .unwrap_or_default() + } +} + #[derive(Debug, Serialize, Deserialize, Clone)] pub struct ReciboData { pub loja: String, @@ -41,16 +85,39 @@ pub struct ReciboData { pub fechamento: f64, pub saldo_esperado: f64, pub diferenca: f64, - pub sangrias: f64, - pub despesas: f64, - pub vales: f64, - pub areceber: f64, + // Totais de cartões pub credito: f64, pub debito: f64, pub alimentacao: f64, pub voucher: f64, + // Totais diversos (mantidos para compatibilidade com JSON antigo) + #[serde(default)] + pub sangrias: f64, + #[serde(default)] + pub despesas: f64, + #[serde(default)] + pub vales: f64, + #[serde(default)] + pub areceber: f64, + #[serde(default)] pub pixcnpj: f64, + #[serde(default)] pub cancelamentos: f64, + // Arrays para linhas detalhadas (recebidos do frontend) + #[serde(default)] + pub sangrias_arr: Vec, + #[serde(default)] + pub despesas_arr: Vec, + #[serde(default)] + pub pixcnpj_arr: Vec, + #[serde(default)] + pub vales_arr: Vec, + #[serde(default)] + pub receber_arr: Vec, + #[serde(default)] + pub cancelamentos_arr: Vec, + #[serde(default)] + pub observacoes: Option, } impl ReciboData { @@ -59,6 +126,7 @@ impl ReciboData { } /// Gera todos os bytes ESC/POS do recibo 80mm + /// Layout: SANGRIAS → ENTRADAS → DESPESAS → PIX CNPJ → VALES → A RECEBER → CARTÕES → CANCELAMENTOS → TROCO → OBSERVAÇÕES → TOTAL SALDO CAIXA → SALDO ESPERADO → DIFERENÇA pub fn to_escpos(&self) -> Vec { let mut out = Vec::new(); @@ -78,40 +146,141 @@ impl ReciboData { out.extend(escpos::kv("Turno:", &self.turno)); out.extend(escpos::divider()); - // Entradas + // ── SANGRIAS ── + out.extend(escpos::line_bold("SANGRIAS")); + for linha in &self.sangrias_arr { + if linha.retirada.is_some() || linha.valor.is_some() { + let label = linha.label(); + let obs = linha.obs(); + out.extend(escpos::kv(&label, &linha.fmt_v())); + if !obs.is_empty() { + out.extend(escpos::line_center(&format!(" {}", obs))); + } + } + } + out.extend(escpos::divider()); + + // ── ENTRADAS ── out.extend(escpos::line_bold("ENTRADAS")); out.extend(escpos::kv("Saldo Troco:", &self.fmt_money(self.saldo_troco))); out.extend(escpos::kv("Fechamento:", &self.fmt_money(self.fechamento))); out.extend(escpos::kv("Saldo Esperado:", &self.fmt_money(self.saldo_esperado))); out.extend(escpos::divider()); - // 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()); + // ── DESPESAS ── + if !self.despesas_arr.is_empty() { + out.extend(escpos::line_bold("DESPESAS")); + for linha in &self.despesas_arr { + if linha.valor.is_some() { + let label = linha.label(); + let obs = linha.obs(); + out.extend(escpos::kv(&label, &linha.fmt_v())); + if !obs.is_empty() { + out.extend(escpos::line_center(&format!(" {}", obs))); + } + } + } + out.extend(escpos::divider()); + } - // Cartões - out.extend(escpos::line_bold("CARTÕES")); + // ── PIX CNPJ ── + if !self.pixcnpj_arr.is_empty() || self.pixcnpj > 0.0 { + out.extend(escpos::line_bold("PIX CNPJ")); + for linha in &self.pixcnpj_arr { + if linha.valor.is_some() { + let label = linha.nome.clone().unwrap_or_else(|| "PIX CNPJ".to_string()); + out.extend(escpos::kv(&label, &linha.fmt_v())); + } + } + out.extend(escpos::divider()); + } + + // ── VALES ── + if !self.vales_arr.is_empty() { + out.extend(escpos::line_bold("VALES")); + for linha in &self.vales_arr { + if linha.valor.is_some() { + let label = linha.label(); + let obs = linha.obs(); + out.extend(escpos::kv(&label, &linha.fmt_v())); + if !obs.is_empty() { + out.extend(escpos::line_center(&format!(" {}", obs))); + } + } + } + out.extend(escpos::divider()); + } + + // ── A RECEBER ── + if !self.receber_arr.is_empty() { + out.extend(escpos::line_bold("A RECEBER")); + for linha in &self.receber_arr { + if linha.valor.is_some() { + let label = linha.label(); + out.extend(escpos::kv(&label, &linha.fmt_v())); + } + } + out.extend(escpos::divider()); + } + + // ── CARTÕES ── + out.extend(escpos::line_bold("CARTOES")); 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("PIX Cartao:", &self.fmt_money(self.voucher))); out.extend(escpos::kv("Cancelamentos:", &self.fmt_money(self.cancelamentos))); out.extend(escpos::divider()); - // Diferença + // ── CANCELAMENTOS (detalhe) ── + if !self.cancelamentos_arr.is_empty() { + out.extend(escpos::line_bold("CANCELAMENTOS")); + for linha in &self.cancelamentos_arr { + if linha.valor.is_some() { + let num = linha.numero.clone().unwrap_or_default(); + out.extend(escpos::kv(&num, &linha.fmt_v())); + if let Some(ref m) = linha.motivo { + if !m.is_empty() { + out.extend(escpos::line_center(&format!(" {}", m))); + } + } + } + } + out.extend(escpos::divider()); + } + + // ── TROCO ── + out.extend(escpos::line_bold("TROCO")); + out.extend(escpos::kv("Troco:", &self.fmt_money(self.saldo_troco))); + out.extend(escpos::divider()); + + // ── OBSERVAÇÕES ── + if let Some(ref obs) = self.observacoes { + if !obs.trim().is_empty() { + out.extend(escpos::line_bold("OBSERVACOES")); + out.extend(escpos::line_center(obs.trim())); + out.extend(escpos::divider()); + } + } + + // ── TOTAL SALDO CAIXA ── + out.extend(escpos::line_bold("TOTAL SALDO DE CAIXA")); + out.extend(escpos::kv("Total:", &self.fmt_money(self.fechamento))); + out.extend(escpos::divider()); + + // ── SALDO ESPERADO ── + out.extend(escpos::line_bold("SALDO ESPERADO")); + out.extend(escpos::kv("Esperado:", &self.fmt_money(self.saldo_esperado))); + out.extend(escpos::divider()); + + // ── DIFERENÇA ── + out.extend(escpos::line_bold("DIFERENCA")); let diff_str = if self.diferenca >= 0.0 { format!("+{}", self.fmt_money(self.diferenca)) } else { self.fmt_money(self.diferenca) }; - out.extend(escpos::line_bold("DIFERENCA:")); out.extend(escpos::title(&diff_str)); - out.extend(escpos::blank_lines(2)); // Rodapé out.extend(escpos::divider()); @@ -123,6 +292,7 @@ impl ReciboData { // Corta papel out.extend(escpos::FEED_CUT); + out.extend(escpos::CUT); out } diff --git a/src/index.html b/src/index.html index 05338d0..9b3bfd3 100644 --- a/src/index.html +++ b/src/index.html @@ -1329,44 +1329,78 @@ ${data.observacoes ? `
Obs: ${data.observacoes}
` : ''} } function openPrintPreview(data) { - // Cria iframe oculto para impressão — NÃO substitui o DOM do app - const iframe = document.createElement('iframe'); - iframe.style.cssText = 'position:fixed;right:0;bottom:0;width:0;height:0;border:none;'; - iframe.id = 'print-iframe'; - document.body.appendChild(iframe); + // Remove preview anterior se existir + const existing = document.getElementById('print-preview-overlay'); + if (existing) document.body.removeChild(existing); - const html = buildPrintHTML(data); - const printStyle = ` + const overlay = document.createElement('div'); + overlay.id = 'print-preview-overlay'; + overlay.style.cssText = ` + position: fixed; top: 0; left: 0; right: 0; bottom: 0; + background: rgba(0,0,0,0.85); z-index: 99999; + display: flex; flex-direction: column; align-items: center; + font-family: 'Courier New', monospace; + `; + + const previewStyle = ` `; + const html = buildPrintHTML(data); + const fullHtml = `Recibo${previewStyle}${html}`; + + const iframe = document.createElement('iframe'); + iframe.style.cssText = 'width:360px; height:600px; border:2px solid #555; margin-top:16px; background:#fff;'; + iframe.id = 'print-iframe'; + + const btnFechar = document.createElement('button'); + btnFechar.textContent = '✕ Fechar'; + btnFechar.style.cssText = ` + margin-top: 12px; padding: 10px 32px; + background: #444; color: #fff; border: none; border-radius: 6px; + font-size: 15px; cursor: pointer; + `; + btnFechar.onclick = () => { + document.body.removeChild(overlay); + }; + + const btnImprimir = document.createElement('button'); + btnImprimir.textContent = '🖨️ Imprimir'; + btnImprimir.style.cssText = ` + margin-top: 8px; padding: 10px 32px; + background: #e65100; color: #fff; border: none; border-radius: 6px; + font-size: 15px; cursor: pointer; + `; + btnImprimir.onclick = () => { + iframe.contentWindow.focus(); + iframe.contentWindow.print(); + }; + + overlay.appendChild(iframe); + overlay.appendChild(btnImprimir); + overlay.appendChild(btnFechar); + document.body.appendChild(overlay); + const iframeDoc = iframe.contentWindow.document; iframeDoc.open(); - iframeDoc.write(`Recibo${printStyle}${html}`); + iframeDoc.write(fullHtml); iframeDoc.close(); - - // Focus e imprime - iframe.contentWindow.focus(); - iframe.contentWindow.print(); - - // Remove iframe após impressão - iframe.contentWindow.onafterprint = () => { - document.body.removeChild(iframe); - }; - // Fallback: remove iframe após 5s (onafterprint pode não disparar em alguns browsers) - setTimeout(() => { - if (document.getElementById('print-iframe')) { - document.body.removeChild(iframe); - } - }, 5000); } function triggerWindowPrint() {