feat: alinha recibo ESC/POS com formato do screenshot
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
This commit is contained in:
@@ -31,6 +31,50 @@ use crate::plugins::escpos;
|
|||||||
|
|
||||||
// ── Struct de dados do recibo ────────────────────────────────────────────────
|
// ── 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<String>,
|
||||||
|
#[serde(rename = "hora", default)]
|
||||||
|
pub hora: Option<String>,
|
||||||
|
#[serde(rename = "nome", default)]
|
||||||
|
pub nome: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub retirada: Option<f64>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub valor: Option<f64>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub obs: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub gerente: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub numero: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub motivo: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
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)]
|
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||||
pub struct ReciboData {
|
pub struct ReciboData {
|
||||||
pub loja: String,
|
pub loja: String,
|
||||||
@@ -41,16 +85,39 @@ pub struct ReciboData {
|
|||||||
pub fechamento: f64,
|
pub fechamento: f64,
|
||||||
pub saldo_esperado: f64,
|
pub saldo_esperado: f64,
|
||||||
pub diferenca: f64,
|
pub diferenca: f64,
|
||||||
pub sangrias: f64,
|
// Totais de cartões
|
||||||
pub despesas: f64,
|
|
||||||
pub vales: f64,
|
|
||||||
pub areceber: f64,
|
|
||||||
pub credito: f64,
|
pub credito: f64,
|
||||||
pub debito: f64,
|
pub debito: f64,
|
||||||
pub alimentacao: f64,
|
pub alimentacao: f64,
|
||||||
pub voucher: 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,
|
pub pixcnpj: f64,
|
||||||
|
#[serde(default)]
|
||||||
pub cancelamentos: f64,
|
pub cancelamentos: f64,
|
||||||
|
// Arrays para linhas detalhadas (recebidos do frontend)
|
||||||
|
#[serde(default)]
|
||||||
|
pub sangrias_arr: Vec<ReciboLinha>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub despesas_arr: Vec<ReciboLinha>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub pixcnpj_arr: Vec<ReciboLinha>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub vales_arr: Vec<ReciboLinha>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub receber_arr: Vec<ReciboLinha>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub cancelamentos_arr: Vec<ReciboLinha>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub observacoes: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ReciboData {
|
impl ReciboData {
|
||||||
@@ -59,6 +126,7 @@ impl ReciboData {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Gera todos os bytes ESC/POS do recibo 80mm
|
/// 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<u8> {
|
pub fn to_escpos(&self) -> Vec<u8> {
|
||||||
let mut out = Vec::new();
|
let mut out = Vec::new();
|
||||||
|
|
||||||
@@ -78,40 +146,141 @@ impl ReciboData {
|
|||||||
out.extend(escpos::kv("Turno:", &self.turno));
|
out.extend(escpos::kv("Turno:", &self.turno));
|
||||||
out.extend(escpos::divider());
|
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::line_bold("ENTRADAS"));
|
||||||
out.extend(escpos::kv("Saldo Troco:", &self.fmt_money(self.saldo_troco)));
|
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("Fechamento:", &self.fmt_money(self.fechamento)));
|
||||||
out.extend(escpos::kv("Saldo Esperado:", &self.fmt_money(self.saldo_esperado)));
|
out.extend(escpos::kv("Saldo Esperado:", &self.fmt_money(self.saldo_esperado)));
|
||||||
out.extend(escpos::divider());
|
out.extend(escpos::divider());
|
||||||
|
|
||||||
// Débitos
|
// ── DESPESAS ──
|
||||||
out.extend(escpos::line_bold("DEBITOS"));
|
if !self.despesas_arr.is_empty() {
|
||||||
out.extend(escpos::kv("Sangrias:", &self.fmt_money(self.sangrias)));
|
out.extend(escpos::line_bold("DESPESAS"));
|
||||||
out.extend(escpos::kv("Despesas:", &self.fmt_money(self.despesas)));
|
for linha in &self.despesas_arr {
|
||||||
out.extend(escpos::kv("Vales:", &self.fmt_money(self.vales)));
|
if linha.valor.is_some() {
|
||||||
out.extend(escpos::kv("A Receber:", &self.fmt_money(self.areceber)));
|
let label = linha.label();
|
||||||
out.extend(escpos::kv("PIX CNPJ:", &self.fmt_money(self.pixcnpj)));
|
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());
|
out.extend(escpos::divider());
|
||||||
|
}
|
||||||
|
|
||||||
// Cartões
|
// ── PIX CNPJ ──
|
||||||
out.extend(escpos::line_bold("CARTÕES"));
|
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("Credito:", &self.fmt_money(self.credito)));
|
||||||
out.extend(escpos::kv("Debito:", &self.fmt_money(self.debito)));
|
out.extend(escpos::kv("Debito:", &self.fmt_money(self.debito)));
|
||||||
out.extend(escpos::kv("Alimentacao:", &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("PIX Cartao:", &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());
|
||||||
|
|
||||||
// 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 {
|
let diff_str = if self.diferenca >= 0.0 {
|
||||||
format!("+{}", self.fmt_money(self.diferenca))
|
format!("+{}", self.fmt_money(self.diferenca))
|
||||||
} else {
|
} else {
|
||||||
self.fmt_money(self.diferenca)
|
self.fmt_money(self.diferenca)
|
||||||
};
|
};
|
||||||
out.extend(escpos::line_bold("DIFERENCA:"));
|
|
||||||
out.extend(escpos::title(&diff_str));
|
out.extend(escpos::title(&diff_str));
|
||||||
out.extend(escpos::blank_lines(2));
|
|
||||||
|
|
||||||
// Rodapé
|
// Rodapé
|
||||||
out.extend(escpos::divider());
|
out.extend(escpos::divider());
|
||||||
@@ -123,6 +292,7 @@ impl ReciboData {
|
|||||||
|
|
||||||
// Corta papel
|
// Corta papel
|
||||||
out.extend(escpos::FEED_CUT);
|
out.extend(escpos::FEED_CUT);
|
||||||
|
out.extend(escpos::CUT);
|
||||||
|
|
||||||
out
|
out
|
||||||
}
|
}
|
||||||
|
|||||||
+63
-29
@@ -1329,44 +1329,78 @@ ${data.observacoes ? `<div class="obs">Obs: ${data.observacoes}</div>` : ''}
|
|||||||
}
|
}
|
||||||
|
|
||||||
function openPrintPreview(data) {
|
function openPrintPreview(data) {
|
||||||
// Cria iframe oculto para impressão — NÃO substitui o DOM do app
|
// Remove preview anterior se existir
|
||||||
const iframe = document.createElement('iframe');
|
const existing = document.getElementById('print-preview-overlay');
|
||||||
iframe.style.cssText = 'position:fixed;right:0;bottom:0;width:0;height:0;border:none;';
|
if (existing) document.body.removeChild(existing);
|
||||||
iframe.id = 'print-iframe';
|
|
||||||
document.body.appendChild(iframe);
|
|
||||||
|
|
||||||
const html = buildPrintHTML(data);
|
const overlay = document.createElement('div');
|
||||||
const printStyle = `
|
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 = `
|
||||||
<style>
|
<style>
|
||||||
@media print {
|
body { width: 80mm; margin: 0 auto; font-family: 'Courier New', monospace; font-size: 13px; background: #fff; padding: 10px; }
|
||||||
body { width: 80mm; margin: 0 auto; font-family: 'Courier New', monospace; font-size: 13px; }
|
.header { text-align: center; margin-bottom: 8px; }
|
||||||
.no-print { display: none !important; }
|
.header h1 { font-size: 18px; }
|
||||||
}
|
.header h2 { font-size: 14px; font-weight: normal; }
|
||||||
body { padding: 10px; }
|
hr { border: none; border-top: 1px dashed #000; margin: 6px 0; }
|
||||||
.print-actions { text-align: center; margin-bottom: 10px; }
|
.section-title { font-weight: bold; font-size: 14px; border-top: 1px dashed #000; padding-top: 4px; margin-top: 6px; }
|
||||||
.print-actions button { padding: 8px 16px; cursor: pointer; background: #e65100; color: #fff; border: none; border-radius: 4px; font-size: 14px; }
|
.row { display: flex; justify-content: space-between; padding: 2px 0; }
|
||||||
|
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; }
|
||||||
|
.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 { background: #fff; } }
|
||||||
</style>
|
</style>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const iframeDoc = iframe.contentWindow.document;
|
const html = buildPrintHTML(data);
|
||||||
iframeDoc.open();
|
const fullHtml = `<!DOCTYPE html><html><head><meta charset="UTF-8"><title>Recibo</title>${previewStyle}</head><body>${html}</body></html>`;
|
||||||
iframeDoc.write(`<!DOCTYPE html><html><head><title>Recibo</title>${printStyle}</head><body>${html}</body></html>`);
|
|
||||||
iframeDoc.close();
|
|
||||||
|
|
||||||
// Focus e imprime
|
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.focus();
|
||||||
iframe.contentWindow.print();
|
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(() => {
|
overlay.appendChild(iframe);
|
||||||
if (document.getElementById('print-iframe')) {
|
overlay.appendChild(btnImprimir);
|
||||||
document.body.removeChild(iframe);
|
overlay.appendChild(btnFechar);
|
||||||
}
|
document.body.appendChild(overlay);
|
||||||
}, 5000);
|
|
||||||
|
const iframeDoc = iframe.contentWindow.document;
|
||||||
|
iframeDoc.open();
|
||||||
|
iframeDoc.write(fullHtml);
|
||||||
|
iframeDoc.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
function triggerWindowPrint() {
|
function triggerWindowPrint() {
|
||||||
|
|||||||
Reference in New Issue
Block a user