Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9176b764fd | |||
| 5cab38a27c | |||
| 5b1f3a2691 | |||
| 7e57d30863 | |||
| 7a93a30c22 | |||
| 230304e878 | |||
| d185f82bd5 |
+13
@@ -7,3 +7,16 @@ src-tauri/gen/
|
||||
*.msi
|
||||
*.dmg
|
||||
.DS_Store
|
||||
|
||||
# Binarios compilados
|
||||
*.exe
|
||||
*.tar.gz
|
||||
*.zip
|
||||
fechamento-caixa.exe
|
||||
fechamento-caixa-latest.tar.gz
|
||||
WebView2Loader.dll
|
||||
src-tauri/target/x86_64-pc-windows-gnu/release/*.exe
|
||||
src-tauri/target/x86_64-pc-windows-gnu/release/*.dll
|
||||
|
||||
# Logs
|
||||
*.log
|
||||
|
||||
@@ -0,0 +1,140 @@
|
||||
# Fechamento de Caixa — O Frangão
|
||||
|
||||
App desktop nativo para fechamento de caixa com impressão em impressora térmica 80mm.
|
||||
|
||||
## Stack
|
||||
|
||||
- **Backend:** Tauri 2 + Rust
|
||||
- **Frontend:** HTML + CSS + JavaScript (vanilla, ~1700 linhas)
|
||||
- **Storage local:** SQLite (rusqlite) — funciona offline
|
||||
- **Sync:** Supabase REST API
|
||||
- **Impressão:** ESC/POS via USB (TM-T20 e compatíveis, Windows)
|
||||
|
||||
## Estrutura do Projeto
|
||||
|
||||
```
|
||||
fechamento-caixa/
|
||||
├── src/
|
||||
│ └── index.html # Frontend completo (HTML + CSS + JS inline)
|
||||
├── src-tauri/
|
||||
│ ├── Cargo.toml # Dependências Rust
|
||||
│ ├── tauri.conf.json # Config do app Tauri
|
||||
│ └── src/
|
||||
│ ├── main.rs # Entry point — registra comandos Tauri
|
||||
│ └── plugins/
|
||||
│ ├── supabase.rs # Integração Supabase (REST API)
|
||||
│ ├── printer.rs # Geração ESC/POS + envio USB
|
||||
│ ├── storage.rs # SQLite local
|
||||
│ ├── app.rs # Config da app (loja, operador default)
|
||||
│ └── escpos.rs # Helpers ESC/POS (INIT, CUT, divider, etc.)
|
||||
└── README.md
|
||||
```
|
||||
|
||||
## Pré-requisitos
|
||||
|
||||
- Rust 1.70+ (`rustup install stable`)
|
||||
- Node.js 18+ (para build do frontend Tauri)
|
||||
- Windows 10/11 (impressão USB só funciona no Windows)
|
||||
|
||||
## Setup
|
||||
|
||||
### 1. Clonar o repositório
|
||||
|
||||
```bash
|
||||
git clone https://git.ofrangao.com.br/filipe/fechamento-caixa.git
|
||||
cd fechamento-caixa
|
||||
```
|
||||
|
||||
### 2. Configurar chaves do Supabase
|
||||
|
||||
Crie o arquivo `config.json` em `%LOCALAPPDATA%\FechamentoCaixa\config.json` (Windows):
|
||||
|
||||
```json
|
||||
{
|
||||
"SUPABASE_ANON_KEY": "sua_chave_anon_aqui",
|
||||
"SUPABASE_SERVICE_KEY": "sua_chave_service_role_aqui"
|
||||
}
|
||||
```
|
||||
|
||||
O app procura esse arquivo na inicialização. Sem ele, tenta ler das variáveis de ambiente `SUPABASE_ANON_KEY` e `SUPABASE_SERVICE_KEY`.
|
||||
|
||||
### 3. Build
|
||||
|
||||
```bash
|
||||
cd src-tauri
|
||||
cargo build --release --target x86_64-pc-windows-gnu
|
||||
```
|
||||
|
||||
O executável fica em:
|
||||
```
|
||||
src-tauri/target/x86_64-pc-windows-gnu/release/fechamento-caixa.exe
|
||||
```
|
||||
|
||||
Para gerar o pacote de distribuição:
|
||||
```bash
|
||||
cp src-tauri/target/x86_64-pc-windows-gnu/release/fechamento-caixa.exe .
|
||||
cp src-tauri/target/x86_64-pc-windows-gnu/release/WebView2Loader.dll .
|
||||
tar -czf fechamento-caixa-vX.X.X.tar.gz fechamento-caixa.exe WebView2Loader.dll
|
||||
```
|
||||
|
||||
## Códigos de comando Tauri
|
||||
|
||||
O frontend chama esses comandos via `window.__TAURI__.core.invoke()`:
|
||||
|
||||
| Comando | Arquivo | Descrição |
|
||||
|---------|---------|-----------|
|
||||
| `salvar_fechamento` | storage.rs | Salva no SQLite local |
|
||||
| `carregar_rascunho` | storage.rs | Busca rascunho por loja+data |
|
||||
| `listar_fechamentos` | storage.rs | Lista histórico do SQLite |
|
||||
| `buscar_fechamento_por_id` | storage.rs | Busca por ID no SQLite |
|
||||
| `sb_salvar_fechamento` | supabase.rs | Envia para Supabase |
|
||||
| `sb_carregar_rascunho` | supabase.rs | Busca rascunho no Supabase |
|
||||
| `sb_listar_recentes` | supabase.rs | Lista fechamentos recentes |
|
||||
| `sb_buscar_por_id` | supabase.rs | Busca por ID no Supabase |
|
||||
| `sb_salvar_listas` | supabase.rs | Salva operadores/gerentes |
|
||||
| `sb_carregar_listas` | supabase.rs | Carrega listas do servidor |
|
||||
| `sb_online` | supabase.rs | Verifica conectividade |
|
||||
| `imprimir_recibo` | printer.rs | Envia bytes ESC/POS pra USB |
|
||||
| `detectar_impressora` | printer.rs | Detecta porta USB |
|
||||
| `configurar_impressora` | printer.rs | Define porta manual |
|
||||
| `teste_impressora` | printer.rs | Imprime página de teste |
|
||||
| `get_loja` / `set_loja` | app.rs | Loja atual |
|
||||
| `get_config` / `set_config` | app.rs | Config completo |
|
||||
|
||||
## Fluxo de dados
|
||||
|
||||
```
|
||||
[Usuário preenche formulário]
|
||||
│
|
||||
▼ (auto-save a cada 1.5s)
|
||||
SQLite local (fechamento.db)
|
||||
│
|
||||
├── [Botão Enviar] ──► Supabase (fechamentos_web)
|
||||
│
|
||||
└── [Botão Recibo] ──► printer.rs ──► to_escpos() ──► USB
|
||||
│
|
||||
├── Sucesso: imprime
|
||||
└── Falha: openPrintPreview() ──► iframe ──► window.print()
|
||||
```
|
||||
|
||||
## Campos do formulário
|
||||
|
||||
| Campo | Tabela Supabase | Notas |
|
||||
|-------|----------------|-------|
|
||||
| uuid, loja, data, operador, turno | fechamentos_web | PK composto |
|
||||
| saldo_troco, saldo_esperado, fechamento, diferenca | fechamentos_web | diferenca = fechamento - saldo_esperado |
|
||||
| despesas[], sangrias[], vales[], receber[], pixcnpj[], cancelamentos[] | dados (JSON) | Arrays de {desc/nome, valor, ...} |
|
||||
| cartoes{credito[],debito[],alimentacao[],pix[]} | dados (JSON) | Arrays de {valor} |
|
||||
| operadores[], gerentes[], despesas_custom[], clientes[] | listas_personalizadas | Listas por loja |
|
||||
|
||||
## Problemas conhecidos
|
||||
|
||||
- Impressão USB só funciona no Windows
|
||||
- `service_role_key` no cliente desktop é risco de segurança — usar apenas anon_key + RLS
|
||||
- Sem testes automatizados
|
||||
|
||||
## Version History
|
||||
|
||||
- **v1.2.7** — 2026-06-24: 4 bugs críticos concertados (impressão, enviar, dia anterior, localStorage)
|
||||
- **v1.2.6** — 2026-06-24: WebView2Loader.dll incluso; init() restaura operador+turno
|
||||
- **v1.2.5** — 2026-06-24: diferenca = fechamento - esperado (não mais saldo_troco - esperado)
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Generated
-35
@@ -900,14 +900,12 @@ dependencies = [
|
||||
"rusqlite",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_yaml",
|
||||
"tauri",
|
||||
"tauri-build",
|
||||
"tauri-plugin-dialog",
|
||||
"tauri-plugin-fs",
|
||||
"tauri-plugin-shell",
|
||||
"thiserror 2.0.18",
|
||||
"tokio",
|
||||
"uuid",
|
||||
]
|
||||
|
||||
@@ -3281,19 +3279,6 @@ dependencies = [
|
||||
"syn 2.0.118",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_yaml"
|
||||
version = "0.9.34+deprecated"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47"
|
||||
dependencies = [
|
||||
"indexmap 2.14.0",
|
||||
"itoa",
|
||||
"ryu",
|
||||
"serde",
|
||||
"unsafe-libyaml",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serialize-to-javascript"
|
||||
version = "0.1.2"
|
||||
@@ -4070,25 +4055,11 @@ dependencies = [
|
||||
"bytes",
|
||||
"libc",
|
||||
"mio",
|
||||
"parking_lot",
|
||||
"pin-project-lite",
|
||||
"signal-hook-registry",
|
||||
"socket2",
|
||||
"tokio-macros",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio-macros"
|
||||
version = "2.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.118",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio-native-tls"
|
||||
version = "0.3.1"
|
||||
@@ -4399,12 +4370,6 @@ version = "1.13.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8"
|
||||
|
||||
[[package]]
|
||||
name = "unsafe-libyaml"
|
||||
version = "0.2.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861"
|
||||
|
||||
[[package]]
|
||||
name = "untrusted"
|
||||
version = "0.9.0"
|
||||
|
||||
@@ -15,8 +15,6 @@ serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
rusqlite = { version = "0.32", features = ["bundled"] }
|
||||
reqwest = { version = "0.12", features = ["json", "blocking"] }
|
||||
tokio = { version = "1", features = ["full"] }
|
||||
serde_yaml = "0.9"
|
||||
uuid = { version = "1", features = ["v4"] }
|
||||
chrono = { version = "0.4", features = ["serde"] }
|
||||
log = "0.4"
|
||||
|
||||
@@ -8,6 +8,7 @@ pub const LF: &[u8] = b"\x0A";
|
||||
/// Cortar papel (parcial)
|
||||
pub const CUT: &[u8] = b"\x1D\x56\x01";
|
||||
/// Cortar papel (total)
|
||||
#[allow(dead_code)]
|
||||
pub const CUT_FULL: &[u8] = b"\x1D\x56\x00";
|
||||
/// Alimentar 3 linhas antes de cortar
|
||||
pub const FEED_CUT: &[u8] = b"\x1B\x64\x03";
|
||||
@@ -17,6 +18,7 @@ pub const BOLD_OFF: &[u8] = b"\x1B\x45\x00";
|
||||
/// Alinhamento
|
||||
pub const ALIGN_LEFT: &[u8] = b"\x1B\x61\x00";
|
||||
pub const ALIGN_CENTER: &[u8] = b"\x1B\x61\x01";
|
||||
#[allow(dead_code)]
|
||||
pub const ALIGN_RIGHT: &[u8] = b"\x1B\x61\x02";
|
||||
/// Fonte normal / dupla
|
||||
pub const FONT_NORMAL: &[u8] = b"\x1B\x21\x00";
|
||||
@@ -38,6 +40,7 @@ pub fn title(text: &str) -> Vec<u8> {
|
||||
}
|
||||
|
||||
/// Monta linha normal (alinhada à esquerda)
|
||||
#[allow(dead_code)]
|
||||
pub fn line(text: &str) -> Vec<u8> {
|
||||
let mut out = Vec::new();
|
||||
out.extend_from_slice(ALIGN_LEFT);
|
||||
|
||||
@@ -5,6 +5,7 @@ use std::sync::Mutex;
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
#[allow(dead_code)]
|
||||
pub enum PrinterError {
|
||||
#[error("Impressora não encontrada. Verifique se está conectada via USB e ligada.")]
|
||||
NotFound,
|
||||
@@ -30,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<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)]
|
||||
pub struct ReciboData {
|
||||
pub loja: String,
|
||||
@@ -40,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<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 {
|
||||
@@ -58,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<u8> {
|
||||
let mut out = Vec::new();
|
||||
|
||||
@@ -77,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)));
|
||||
// ── 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());
|
||||
@@ -122,6 +292,7 @@ impl ReciboData {
|
||||
|
||||
// Corta papel
|
||||
out.extend(escpos::FEED_CUT);
|
||||
out.extend(escpos::CUT);
|
||||
|
||||
out
|
||||
}
|
||||
|
||||
@@ -335,6 +335,7 @@ impl DbManager {
|
||||
}
|
||||
|
||||
/// Atualiza sync_status de um registro.
|
||||
#[allow(dead_code)]
|
||||
pub fn atualizar_sync(&self, uuid: &str, status: SyncStatus) -> Result<(), StorageError> {
|
||||
let conn = self.conn.lock().map_err(|_| StorageError::Lock)?;
|
||||
let now = Utc::now().format("%Y-%m-%dT%H:%M:%S%.3fZ").to_string();
|
||||
@@ -346,6 +347,7 @@ impl DbManager {
|
||||
}
|
||||
|
||||
/// Remove um registro (para debugging).
|
||||
#[allow(dead_code)]
|
||||
pub fn remover(&self, uuid: &str) -> Result<bool, StorageError> {
|
||||
let conn = self.conn.lock().map_err(|_| StorageError::Lock)?;
|
||||
let n = conn.execute("DELETE FROM fechamentos WHERE uuid = ?1", params![uuid])?;
|
||||
@@ -508,7 +510,7 @@ pub fn carregar_rascunho(
|
||||
// Simplifica para formato que o frontend espera
|
||||
let total_sangrias: f64 = e.tabelas.sangrias.iter().filter_map(|r| r.valor).sum();
|
||||
let total_despesas: f64 = e.tabelas.despesas.iter().filter_map(|r| r.valor).sum();
|
||||
let total_receber: f64 = e.tabelas.areceber.iter().filter_map(|r| r.valor).sum();
|
||||
let _total_receber: f64 = e.tabelas.areceber.iter().filter_map(|r| r.valor).sum();
|
||||
let despesa_rows: Vec<_> = e.tabelas.despesas.iter().filter_map(|r| {
|
||||
r.descricao.as_ref().map(|d| serde_json::json!({"desc": d, "valor": r.valor}))
|
||||
}).collect();
|
||||
|
||||
@@ -8,6 +8,7 @@ use tauri::State;
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
#[allow(dead_code)]
|
||||
pub enum SupabaseError {
|
||||
#[error("HTTP error: {0}")]
|
||||
Http(#[from] reqwest::Error),
|
||||
|
||||
+248
-165
@@ -1022,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);
|
||||
@@ -1034,7 +1036,7 @@ const LS_KEY = 'fc_v7';
|
||||
const CFG_KEY = 'fc_cfg';
|
||||
|
||||
function buildEstadoForBackend() {
|
||||
// Monta o objeto que vai para o Supabase — inclui TUDO, todos os detalhes
|
||||
// Monta o objeto exactly as o backend espera (storage.rs → salvar_fechamento)
|
||||
return {
|
||||
uuid: estado.uuid,
|
||||
id_local: estado.id_local,
|
||||
@@ -1042,22 +1044,20 @@ function buildEstadoForBackend() {
|
||||
data: estado.data,
|
||||
operador: estado.operador,
|
||||
turno: estado.turno,
|
||||
saldo_troco: parseNum(val('sys-troco')),
|
||||
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 })),
|
||||
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 || '' })),
|
||||
// Cartões com detalhes completos (não só totais)
|
||||
cartoes: {
|
||||
credito: estado.cartoes.credito.map(r => ({ valor: r.valor || 0, obs: r.obs || '' })),
|
||||
debito: estado.cartoes.debito.map(r => ({ valor: r.valor || 0, obs: r.obs || '' })),
|
||||
alimentacao: estado.cartoes.alimentacao.map(r => ({ valor: r.valor || 0, obs: r.obs || '' })),
|
||||
pix: estado.cartoes.pix.map(r => ({ valor: r.valor || 0, obs: r.obs || '' })),
|
||||
credito: estado.cartoes.credito.map(r => r.valor || 0),
|
||||
debito: estado.cartoes.debito.map(r => r.valor || 0),
|
||||
alimentacao: estado.cartoes.alimentacao.map(r => r.valor || 0),
|
||||
pix: estado.cartoes.pix.map(r => r.valor || 0),
|
||||
},
|
||||
observacoes: estado.observacoes,
|
||||
sync_status: estado.sync_status,
|
||||
@@ -1084,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 || '');
|
||||
@@ -1093,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);
|
||||
@@ -1195,37 +1232,42 @@ async function tauriEnviar() {
|
||||
// ─── Print Preview Window ──────────────────────────────────────────────────
|
||||
function buildPrintHTML(data) {
|
||||
const fmt = v => 'R$ ' + (v || 0).toFixed(2).replace('.', ',');
|
||||
const vSangrias = (data.sangrias||[]).reduce((s,r) => s+(parseFloat(r.retirada)||0), 0);
|
||||
const vDespesas = (data.despesas||[]).reduce((s,r) => s+(parseFloat(r.valor)||0), 0);
|
||||
const vVales = (data.vales||[]).reduce((s,r) => s+(parseFloat(r.valor)||0), 0);
|
||||
const vReceber = (data.receber||[]).reduce((s,r) => s+(parseFloat(r.valor)||0), 0);
|
||||
const vPixCnpj = (data.pixcnpj||[]).reduce((s,r) => s+(parseFloat(r.valor)||0), 0);
|
||||
const vCredito = data.cartoes?.credito?.reduce?.((s,r) => s+(parseFloat(r)||0), 0) || data.credito || 0;
|
||||
const vDebito = data.cartoes?.debito?.reduce?.((s,r) => s+(parseFloat(r)||0), 0) || data.debito || 0;
|
||||
const vAliment = data.cartoes?.alimentacao?.reduce?.((s,r) => s+(parseFloat(r)||0), 0) || data.alimentacao || 0;
|
||||
const vPixCartao = data.cartoes?.pix?.reduce?.((s,r) => s+(parseFloat(r)||0), 0) || 0;
|
||||
const vCancel = (data.cancelamentos||[]).reduce((s,r) => s+(parseFloat(r.valor)||0), 0);
|
||||
// Diferença = Fechamento Total − Saldo Esperado
|
||||
const vFechamento = (data.fechamento || 0);
|
||||
const diferenca = vFechamento - (data.saldo_esperado || 0);
|
||||
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');
|
||||
|
||||
const fmtRow = (label, valor) =>
|
||||
`<div class="row"><span>${label}</span><span>${fmt(valor)}</span></div>`;
|
||||
const fmtDetail = (label, valor, obs) =>
|
||||
`<div class="row sub"><span>${label}</span><span>${fmt(valor)}</span></div>`;
|
||||
|
||||
const mkDet = (arr, fmtFn) => {
|
||||
if (!arr || arr.length === 0) return '';
|
||||
return arr.map(r => {
|
||||
if (!r) return '';
|
||||
const v = fmtFn(r);
|
||||
const nome = r.nome || r.desc || r.hora || r.numero || '';
|
||||
const obs = r.obs || r.motivo || '';
|
||||
return fmtDetail(nome, v, obs);
|
||||
}).join('');
|
||||
};
|
||||
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>
|
||||
@@ -1233,122 +1275,132 @@ function buildPrintHTML(data) {
|
||||
<title>Relatório de Fechamento</title>
|
||||
<style>
|
||||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
body { font-family: 'Courier New', monospace; font-size: 12px; width: 80mm; margin: 0 auto; padding: 6px; }
|
||||
.header { text-align: center; margin-bottom: 6px; }
|
||||
.header h1 { font-size: 16px; }
|
||||
.header h2 { font-size: 12px; font-weight: normal; }
|
||||
hr { border: none; border-top: 1px dashed #000; margin: 4px 0; }
|
||||
.row { display: flex; justify-content: space-between; padding: 1px 0; }
|
||||
.row.sub { padding-left: 8px; font-size: 11px; color: #444; }
|
||||
.row strong { font-weight: bold; }
|
||||
.section-title { font-weight: bold; font-size: 12px; border-top: 1px dashed #000; padding-top: 3px; margin-top: 5px; }
|
||||
.grand-total { font-size: 14px; font-weight: bold; text-align: center; padding: 5px; border: 2px solid #000; margin: 6px 0; }
|
||||
.footer { text-align: center; font-size: 10px; color: #777; margin-top: 6px; }
|
||||
@media print { body { margin: 0; width: 80mm; } }
|
||||
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>
|
||||
<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="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">SANGRIAS</div>
|
||||
${mkDet(data.sangrias, r => parseFloat(r.retirada)||0)}
|
||||
${fmtRow('TOTAL SANGRIAS', vSangrias)}
|
||||
|
||||
<div class="section-title">DESPESAS</div>
|
||||
${mkDet(data.despesas, r => parseFloat(r.valor)||0)}
|
||||
${fmtRow('TOTAL DESPESAS', vDespesas)}
|
||||
|
||||
<div class="section-title">VALES</div>
|
||||
${mkDet(data.vales, r => parseFloat(r.valor)||0)}
|
||||
${fmtRow('TOTAL VALES', vVales)}
|
||||
|
||||
<div class="section-title">A RECEBER</div>
|
||||
${mkDet(data.receber, r => parseFloat(r.valor)||0)}
|
||||
${fmtRow('TOTAL A RECEBER', vReceber)}
|
||||
|
||||
<div class="section-title">PIX</div>
|
||||
<div class="section-title" style="font-size:11px;font-weight:normal">— Cartão —</div>
|
||||
${fmtRow('PIX CARTAO', vPixCartao)}
|
||||
<div class="section-title" style="font-size:11px;font-weight:normal">— CNPJ —</div>
|
||||
${mkDet(data.pixcnpj, r => parseFloat(r.valor)||0)}
|
||||
${fmtRow('TOTAL PIX CNPJ', vPixCnpj)}
|
||||
|
||||
<div class="section-title">CARTOES</div>
|
||||
${fmtRow('CREDITO', vCredito)}
|
||||
${fmtRow('DEBITO', vDebito)}
|
||||
${fmtRow('VOUCHER', vAliment)}
|
||||
|
||||
<div class="section-title">CANCELAMENTOS</div>
|
||||
${mkDet(data.cancelamentos, r => parseFloat(r.valor)||0)}
|
||||
${fmtRow('TOTAL CANCELAMENTOS', vCancel)}
|
||||
<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>
|
||||
${fmtRow('SALDO ESPERADO', data.saldo_esperado || 0)}
|
||||
${fmtRow('FECHAMENTO', vFechamento)}
|
||||
<div class="grand-total" style="color:${diffColor}">DIFERENCA: ${diffStr}</div>
|
||||
<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 para window.print()
|
||||
const html = buildPrintHTML(data);
|
||||
const style = document.createElement('style');
|
||||
style.id = '_print_style';
|
||||
style.textContent = `
|
||||
@media print {
|
||||
body { width: 80mm; margin: 0 auto; font-family: 'Courier New', monospace; font-size: 13px; }
|
||||
.no-print, .print-actions { display: none !important; }
|
||||
hr { border-top: 1px dashed #000 !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; margin-left: 8px; }
|
||||
.print-actions button:hover { background: #1d4ed8; }
|
||||
.print-actions .btn-back { background: #6b7280; }
|
||||
.print-actions .btn-back:hover { background: #4b5563; }
|
||||
// Remove preview anterior se existir
|
||||
const existing = document.getElementById('print-preview-overlay');
|
||||
if (existing) document.body.removeChild(existing);
|
||||
|
||||
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 actions = document.createElement('div');
|
||||
actions.className = 'print-actions no-print';
|
||||
actions.innerHTML =
|
||||
'<button class="btn-back" id="_btn_back_print">← Voltar ao App</button>' +
|
||||
'<button onclick="window.print()">🖨️ Imprimir (Ctrl+P)</button>';
|
||||
|
||||
// Guarda originais
|
||||
const originalBody = document.body.innerHTML;
|
||||
const originalOnAfterPrint = window.onafterprint;
|
||||
const previewStyle = `
|
||||
<style>
|
||||
body { width: 80mm; margin: 0 auto; font-family: 'Courier New', monospace; font-size: 13px; background: #fff; padding: 10px; }
|
||||
.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-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; }
|
||||
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>
|
||||
`;
|
||||
|
||||
// Botão voltar restaura sem reload
|
||||
document.addEventListener('click', function onBackClick(e) {
|
||||
if (e.target.id === '_btn_back_print') {
|
||||
document.body.innerHTML = originalBody;
|
||||
const oldStyle = document.getElementById('_print_style');
|
||||
if (oldStyle) oldStyle.remove();
|
||||
document.removeEventListener('click', onBackClick);
|
||||
window.onafterprint = originalOnAfterPrint;
|
||||
// Re-init app state — mínimo viable restore
|
||||
initApp();
|
||||
}
|
||||
});
|
||||
const html = buildPrintHTML(data);
|
||||
const fullHtml = `<!DOCTYPE html><html><head><meta charset="UTF-8"><title>Recibo</title>${previewStyle}</head><body>${html}</body></html>`;
|
||||
|
||||
document.body.innerHTML = html;
|
||||
document.head.appendChild(style);
|
||||
document.body.insertBefore(actions, document.body.firstChild);
|
||||
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';
|
||||
|
||||
window.onafterprint = () => {
|
||||
document.body.innerHTML = originalBody;
|
||||
const oldStyle = document.getElementById('_print_style');
|
||||
if (oldStyle) oldStyle.remove();
|
||||
window.onafterprint = originalOnAfterPrint;
|
||||
document.removeEventListener('click', onBackClick);
|
||||
initApp();
|
||||
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(fullHtml);
|
||||
iframeDoc.close();
|
||||
}
|
||||
|
||||
function triggerWindowPrint() {
|
||||
@@ -1372,7 +1424,6 @@ async function tauriRecibo() {
|
||||
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,
|
||||
@@ -1383,15 +1434,8 @@ async function tauriRecibo() {
|
||||
saldo_troco: sysTroco,
|
||||
saldo_esperado: saldoEsp,
|
||||
fechamento: fechamentoCounted,
|
||||
diferenca: sysTroco - saldoEsp,
|
||||
despesas: estado.despesas.map(r => ({ desc: r.desc||'', obs: r.obs||'', valor: r.valor||0 })),
|
||||
sangrias: estado.sangrias.map(r => ({ hora: r.hora||'', retirada: parseNum(r.retirada)||0, gerente: r.gerente||'' })),
|
||||
pixcnpj: estado.pixcnpj.map(r => ({ nome: r.nome||'', valor: r.valor||0 })),
|
||||
vales: estado.vales.map(r => ({ nome: r.nome||'', obs: r.obs||'', valor: r.valor||0 })),
|
||||
receber: estado.receber.map(r => ({ nome: r.nome||'', valor: r.valor||0 })),
|
||||
cancelamentos: estado.cancelamentos.map(r => ({ numero: r.numero||'', valor: r.valor||0, motivo: r.motivo||'' })),
|
||||
cartoes: { credito: cartCred, debito: cartDeb, alimentacao: cartAli, pix: cartPix },
|
||||
// Flat totals for ReciboData struct (Rust)
|
||||
diferenca: fechamentoCounted - saldoEsp,
|
||||
// Flat totals for ReciboData struct (Rust) — must be numbers
|
||||
credito: cartCred,
|
||||
debito: cartDeb,
|
||||
alimentacao: cartAli,
|
||||
@@ -1402,6 +1446,14 @@ async function tauriRecibo() {
|
||||
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,
|
||||
};
|
||||
@@ -1480,16 +1532,19 @@ document.getElementById('modal-confirm').addEventListener('click', e => { if (e.
|
||||
// Print from confirm modal — abre preview no navegador
|
||||
document.getElementById('confirm-print').addEventListener('click', async () => {
|
||||
closeConfirmModal();
|
||||
// Reconstrói os mesmos dados do tauriRecibo e abre preview
|
||||
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: parseNum(val('f-saldo-esperado')) || 0,
|
||||
fechamento: parseNum(val('f-fech-dinheiro')) + parseNum(val('f-fech-cartoes')) + parseNum(val('f-fech-receber')),
|
||||
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 })),
|
||||
@@ -1497,7 +1552,8 @@ document.getElementById('confirm-print').addEventListener('click', async () => {
|
||||
receber: estado.receber.map(r => ({ nome: r.nome||'', valor: r.valor||0 })),
|
||||
cancelamentos: estado.cancelamentos.map(r => ({ numero: r.numero||'', valor: r.valor||0, motivo: r.motivo||'' })),
|
||||
credito: cartCred, debito: cartDeb, alimentacao: cartAli,
|
||||
cartoes: { pix: cartPix },
|
||||
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);
|
||||
@@ -1554,36 +1610,55 @@ document.getElementById('btn-recibo').addEventListener('click', async () => {
|
||||
document.getElementById('btn-limpar').addEventListener('click', () => {
|
||||
if (!confirm('Limpar rascunho? Esta acção não pode ser revertida.')) return;
|
||||
localStorage.removeItem(LS_KEY);
|
||||
location.reload();
|
||||
// Reseta estado e re-renderiza sem reload
|
||||
estado = newEstado();
|
||||
estado.loja = val('f-loja') || 'União';
|
||||
estado.data = val('f-data') || new Date().toISOString().split('T')[0];
|
||||
calcular();
|
||||
renderAllTables();
|
||||
syncUI();
|
||||
showToast('Rascunho limpo.', 'info');
|
||||
});
|
||||
|
||||
document.getElementById('btn-anterior').addEventListener('click', async () => {
|
||||
const loja = val('f-loja') || 'União';
|
||||
|
||||
// Carrega lista de datas recentes do Supabase
|
||||
let datas = [];
|
||||
// Carrega lista de fechamentos recentes do Supabase com uuid
|
||||
let recentList = [];
|
||||
try {
|
||||
const lista = await window.__TAURI__.core.invoke('sb_listar_recentes', { loja, limite: 20 });
|
||||
if (lista && lista.length > 0) {
|
||||
// Extrai datas únicas e já ordenadas
|
||||
datas = [...new Set(lista.map(l => l.data).filter(Boolean))].sort().reverse();
|
||||
}
|
||||
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;
|
||||
if (datas.length > 0) {
|
||||
// Mostra seletor com as datas disponíveis
|
||||
const msg = datas.map((d, i) => `${i + 1}) ${d}`).join('\n');
|
||||
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 < datas.length) selectedDate = datas[idx];
|
||||
if (idx >= 0 && idx < dateList.length) {
|
||||
selectedDate = dateList[idx];
|
||||
selectedId = dateIdMap[selectedDate];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Se não escolheu data, usa dia anterior automático
|
||||
// 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];
|
||||
@@ -1596,11 +1671,19 @@ document.getElementById('btn-anterior').addEventListener('click', async () => {
|
||||
estado.id_local = null;
|
||||
syncHeader();
|
||||
|
||||
// Tenta Supabase primeiro, senão local
|
||||
// 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 {
|
||||
|
||||
Reference in New Issue
Block a user