Compare commits

...

22 Commits

Author SHA1 Message Date
root 130b2b6da7 v1.3.11: 3 bugs corrigidos — preview sempre abre, tabela cartoes com traços, variaveis ausentes
Build Fechamento de Caixa / build (macos-14, app) (push) Has been cancelled
Build Fechamento de Caixa / build (ubuntu-22.04, deb) (push) Has been cancelled
Build Fechamento de Caixa / build (windows-2022, msi) (push) Has been cancelled
2026-06-26 03:40:48 +00:00
root b47d7766ad v1.3.10: relatório completo idêntico ao PDF — todas seções, tabela cartões 4 colunas, saldo sistema, fechamento contado, diff sys_total
Build Fechamento de Caixa / build (macos-14, app) (push) Has been cancelled
Build Fechamento de Caixa / build (ubuntu-22.04, deb) (push) Has been cancelled
Build Fechamento de Caixa / build (windows-2022, msi) (push) Has been cancelled
2026-06-26 03:24:27 +00:00
root 78fa778197 v1.3.9: dif ESC/POS usa self.diferenca (sys_total - saldo_esperado)
Build Fechamento de Caixa / build (macos-14, app) (push) Has been cancelled
Build Fechamento de Caixa / build (ubuntu-22.04, deb) (push) Has been cancelled
Build Fechamento de Caixa / build (windows-2022, msi) (push) Has been cancelled
2026-06-26 03:11:16 +00:00
root 5e4340adca v1.3.7: só nomenclatura — Fechamento de Caixa / Saldo de Caixa
Build Fechamento de Caixa / build (macos-14, app) (push) Has been cancelled
Build Fechamento de Caixa / build (ubuntu-22.04, deb) (push) Has been cancelled
Build Fechamento de Caixa / build (windows-2022, msi) (push) Has been cancelled
2026-06-26 02:57:34 +00:00
root c01133f39c v1.3.6: nomenclatura trocada; relatórios com todos os lançamentos detalhados por seção
Build Fechamento de Caixa / build (macos-14, app) (push) Has been cancelled
Build Fechamento de Caixa / build (ubuntu-22.04, deb) (push) Has been cancelled
Build Fechamento de Caixa / build (windows-2022, msi) (push) Has been cancelled
2026-06-26 02:52:08 +00:00
root a6b295a6d0 v1.3.5: to_escpos completo — totais por seção, diferenca via self.diferenca; confirm antes de dia anterior; sem senha
Build Fechamento de Caixa / build (macos-14, app) (push) Has been cancelled
Build Fechamento de Caixa / build (ubuntu-22.04, deb) (push) Has been cancelled
Build Fechamento de Caixa / build (windows-2022, msi) (push) Has been cancelled
2026-06-26 02:41:37 +00:00
root f850831e98 fix: buildPrintHTML usa data.diferenca (sys_total - saldo_esperado); btn-anterior null-check robusto
Build Fechamento de Caixa / build (macos-14, app) (push) Has been cancelled
Build Fechamento de Caixa / build (ubuntu-22.04, deb) (push) Has been cancelled
Build Fechamento de Caixa / build (windows-2022, msi) (push) Has been cancelled
2026-06-26 02:23:57 +00:00
root e70f31e00f debug: btn-anterior logs; cartoes.norm guard
Build Fechamento de Caixa / build (macos-14, app) (push) Has been cancelled
Build Fechamento de Caixa / build (ubuntu-22.04, deb) (push) Has been cancelled
Build Fechamento de Caixa / build (windows-2022, msi) (push) Has been cancelled
2026-06-26 01:44:15 +00:00
root 79e1dbd645 fix: capabilities Tauri 2 dialog; imprimir_recibo {data}; cartoes.map guard; debug logs
Build Fechamento de Caixa / build (macos-14, app) (push) Has been cancelled
Build Fechamento de Caixa / build (ubuntu-22.04, deb) (push) Has been cancelled
Build Fechamento de Caixa / build (windows-2022, msi) (push) Has been cancelled
2026-06-25 17:44:22 +00:00
root cce8f3df60 fix: newEstado is not defined — reset estado inline; clear form fields on limpar
Build Fechamento de Caixa / build (macos-14, app) (push) Has been cancelled
Build Fechamento de Caixa / build (ubuntu-22.04, deb) (push) Has been cancelled
Build Fechamento de Caixa / build (windows-2022, msi) (push) Has been cancelled
2026-06-25 17:30:07 +00:00
root 318d44e934 fix: modal confirm usa sysTotal pra diferenca, nao so sys-troco
Build Fechamento de Caixa / build (macos-14, app) (push) Has been cancelled
Build Fechamento de Caixa / build (ubuntu-22.04, deb) (push) Has been cancelled
Build Fechamento de Caixa / build (windows-2022, msi) (push) Has been cancelled
2026-06-25 16:38:26 +00:00
root bfbcd89be9 fix: diferenca usa sys_total; novo layout 80mm sem ENTRADAS; confirm modal
Build Fechamento de Caixa / build (macos-14, app) (push) Has been cancelled
Build Fechamento de Caixa / build (ubuntu-22.04, deb) (push) Has been cancelled
Build Fechamento de Caixa / build (windows-2022, msi) (push) Has been cancelled
2026-06-25 14:32:05 +00:00
root 1848c1aa35 fix: diferenca no recibo - TOTAL agora usa total_operado, diferenca = fechamento - saldo_esperado
Build Fechamento de Caixa / build (macos-14, app) (push) Has been cancelled
Build Fechamento de Caixa / build (ubuntu-22.04, deb) (push) Has been cancelled
Build Fechamento de Caixa / build (windows-2022, msi) (push) Has been cancelled
- Adicionado campo total_operado (credito+debito+alimentacao+voucher+pixcnpj)
- WHITE section: Fechamento mostra total_operado (era fechamento contado)
- BLACK section: diferenca usa fechamento - saldo_esperado (nao mais self.diferenca)
- Confirm modal: diferenca usava sysTroco - saldoEsp (errado), corrigido
- buildPrintHTML: diferenca usa total_operado
2026-06-25 13:22:03 +00:00
root 9176b764fd feat: alinha recibo ESC/POS com formato do screenshot
Build Fechamento de Caixa / build (macos-14, app) (push) Has been cancelled
Build Fechamento de Caixa / build (ubuntu-22.04, deb) (push) Has been cancelled
Build Fechamento de Caixa / build (windows-2022, msi) (push) Has been cancelled
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
2026-06-24 18:31:03 +00:00
root 5cab38a27c chore: clean Rust warnings and remove unused deps
Build Fechamento de Caixa / build (macos-14, app) (push) Has been cancelled
Build Fechamento de Caixa / build (ubuntu-22.04, deb) (push) Has been cancelled
Build Fechamento de Caixa / build (windows-2022, msi) (push) Has been cancelled
- Remove serde_yaml and tokio from Cargo.toml (unused)
- Add #[allow(dead_code)] to utility code kept for future use
  (CUT_FULL, ALIGN_RIGHT, line(), PrinterError, SupabaseError,
   atualizar_sync, remover, Lock variant)
- Zero compiler warnings on release build
2026-06-24 14:51:56 +00:00
root 5b1f3a2691 chore: adiciona README, .gitignore, remove binarios do repo
Build Fechamento de Caixa / build (macos-14, app) (push) Has been cancelled
Build Fechamento de Caixa / build (ubuntu-22.04, deb) (push) Has been cancelled
Build Fechamento de Caixa / build (windows-2022, msi) (push) Has been cancelled
- Adiciona .gitignore com *.exe, *.tar.gz, *.dll, *.log
- Remove binários commitados do tracking (58MB节省)
- Adiciona README.md com instruções de setup, estrutura, comandos Tauri
- Substitui location.reload() na impressão por iframe (preserva estado)
- Substitui location.reload() no btn-limpar por reset de estado in-memory
- Nota: security — service_role_key ainda no cliente desktop
2026-06-24 14:30:26 +00:00
root 7e57d30863 v1.2.7: conserta 4 bugs — impressao, enviar, dia anterior, localStorage
Build Fechamento de Caixa / build (macos-14, app) (push) Has been cancelled
Build Fechamento de Caixa / build (ubuntu-22.04, deb) (push) Has been cancelled
Build Fechamento de Caixa / build (windows-2022, msi) (push) Has been cancelled
2026-06-24 13:38:00 +00:00
root 7a93a30c22 v1.2.6: init() restaura operador+turno; tar inclui WebView2Loader.dll
Build Fechamento de Caixa / build (macos-14, app) (push) Has been cancelled
Build Fechamento de Caixa / build (ubuntu-22.04, deb) (push) Has been cancelled
Build Fechamento de Caixa / build (windows-2022, msi) (push) Has been cancelled
2026-06-24 13:11:43 +00:00
root 230304e878 v1.2.5 — fixes: diferenca=fechamento-esperado; btn-enviar func; mapas tabela Supabase
Build Fechamento de Caixa / build (macos-14, app) (push) Has been cancelled
Build Fechamento de Caixa / build (ubuntu-22.04, deb) (push) Has been cancelled
Build Fechamento de Caixa / build (windows-2022, msi) (push) Has been cancelled
MAPA SUPABASE:
Tabela: fechamentos_web
  uuid | loja | data | operador | turno
  saldo_troco | saldo_esperado | fechamento | diferenca
  sync_status | observacoes | dados (JSON com tudo)
  atualizado_em

Tabela: despesas (via JSON em dados->despesas)
Tabela: sangrias (via JSON em dados->sangrias)
Tabela: vales (via JSON em dados->vales)
Tabela: receber (via JSON em dados->receber)
Tabela: pixcnpj (via JSON em dados->pixcnpj)
Tabela: cancelamentos (via JSON em dados->cancelamentos)
Tabela: cartoes (dados->cartoes: credito/debito/alimentacao/pix arrays)

Tabela: listas_personalizadas (operadores/gerentes/despesas_custom/clientes)
  loja | operadores[] | gerentes[] | despesas_custom[] | clientes[]

FUNCOES RPC:
  upsert_fechamento() — upsert fechamentos_web por uuid
  upsert_listas() — upsert listas_personalizadas por loja

BOTÃO DIA ANTERIOR: usa sb_listar_recentes → tabela fechamentos_web
  Seleciona por numero (1=N Mais Recente, 2=2o Mais Recente...)
  ou automatico: data-1dia
2026-06-24 01:32:18 +00:00
root d185f82bd5 Revert "v1.2.3: btn voltar no preview; relatorio corrigido (fechamento-esperado); cartoes com detalhes p/ Supabase"
Build Fechamento de Caixa / build (macos-14, app) (push) Has been cancelled
Build Fechamento de Caixa / build (ubuntu-22.04, deb) (push) Has been cancelled
Build Fechamento de Caixa / build (windows-2022, msi) (push) Has been cancelled
This reverts commit 2253d45d00.
2026-06-24 01:18:57 +00:00
root 2253d45d00 v1.2.3: btn voltar no preview; relatorio corrigido (fechamento-esperado); cartoes com detalhes p/ Supabase
Build Fechamento de Caixa / build (macos-14, app) (push) Has been cancelled
Build Fechamento de Caixa / build (ubuntu-22.04, deb) (push) Has been cancelled
Build Fechamento de Caixa / build (windows-2022, msi) (push) Has been cancelled
2026-06-24 01:10:43 +00:00
root 60e15dbed3 fix impressão: window.print() in-app em vez de window.open(); alert com tabela destino no envio
Build Fechamento de Caixa / build (macos-14, app) (push) Has been cancelled
Build Fechamento de Caixa / build (ubuntu-22.04, deb) (push) Has been cancelled
Build Fechamento de Caixa / build (windows-2022, msi) (push) Has been cancelled
2026-06-23 18:28:58 +00:00
13 changed files with 1143 additions and 282 deletions
+13
View File
@@ -7,3 +7,16 @@ src-tauri/gen/
*.msi *.msi
*.dmg *.dmg
.DS_Store .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
+140
View File
@@ -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.
-35
View File
@@ -900,14 +900,12 @@ dependencies = [
"rusqlite", "rusqlite",
"serde", "serde",
"serde_json", "serde_json",
"serde_yaml",
"tauri", "tauri",
"tauri-build", "tauri-build",
"tauri-plugin-dialog", "tauri-plugin-dialog",
"tauri-plugin-fs", "tauri-plugin-fs",
"tauri-plugin-shell", "tauri-plugin-shell",
"thiserror 2.0.18", "thiserror 2.0.18",
"tokio",
"uuid", "uuid",
] ]
@@ -3281,19 +3279,6 @@ dependencies = [
"syn 2.0.118", "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]] [[package]]
name = "serialize-to-javascript" name = "serialize-to-javascript"
version = "0.1.2" version = "0.1.2"
@@ -4070,25 +4055,11 @@ dependencies = [
"bytes", "bytes",
"libc", "libc",
"mio", "mio",
"parking_lot",
"pin-project-lite", "pin-project-lite",
"signal-hook-registry",
"socket2", "socket2",
"tokio-macros",
"windows-sys 0.61.2", "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]] [[package]]
name = "tokio-native-tls" name = "tokio-native-tls"
version = "0.3.1" version = "0.3.1"
@@ -4399,12 +4370,6 @@ version = "1.13.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8" checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8"
[[package]]
name = "unsafe-libyaml"
version = "0.2.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861"
[[package]] [[package]]
name = "untrusted" name = "untrusted"
version = "0.9.0" version = "0.9.0"
-2
View File
@@ -15,8 +15,6 @@ serde = { version = "1", features = ["derive"] }
serde_json = "1" serde_json = "1"
rusqlite = { version = "0.32", features = ["bundled"] } rusqlite = { version = "0.32", features = ["bundled"] }
reqwest = { version = "0.12", features = ["json", "blocking"] } reqwest = { version = "0.12", features = ["json", "blocking"] }
tokio = { version = "1", features = ["full"] }
serde_yaml = "0.9"
uuid = { version = "1", features = ["v4"] } uuid = { version = "1", features = ["v4"] }
chrono = { version = "0.4", features = ["serde"] } chrono = { version = "0.4", features = ["serde"] }
log = "0.4" log = "0.4"
+23
View File
@@ -0,0 +1,23 @@
{
"identifier": "default",
"description": "Default capabilities for Fechamento de Caixa",
"windows": ["main"],
"permissions": [
"core:default",
"shell:allow-open",
"dialog:allow-message",
"dialog:allow-confirm",
"dialog:allow-ask",
"dialog:allow-save",
"dialog:allow-open",
"fs:default",
"fs:allow-app-read",
"fs:allow-app-write",
"fs:allow-appdata-read",
"fs:allow-appdata-write",
"fs:allow-appconfig-read",
"fs:allow-appconfig-write",
"fs:allow-applocaldata-read",
"fs:allow-applocaldata-write"
]
}
+3
View File
@@ -8,6 +8,7 @@ pub const LF: &[u8] = b"\x0A";
/// Cortar papel (parcial) /// Cortar papel (parcial)
pub const CUT: &[u8] = b"\x1D\x56\x01"; pub const CUT: &[u8] = b"\x1D\x56\x01";
/// Cortar papel (total) /// Cortar papel (total)
#[allow(dead_code)]
pub const CUT_FULL: &[u8] = b"\x1D\x56\x00"; pub const CUT_FULL: &[u8] = b"\x1D\x56\x00";
/// Alimentar 3 linhas antes de cortar /// Alimentar 3 linhas antes de cortar
pub const FEED_CUT: &[u8] = b"\x1B\x64\x03"; pub const FEED_CUT: &[u8] = b"\x1B\x64\x03";
@@ -17,6 +18,7 @@ pub const BOLD_OFF: &[u8] = b"\x1B\x45\x00";
/// Alinhamento /// Alinhamento
pub const ALIGN_LEFT: &[u8] = b"\x1B\x61\x00"; pub const ALIGN_LEFT: &[u8] = b"\x1B\x61\x00";
pub const ALIGN_CENTER: &[u8] = b"\x1B\x61\x01"; pub const ALIGN_CENTER: &[u8] = b"\x1B\x61\x01";
#[allow(dead_code)]
pub const ALIGN_RIGHT: &[u8] = b"\x1B\x61\x02"; pub const ALIGN_RIGHT: &[u8] = b"\x1B\x61\x02";
/// Fonte normal / dupla /// Fonte normal / dupla
pub const FONT_NORMAL: &[u8] = b"\x1B\x21\x00"; 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) /// Monta linha normal (alinhada à esquerda)
#[allow(dead_code)]
pub fn line(text: &str) -> Vec<u8> { pub fn line(text: &str) -> Vec<u8> {
let mut out = Vec::new(); let mut out = Vec::new();
out.extend_from_slice(ALIGN_LEFT); out.extend_from_slice(ALIGN_LEFT);
+252 -25
View File
@@ -5,6 +5,7 @@ use std::sync::Mutex;
use thiserror::Error; use thiserror::Error;
#[derive(Error, Debug)] #[derive(Error, Debug)]
#[allow(dead_code)]
pub enum PrinterError { pub enum PrinterError {
#[error("Impressora não encontrada. Verifique se está conectada via USB e ligada.")] #[error("Impressora não encontrada. Verifique se está conectada via USB e ligada.")]
NotFound, NotFound,
@@ -30,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,
@@ -37,19 +82,51 @@ pub struct ReciboData {
pub operador: String, pub operador: String,
pub turno: String, pub turno: String,
pub saldo_troco: f64, pub saldo_troco: f64,
/// Valor contado no fechamento (dinheiro + cartões + receber)
pub fechamento: f64, pub fechamento: f64,
/// Saldo esperado = total operações sangrias despesas
pub saldo_esperado: f64, pub saldo_esperado: f64,
/// Diferença = fechamento saldo_esperado
pub diferenca: f64, pub diferenca: f64,
pub sangrias: f64, /// Total do sistema (troco + crédito + débito + alimentação + voucher + pixcnpj + vales + receber)
pub despesas: f64, #[serde(default)]
pub vales: f64, pub sys_total: f64,
pub areceber: f64, /// Soma de todas operações creditadas (crédito + débito + alimentação + voucher + pixcnpj) — usado no BLACK section como TOTAL
#[serde(default)]
pub total_operado: f64,
// Totais de cartões
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 {
@@ -58,6 +135,7 @@ impl ReciboData {
} }
/// Gera todos os bytes ESC/POS do recibo 80mm /// Gera todos os bytes ESC/POS do recibo 80mm
/// Layout: Header → SANGRIAS → DESPESAS → PIX CNPJ → VALES → A RECEBER → CANCELAMENTOS → CARTÕES → TROCO/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();
@@ -77,40 +155,188 @@ 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("ENTRADAS")); if !self.sangrias_arr.is_empty() {
out.extend(escpos::kv("Saldo Troco:", &self.fmt_money(self.saldo_troco))); out.extend(escpos::line_bold("SANGRIAS"));
out.extend(escpos::kv("Fechamento:", &self.fmt_money(self.fechamento))); for linha in &self.sangrias_arr {
out.extend(escpos::kv("Saldo Esperado:", &self.fmt_money(self.saldo_esperado))); if linha.retirada.unwrap_or(0.0) > 0.0 || linha.valor.unwrap_or(0.0) > 0.0 {
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)));
}
}
}
// Total sangrias
let total_sangrias: f64 = self.sangrias_arr.iter()
.map(|l| l.retirada.unwrap_or(0.0) + l.valor.unwrap_or(0.0))
.sum();
if total_sangrias > 0.0 {
out.extend(escpos::kv("Total:", &self.fmt_money(total_sangrias)));
}
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.unwrap_or(0.0) > 0.0 {
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)));
}
}
}
// Total despesas
let total_despesas: f64 = self.despesas_arr.iter()
.map(|l| l.valor.unwrap_or(0.0))
.sum();
out.extend(escpos::kv("Total:", &self.fmt_money(total_despesas)));
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() {
out.extend(escpos::line_bold("PIX CNPJ"));
for linha in &self.pixcnpj_arr {
if linha.valor.unwrap_or(0.0) > 0.0 {
let label = linha.nome.clone().unwrap_or_else(|| "PIX CNPJ".to_string());
out.extend(escpos::kv(&label, &linha.fmt_v()));
}
}
// Total PIX CNPJ
let total_pixcnpj: f64 = self.pixcnpj_arr.iter()
.map(|l| l.valor.unwrap_or(0.0))
.sum();
if total_pixcnpj > 0.0 {
out.extend(escpos::kv("Total:", &self.fmt_money(total_pixcnpj)));
}
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.unwrap_or(0.0) > 0.0 {
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)));
}
}
}
// Total vales
let total_vales: f64 = self.vales_arr.iter()
.map(|l| l.valor.unwrap_or(0.0))
.sum();
if total_vales > 0.0 {
out.extend(escpos::kv("Total:", &self.fmt_money(total_vales)));
}
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.unwrap_or(0.0) > 0.0 {
let label = linha.label();
out.extend(escpos::kv(&label, &linha.fmt_v()));
}
}
// Total a receber
let total_receber: f64 = self.receber_arr.iter()
.map(|l| l.valor.unwrap_or(0.0))
.sum();
if total_receber > 0.0 {
out.extend(escpos::kv("Total:", &self.fmt_money(total_receber)));
}
out.extend(escpos::divider());
}
// ── CANCELAMENTOS ──
if !self.cancelamentos_arr.is_empty() {
out.extend(escpos::line_bold("CANCELAMENTOS"));
for linha in &self.cancelamentos_arr {
if linha.numero.is_some() || linha.valor.unwrap_or(0.0) > 0.0 {
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)));
}
}
}
}
// Total cancelamentos
let total_cancel: f64 = self.cancelamentos_arr.iter()
.map(|l| l.valor.unwrap_or(0.0))
.sum();
if total_cancel > 0.0 {
out.extend(escpos::kv("Total:", &self.fmt_money(total_cancel)));
}
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 // ── TROCO / SALDO DE CAIXA ──
let diff_str = if self.diferenca >= 0.0 { out.extend(escpos::line_bold("SALDO DE CAIXA"));
format!("+{}", self.fmt_money(self.diferenca)) out.extend(escpos::kv("Troco (+):", &self.fmt_money(self.saldo_troco)));
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("PIX Cartao (+):", &self.fmt_money(self.voucher)));
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());
// ── TOTAL ──
if self.sys_total > 0.0 {
out.extend(escpos::line_bold("TOTAL"));
out.extend(escpos::kv("Total:", &self.fmt_money(self.sys_total)));
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 ──
// Usa self.diferenca que o frontend calcula como sys_total - saldo_esperado
// (não recalcula de fechamento, que é o valor contado e não deve ser usado na diferença)
out.extend(escpos::line_bold("DIFERENCA"));
let diff = self.diferenca;
let diff_str = if diff >= 0.0 {
format!("+{}", self.fmt_money(diff))
} else { } else {
self.fmt_money(self.diferenca) self.fmt_money(diff)
}; };
out.extend(escpos::line_bold("DIFERENCA:"));
out.extend(escpos::title(&diff_str)); out.extend(escpos::title(&diff_str));
out.extend(escpos::blank_lines(2));
// ── 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());
}
}
// Rodapé // Rodapé
out.extend(escpos::divider()); out.extend(escpos::divider());
@@ -122,6 +348,7 @@ impl ReciboData {
// Corta papel // Corta papel
out.extend(escpos::FEED_CUT); out.extend(escpos::FEED_CUT);
out.extend(escpos::CUT);
out out
} }
+3 -1
View File
@@ -335,6 +335,7 @@ impl DbManager {
} }
/// Atualiza sync_status de um registro. /// Atualiza sync_status de um registro.
#[allow(dead_code)]
pub fn atualizar_sync(&self, uuid: &str, status: SyncStatus) -> Result<(), StorageError> { pub fn atualizar_sync(&self, uuid: &str, status: SyncStatus) -> Result<(), StorageError> {
let conn = self.conn.lock().map_err(|_| StorageError::Lock)?; let conn = self.conn.lock().map_err(|_| StorageError::Lock)?;
let now = Utc::now().format("%Y-%m-%dT%H:%M:%S%.3fZ").to_string(); 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). /// Remove um registro (para debugging).
#[allow(dead_code)]
pub fn remover(&self, uuid: &str) -> Result<bool, StorageError> { pub fn remover(&self, uuid: &str) -> Result<bool, StorageError> {
let conn = self.conn.lock().map_err(|_| StorageError::Lock)?; let conn = self.conn.lock().map_err(|_| StorageError::Lock)?;
let n = conn.execute("DELETE FROM fechamentos WHERE uuid = ?1", params![uuid])?; 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 // Simplifica para formato que o frontend espera
let total_sangrias: f64 = e.tabelas.sangrias.iter().filter_map(|r| r.valor).sum(); 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_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| { 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})) r.descricao.as_ref().map(|d| serde_json::json!({"desc": d, "valor": r.valor}))
}).collect(); }).collect();
+1
View File
@@ -8,6 +8,7 @@ use tauri::State;
use thiserror::Error; use thiserror::Error;
#[derive(Error, Debug)] #[derive(Error, Debug)]
#[allow(dead_code)]
pub enum SupabaseError { pub enum SupabaseError {
#[error("HTTP error: {0}")] #[error("HTTP error: {0}")]
Http(#[from] reqwest::Error), Http(#[from] reqwest::Error),
+11 -4
View File
@@ -11,7 +11,7 @@
"app": { "app": {
"windows": [ "windows": [
{ {
"title": "Fechamento de Caixa O Frangão", "title": "Fechamento de Caixa \u2014 O Frang\u00e3o",
"width": 1100, "width": 1100,
"height": 780, "height": 780,
"minWidth": 900, "minWidth": 900,
@@ -22,13 +22,18 @@
} }
], ],
"security": { "security": {
"csp": null "csp": null,
"capabilities": [
"default"
]
}, },
"withGlobalTauri": true "withGlobalTauri": true
}, },
"bundle": { "bundle": {
"active": true, "active": true,
"targets": ["nsis"], "targets": [
"nsis"
],
"icon": [ "icon": [
"icons/32x32.png", "icons/32x32.png",
"icons/128x128.png", "icons/128x128.png",
@@ -37,7 +42,9 @@
"icons/icon.ico" "icons/icon.ico"
], ],
"windows": { "windows": {
"webviewInstallMode": { "type": "embedBootstrapper" } "webviewInstallMode": {
"type": "embedBootstrapper"
}
}, },
"resources": { "resources": {
"../src/*": "./" "../src/*": "./"
+629 -147
View File
@@ -384,7 +384,7 @@
<!-- SALDO DE CAIXA --> <!-- SALDO DE CAIXA -->
<div class="section"> <div class="section">
<div class="section-header">🖥️ Saldo de Caixa (Valores do Sistema)</div> <div class="section-header">🖥️ Fechamento de Caixa</div>
<div class="sys-grid"> <div class="sys-grid">
<div class="sys-row"><span class="lbl">Troco (+)</span><input type="text" id="sys-troco" class="editable" placeholder="0,00" /></div> <div class="sys-row"><span class="lbl">Troco (+)</span><input type="text" id="sys-troco" class="editable" placeholder="0,00" /></div>
<div class="sys-row"><span class="lbl">Crédito (+)</span><input type="text" id="sys-credito" readonly tabindex="-1" /></div> <div class="sys-row"><span class="lbl">Crédito (+)</span><input type="text" id="sys-credito" readonly tabindex="-1" /></div>
@@ -415,7 +415,7 @@
<!-- FECHAMENTO (VALORES CONTADOS) --> <!-- FECHAMENTO (VALORES CONTADOS) -->
<div class="section"> <div class="section">
<div class="section-header">🛒 Fechamento (Valores Contados)</div> <div class="section-header">🛒 Saldo de Caixa</div>
<div class="fech-grid"> <div class="fech-grid">
<div class="fech-item"><div class="fech-item-label">Dinheiro</div><input type="text" id="f-fech-dinheiro" placeholder="0,00" /></div> <div class="fech-item"><div class="fech-item-label">Dinheiro</div><input type="text" id="f-fech-dinheiro" placeholder="0,00" /></div>
<div class="fech-item"><div class="fech-item-label">Cartões</div><input type="text" id="f-fech-cartoes" placeholder="0,00" /></div> <div class="fech-item"><div class="fech-item-label">Cartões</div><input type="text" id="f-fech-cartoes" placeholder="0,00" /></div>
@@ -1022,7 +1022,9 @@ function init() {
const today = new Date().toISOString().split('T')[0]; const today = new Date().toISOString().split('T')[0];
setVal('f-data', today); setVal('f-data', today);
estado.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(); syncHeader();
PRE_FILL_TABLES.forEach(ensureOneRow); PRE_FILL_TABLES.forEach(ensureOneRow);
Object.keys(TABLES).forEach(renderTable); Object.keys(TABLES).forEach(renderTable);
@@ -1045,7 +1047,6 @@ function buildEstadoForBackend() {
saldo_troco: parseNum(val('sys-troco')), // troco do sistema saldo_troco: parseNum(val('sys-troco')), // troco do sistema
saldo_esperado: parseNum(val('f-saldo-esperado')), saldo_esperado: parseNum(val('f-saldo-esperado')),
fechamento: parseNum(val('f-fech-dinheiro')) + parseNum(val('f-fech-cartoes')) + parseNum(val('f-fech-receber')), 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 })), 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 || '' })), 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 })), pixcnpj: estado.pixcnpj.map(r => ({ nome: r.nome || '', valor: r.valor || 0 })),
@@ -1083,8 +1084,44 @@ function loadState() {
const raw = localStorage.getItem(LS_KEY); const raw = localStorage.getItem(LS_KEY);
if (!raw) return false; if (!raw) return false;
const saved = JSON.parse(raw); const saved = JSON.parse(raw);
// Merge carefully (cartoes may be in old format) // Restore simple fields
Object.assign(estado, saved); 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-data', estado.data || '');
setVal('f-operador', estado.operador || ''); setVal('f-operador', estado.operador || '');
setVal('f-turno', estado.turno || ''); setVal('f-turno', estado.turno || '');
@@ -1092,9 +1129,10 @@ function loadState() {
setVal('f-obs', estado.observacoes || ''); setVal('f-obs', estado.observacoes || '');
setVal('f-saldo-esperado', estado.saldo_esperado === 0 ? '' : fmtNum(estado.saldo_esperado)); setVal('f-saldo-esperado', estado.saldo_esperado === 0 ? '' : fmtNum(estado.saldo_esperado));
setVal('sys-troco', estado.saldo_troco === 0 ? '' : fmtNum(estado.saldo_troco)); 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) : ''); const fc = estado.fechamento_contado || {};
setVal('f-fech-cartoes', estado.fechamento_contado && estado.fechamento_contado.cartoes ? fmtNum(estado.fechamento_contado.cartoes) : ''); setVal('f-fech-dinheiro', fc.dinheiro ? fmtNum(fc.dinheiro) : '');
setVal('f-fech-receber', estado.fechamento_contado && estado.fechamento_contado.receber ? fmtNum(estado.fechamento_contado.receber) : ''); setVal('f-fech-cartoes', fc.cartoes ? fmtNum(fc.cartoes) : '');
setVal('f-fech-receber', fc.receber ? fmtNum(fc.receber) : '');
syncHeader(); syncHeader();
PRE_FILL_TABLES.forEach(ensureOneRow); PRE_FILL_TABLES.forEach(ensureOneRow);
Object.keys(TABLES).forEach(renderTable); Object.keys(TABLES).forEach(renderTable);
@@ -1191,61 +1229,91 @@ async function tauriEnviar() {
} }
} }
// ─── Print Preview Window ────────────────────────────────────────────────── function buildConfirmHTML(data) {
function buildPrintHTML(data) { // Relatório completo — mesmo layout do PDF
const fmt = v => 'R$ ' + (v || 0).toFixed(2).replace('.', ','); const fmt = v => 'R$ ' + (v || 0).toFixed(2).replace('.', ',');
const diff = (data.saldo_troco - data.saldo_esperado); const fmtNeg = v => (v < 0 ? '-' : '') + 'R$ ' + Math.abs(v || 0).toFixed(2).replace('.', ',');
const diffStr = diff >= 0 ? `+${fmt(diff)}` : fmt(diff); const diferenca = data.diferenca !== undefined ? data.diferenca
const diffColor = diff === 0 ? '#2e7d32' : (diff > 0 ? '#e65100' : '#c0272d'); : ((data.sys_total || 0) - (data.saldo_esperado || 0));
const diffStr = diferenca >= 0 ? `+${fmt(diferenca)}` : fmtNeg(diferenca);
const diffOk = diferenca === 0;
let rows = ''; const sangrias_arr = Array.isArray(data.sangrias_arr) ? data.sangrias_arr : [];
// Sangrias const despesas_arr = Array.isArray(data.despesas_arr) ? data.despesas_arr : [];
if (data.sangrias && data.sangrias.length > 0) { const pixcnpj_arr = Array.isArray(data.pixcnpj_arr) ? data.pixcnpj_arr : [];
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>`; }); const vales_arr = Array.isArray(data.vales_arr) ? data.vales_arr : [];
} const receber_arr = Array.isArray(data.receber_arr) ? data.receber_arr : [];
// Despesas const cancelamentos_arr = Array.isArray(data.cancelamentos_arr) ? data.cancelamentos_arr : [];
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>`; }); const cartCred_arr = Array.isArray(data.cartoes_credito) ? data.cartoes_credito : [];
} const cartDeb_arr = Array.isArray(data.cartoes_debito) ? data.cartoes_debito : [];
// Vales const cartAli_arr = Array.isArray(data.cartoes_alimentacao) ? data.cartoes_alimentacao : [];
if (data.vales && data.vales.length > 0) { const cartPix_arr = Array.isArray(data.cartoes_pix) ? data.cartoes_pix : [];
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>`; });
} const totalSangrias = sangrias_arr.reduce((s,r) => s+(r.retirada||0)+(r.valor||0), 0);
// Receber const totalDespesas = despesas_arr.reduce((s,r) => s+(r.valor||0), 0);
if (data.receber && data.receber.length > 0) { const totalPixCnpj = pixcnpj_arr.reduce((s,r) => s+(r.valor||0), 0);
data.receber.forEach(r => { if (r.nome || r.valor) rows += `<tr><td>${r.nome||''}</td><td>${fmt(r.valor)}</td><td></td></tr>`; }); const totalVales = vales_arr.reduce((s,r) => s+(r.valor||0), 0);
} const totalReceber = receber_arr.reduce((s,r) => s+(r.valor||0), 0);
// PIX CNPJ const totalCancel = cancelamentos_arr.reduce((s,r) => s+(r.valor||0), 0);
if (data.pixcnpj && data.pixcnpj.length > 0) {
data.pixcnpj.forEach(p => { if (p.nome || p.valor) rows += `<tr><td>PIX: ${p.nome||''}</td><td>${fmt(p.valor)}</td><td></td></tr>`; }); const cartCred = cartCred_arr.reduce((s,v) => s+(v||0), 0);
} const cartDeb = cartDeb_arr.reduce((s,v) => s+(v||0), 0);
// Cancelamentos const cartAli = cartAli_arr.reduce((s,v) => s+(v||0), 0);
if (data.cancelamentos && data.cancelamentos.length > 0) { const cartPix = cartPix_arr.reduce((s,v) => s+(v||0), 0);
data.cancelamentos.forEach(c => { if (c.numero || c.valor) rows += `<tr><td>${c.numero||''}</td><td>${fmt(c.valor)}</td><td>${c.motivo||''}</td></tr>`; }); const totalCartoes = cartCred + cartDeb + cartAli + cartPix;
const fdin = data.fechamento_dinheiro || 0;
const fcarts = data.fechamento_cartoes || 0;
const frec = data.fechamento_receber || 0;
const ftotal = data.fechamento || (fdin + fcarts + frec);
const sysTotal = data.sys_total || 0;
const saldoEsperado = data.saldo_esperado || 0;
// Cartoes table
const maxRows = Math.max(cartCred_arr.length, cartDeb_arr.length, cartAli_arr.length, cartPix_arr.length);
let cartTableRows = '';
for (let i = 0; i < maxRows; i++) {
const cr = cartCred_arr[i] || 0;
const db = cartDeb_arr[i] || 0;
const al = cartAli_arr[i] || 0;
const px = cartPix_arr[i] || 0;
cartTableRows += `<tr>
<td>${cr > 0 ? fmt(cr) : '-'}</td>
<td>${db > 0 ? fmt(db) : '-'}</td>
<td>${al > 0 ? fmt(al) : '-'}</td>
<td>${px > 0 ? fmt(px) : '-'}</td>
</tr>`;
} }
cartTableRows += `<tr class="total-row">
<td>${cartCred > 0 ? fmt(cartCred) : '-'}</td>
<td>${cartDeb > 0 ? fmt(cartDeb) : '-'}</td>
<td>${cartAli > 0 ? fmt(cartAli) : '-'}</td>
<td>${cartPix > 0 ? fmt(cartPix) : '-'}</td>
</tr>`;
return `<!DOCTYPE html> return `<!DOCTYPE html>
<html><head> <html><head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<title>Relatório de Fechamento</title> <title>Fechamento de Caixa</title>
<style> <style>
* { box-sizing: border-box; margin: 0; padding: 0; } * { box-sizing: border-box; margin: 0; padding: 0; }
body { font-family: 'Courier New', monospace; font-size: 13px; width: 80mm; margin: 0 auto; padding: 8px; } body { font-family: 'Courier New', monospace; font-size: 13px; width: 80mm; margin: 0 auto; padding: 8px; }
.header { text-align: center; margin-bottom: 8px; } .header { text-align: center; margin-bottom: 8px; }
.header h1 { font-size: 18px; } .header h1 { font-size: 18px; }
.header h2 { font-size: 14px; font-weight: normal; } .header h2 { font-size: 14px; font-weight: normal; }
hr { border: none; border-top: 1px dashed #000; margin: 6px 0; } hr { border: none; border-top: 1px dashed #000; margin: 5px 0; }
.section { margin: 6px 0; } .section-title { font-weight: bold; font-size: 13px; border-top: 1px dashed #000; padding-top: 3px; margin-top: 6px; }
.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: 1px 0; font-size: 12px; }
.row { display: flex; justify-content: space-between; padding: 2px 0; } .row.total-row { font-weight: bold; }
.row.space-between { justify-content: space-between; } .cart-table { width: 100%; border-collapse: collapse; font-size: 11px; margin-top: 4px; }
.row.below { margin-top: 4px; } .cart-table th { font-weight: bold; border-bottom: 1px solid #000; padding: 2px; }
table { width: 100%; border-collapse: collapse; font-size: 12px; } .cart-table td { padding: 2px 4px; text-align: right; }
td { padding: 2px 4px; vertical-align: top; } .cart-table td:first-child { text-align: left; }
td:nth-child(2) { text-align: right; } .cart-table tr.total-row { font-weight: bold; border-top: 1px dashed #000; }
td:nth-child(3) { text-align: right; color: #555; font-size: 11px; } .subtotal { font-size: 12px; margin-top: 4px; }
.total-row { font-weight: bold; border-top: 1px dashed #000; } .diff-ok { color: #2e7d32; }
.grand-total { font-size: 16px; font-weight: bold; text-align: center; padding: 6px; border: 2px solid #000; margin: 8px 0; } .diff-bad { color: #c0272d; }
.obs { font-size: 11px; color: #555; margin-top: 6px; white-space: pre-wrap; } .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; } .footer { text-align: center; font-size: 11px; color: #555; margin-top: 8px; }
@media print { body { margin: 0; } } @media print { body { margin: 0; } }
@@ -1260,35 +1328,357 @@ function buildPrintHTML(data) {
<div class="row"><span>Operador:</span><span>${data.operador}</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>Turno:</span><span>${data.turno}</span></div>
<div class="section-title">ENTRADAS</div> ${sangrias_arr.length > 0 ? `<div class="section-title">SANGRIAS</div>
<div class="row"><span>Saldo Troco:</span><span>${fmt(data.saldo_troco)}</span></div> ${sangrias_arr.map(s => `<div class="row"><span>${(s.hora||'')}: ${s.gerente||''}</span><span>${fmt((s.retirada||0)+(s.valor||0))}</span></div>`).join('')}
<div class="row"><span>Fechamento:</span><span>${fmt(data.fechamento)}</span></div> <div class="row total-row"><span>TOTAL SANGRIAS:</span><span>${fmt(totalSangrias)}</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>` : ''} ${despesas_arr.length > 0 ? `<div class="section-title">DESPESAS</div>
${despesas_arr.map(d => `<div class="row"><span>${d.desc||''}${d.obs ? ` (${d.obs})` : ''}</span><span>${fmt(d.valor||0)}</span></div>`).join('')}
<div class="row total-row"><span>TOTAL DESPESAS:</span><span>${fmt(totalDespesas)}</span></div>` : ''}
${data.observacoes ? `<div class="obs">Obs: ${data.observacoes}</div>` : ''} ${pixcnpj_arr.length > 0 ? `<div class="section-title">PIX CNPJ</div>
${pixcnpj_arr.map(p => `<div class="row"><span>${p.nome||''}</span><span>${fmt(p.valor||0)}</span></div>`).join('')}
<div class="row total-row"><span>TOTAL PIX CNPJ:</span><span>${fmt(totalPixCnpj)}</span></div>` : ''}
<hr> ${vales_arr.length > 0 ? `<div class="section-title">VALES</div>
<div class="section-title">CARTÕES</div> ${vales_arr.map(v => `<div class="row"><span>${v.nome||''}${v.obs ? ` (${v.obs})` : ''}</span><span>${fmt(v.valor||0)}</span></div>`).join('')}
<div class="row"><span>Crédito:</span><span>${fmt(data.credito)}</span></div> <div class="row total-row"><span>TOTAL VALES:</span><span>${fmt(totalVales)}</span></div>` : ''}
<div class="row"><span>Débito:</span><span>${fmt(data.debito)}</span></div>
<div class="row"><span>Alimentação:</span><span>${fmt(data.alimentacao)}</span></div> ${receber_arr.length > 0 ? `<div class="section-title">A RECEBER</div>
<div class="row"><span>PIX Cartão:</span><span>${fmt(data.cartoes?.pix||0)}</span></div> ${receber_arr.map(r => `<div class="row"><span>${r.nome||''}</span><span>${fmt(r.valor||0)}</span></div>`).join('')}
<div class="row"><span>Cancelamentos:</span><span>${fmt(data.cancelamentos)}</span></div> <div class="row total-row"><span>TOTAL A RECEBER:</span><span>${fmt(totalReceber)}</span></div>` : ''}
${cancelamentos_arr.length > 0 ? `<div class="section-title">CANCELAMENTOS</div>
${cancelamentos_arr.map(c => `<div class="row"><span>${c.numero ? `Nº ${c.numero}` : ''}${c.motivo ? ` (${c.motivo})` : ''}</span><span>${fmt(c.valor||0)}</span></div>`).join('')}
<div class="row total-row"><span>TOTAL:</span><span>${fmt(totalCancel)}</span></div>` : ''}
<div class="section-title">DETALHAMENTO DE CARTÕES</div>
<table class="cart-table">
<thead>
<tr>
<th>CRÉDITO</th><th>DÉBITO</th><th>ALIMENT.</th><th>PIX</th>
</tr>
</thead>
<tbody>
${cartTableRows}
</tbody>
</table>
<div class="subtotal"><div class="row total-row"><span>Soma Cartões:</span><span>${fmt(totalCartoes)}</span></div></div>
<div class="section-title">SALDO DE CAIXA (SISTEMA) — RESUMO</div>
<div class="row"><span>Troco</span><span>${fmt(data.saldo_troco||0)}</span></div>
<div class="row"><span>Crédito</span><span>${fmt(cartCred)}</span></div>
<div class="row"><span>Débito</span><span>${fmt(cartDeb)}</span></div>
<div class="row"><span>Alimentação</span><span>${fmt(cartAli)}</span></div>
<div class="row"><span>Vales</span><span>${fmt(totalVales)}</span></div>
<div class="row"><span>À Receber</span><span>${fmt(totalReceber)}</span></div>
<div class="row"><span>PIX Cartão</span><span>${fmt(cartPix)}</span></div>
<div class="row"><span>PIX CNPJ</span><span>${fmt(totalPixCnpj)}</span></div>
<div class="row total-row"><span>TOTAL SISTEMA</span><span>${fmt(sysTotal)}</span></div>
<div class="row"><span>Saldo Esperado</span><span>${fmt(saldoEsperado)}</span></div>
<div class="row"><span>DIFERENÇA</span><span class="${diffOk ? 'diff-ok' : 'diff-bad'}">${diffStr}${diffOk ? ' ✓ OK' : ''}</span></div>
<div class="section-title">FECHAMENTO (CONTADO)</div>
<div class="row"><span>Dinheiro:</span><span>${fmt(fdin)}</span></div>
<div class="row"><span>Cartões:</span><span>${fmt(fcarts)}</span></div>
<div class="row"><span>À Receber:</span><span>${fmt(frec)}</span></div>
<div class="row total-row"><span>TOTAL:</span><span>${fmt(ftotal)}</span></div>
${data.observacoes ? `<div class="obs">${data.observacoes}</div>` : ''}
<hr>
<div class="grand-total">DIFERENÇA: ${diffStr}</div>
<div class="footer">Gerado em ${new Date().toLocaleString('pt-BR')}</div> <div class="footer">Gerado em ${new Date().toLocaleString('pt-BR')}</div>
</body></html>`; </body></html>`;
} }
function openPrintPreview(data) { // ─── Print Preview Window ──────────────────────────────────────────────────
const html = buildPrintHTML(data); function buildPrintHTML(data) {
const win = window.open('', 'print_preview', 'width=400,height=700,scrollbars=yes,resizable=yes'); const fmt = v => 'R$ ' + (v || 0).toFixed(2).replace('.', ',');
if (!win) { alert('Bloqueador de pop-up impediu abrir a pré-visualização.'); return; } const fmtNeg = v => (v < 0 ? '-' : '') + 'R$ ' + Math.abs(v || 0).toFixed(2).replace('.', ',');
win.document.write(html); const diferenca = data.diferenca !== undefined ? data.diferenca
win.document.close(); : ((data.sys_total || 0) - (data.saldo_esperado || 0));
const diffStr = diferenca >= 0 ? `+${fmt(diferenca)}` : fmtNeg(diferenca);
const diffColor = diferenca === 0 ? '#2e7d32' : (diferenca > 0 ? '#e65100' : '#c0272d');
// Arrays — support both array-of-objects and flat totals
const sangrias_arr = Array.isArray(data.sangrias_arr) ? data.sangrias_arr : [];
const despesas_arr = Array.isArray(data.despesas_arr) ? data.despesas_arr : [];
const pixcnpj_arr = Array.isArray(data.pixcnpj_arr) ? data.pixcnpj_arr : [];
const vales_arr = Array.isArray(data.vales_arr) ? data.vales_arr : [];
const receber_arr = Array.isArray(data.receber_arr) ? data.receber_arr : [];
const cancelamentos_arr = Array.isArray(data.cancelamentos_arr) ? data.cancelamentos_arr : [];
// Cartoes arrays from estado
const cartCred_arr = Array.isArray(data.cartoes_credito) ? data.cartoes_credito : [];
const cartDeb_arr = Array.isArray(data.cartoes_debito) ? data.cartoes_debito : [];
const cartAli_arr = Array.isArray(data.cartoes_alimentacao) ? data.cartoes_alimentacao : [];
const cartPix_arr = Array.isArray(data.cartoes_pix) ? data.cartoes_pix : [];
// Flat totals
const totalSangrias = sangrias_arr.reduce((s,r) => s+(r.retirada||0)+(r.valor||0), 0);
const totalDespesas = despesas_arr.reduce((s,r) => s+(r.valor||0), 0);
const totalPixCnpj = pixcnpj_arr.reduce((s,r) => s+(r.valor||0), 0);
const totalVales = vales_arr.reduce((s,r) => s+(r.valor||0), 0);
const totalReceber = receber_arr.reduce((s,r) => s+(r.valor||0), 0);
const totalCancel = cancelamentos_arr.reduce((s,r) => s+(r.valor||0), 0);
const cartCred = cartCred_arr.reduce((s,v) => s+(v||0), 0);
const cartDeb = cartDeb_arr.reduce((s,v) => s+(v||0), 0);
const cartAli = cartAli_arr.reduce((s,v) => s+(v||0), 0);
const cartPix = cartPix_arr.reduce((s,v) => s+(v||0), 0);
const totalCartoes = cartCred + cartDeb + cartAli + cartPix;
// Closed-form fields
const fdin = data.fechamento_dinheiro || 0;
const fcarts = data.fechamento_cartoes || 0;
const frec = data.fechamento_receber || 0;
const ftotal = data.fechamento || (fdin + fcarts + frec);
// Build section rows for sangrias, despesas, etc.
function section(title, lines, total, fmtFn) {
if (!lines.length && total <= 0) return '';
let html = `<div class="section-title">${title}</div>`;
lines.forEach(l => { if (l.valor > 0 || l.retirada > 0) { html += `<div class="row"><span>${l.label||''}</span><span>${fmtFn(l.valor||l.retirada||0)}</span></div>`; } });
if (total > 0) html += `<div class="row total-row"><span>TOTAL ${title}:</span><span>${fmtFn(total)}</span></div>`;
return html;
}
// Sangrias rows
const sangriaLines = sangrias_arr.map(s => ({
label: (s.hora ? s.hora + ': ' : '') + (s.gerente ? `(${s.gerente})` : ''),
valor: (s.retirada||0)+(s.valor||0)
})).filter(l => l.valor > 0);
// Despesas rows
const despLines = despesas_arr.map(d => ({
label: (d.desc||'') + (d.obs ? ` (${d.obs})` : ''),
valor: d.valor||0
})).filter(l => l.valor > 0);
// PIX CNPJ rows
const pixLines = pixcnpj_arr.map(p => ({
label: p.nome||'PIX CNPJ',
valor: p.valor||0
})).filter(l => l.valor > 0);
// Vales rows
const valesLines = vales_arr.map(v => ({
label: (v.nome||'') + (v.obs ? ` (${v.obs})` : ''),
valor: v.valor||0
})).filter(l => l.valor > 0);
// A Receber rows
const recLines = receber_arr.map(r => ({
label: r.nome||'',
valor: r.valor||0
})).filter(l => l.valor > 0);
// Cancelamentos rows
const cancLines = cancelamentos_arr.map(c => ({
label: (c.numero ? `Nº ${c.numero}` : '') + (c.motivo ? ` (${c.motivo})` : ''),
valor: c.valor||0
})).filter(l => l.valor > 0);
// Cartoes table (4 columns)
const maxRows = Math.max(cartCred_arr.length, cartDeb_arr.length, cartAli_arr.length, cartPix_arr.length);
let cartTableRows = '';
for (let i = 0; i < maxRows; i++) {
const cr = cartCred_arr[i] || 0;
const db = cartDeb_arr[i] || 0;
const al = cartAli_arr[i] || 0;
const px = cartPix_arr[i] || 0;
cartTableRows += `<tr>
<td>${cr > 0 ? fmt(cr) : '-'}</td>
<td>${db > 0 ? fmt(db) : '-'}</td>
<td>${al > 0 ? fmt(al) : '-'}</td>
<td>${px > 0 ? fmt(px) : '-'}</td>
</tr>`;
}
// Totals row
cartTableRows += `<tr class="total-row">
<td>${cartCred > 0 ? fmt(cartCred) : '-'}</td>
<td>${cartDeb > 0 ? fmt(cartDeb) : '-'}</td>
<td>${cartAli > 0 ? fmt(cartAli) : '-'}</td>
<td>${cartPix > 0 ? fmt(cartPix) : '-'}</td>
</tr>`;
const sysTotal = data.sys_total || 0;
const saldoEsperado = data.saldo_esperado || 0;
const diffOk = diferenca === 0;
let html = `<!DOCTYPE html>
<html><head>
<meta charset="UTF-8">
<title>Fechamento de Caixa</title>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body { font-family: 'Courier New', monospace; font-size: 13px; width: 80mm; margin: 0 auto; padding: 8px; }
.header { text-align: center; margin-bottom: 8px; }
.header h1 { font-size: 18px; }
.header h2 { font-size: 14px; font-weight: normal; }
hr { border: none; border-top: 1px dashed #000; margin: 5px 0; }
.section-title { font-weight: bold; font-size: 13px; border-top: 1px dashed #000; padding-top: 3px; margin-top: 6px; }
.row { display: flex; justify-content: space-between; padding: 1px 0; font-size: 12px; }
.row.total-row { font-weight: bold; }
.cart-table { width: 100%; border-collapse: collapse; font-size: 11px; margin-top: 4px; }
.cart-table th { font-weight: bold; border-bottom: 1px solid #000; padding: 2px; }
.cart-table td { padding: 2px 4px; text-align: right; }
.cart-table td:first-child { text-align: left; }
.cart-table tr.total-row { font-weight: bold; border-top: 1px dashed #000; }
.subtotal { font-size: 12px; margin-top: 4px; }
.subtotal .row { padding: 1px 0; }
.diff-ok { color: #2e7d32; }
.diff-bad { color: #c0272d; }
.obs { font-size: 11px; color: #555; margin-top: 6px; white-space: pre-wrap; }
.footer { text-align: center; font-size: 11px; color: #555; margin-top: 8px; }
@media print { body { margin: 0; } }
</style>
</head><body>
<div class="header">
<h1>${data.loja}</h1>
<h2>Fechamento de Caixa</h2>
</div>
<hr>
<div class="row"><span>Data:</span><span>${data.data}</span></div>
<div class="row"><span>Operador:</span><span>${data.operador}</span></div>
<div class="row"><span>Turno:</span><span>${data.turno}</span></div>
${sangriaLines.length > 0 ? `<div class="section-title">SANGRIAS</div>
${sangriaLines.map(l => `<div class="row"><span>${l.label}</span><span>${fmt(l.valor)}</span></div>`).join('')}
<div class="row total-row"><span>TOTAL SANGRIAS:</span><span>${fmt(totalSangrias)}</span></div>` : ''}
${despLines.length > 0 ? `<div class="section-title">DESPESAS</div>
${despLines.map(l => `<div class="row"><span>${l.label}</span><span>${fmt(l.valor)}</span></div>`).join('')}
<div class="row total-row"><span>TOTAL DESPESAS:</span><span>${fmt(totalDespesas)}</span></div>` : ''}
${pixLines.length > 0 ? `<div class="section-title">PIX CNPJ</div>
${pixLines.map(l => `<div class="row"><span>${l.label}</span><span>${fmt(l.valor)}</span></div>`).join('')}
<div class="row total-row"><span>TOTAL PIX CNPJ:</span><span>${fmt(totalPixCnpj)}</span></div>` : ''}
${valesLines.length > 0 ? `<div class="section-title">VALES</div>
${valesLines.map(l => `<div class="row"><span>${l.label}</span><span>${fmt(l.valor)}</span></div>`).join('')}
<div class="row total-row"><span>TOTAL VALES:</span><span>${fmt(totalVales)}</span></div>` : ''}
${recLines.length > 0 ? `<div class="section-title">A RECEBER</div>
${recLines.map(l => `<div class="row"><span>${l.label}</span><span>${fmt(l.valor)}</span></div>`).join('')}
<div class="row total-row"><span>TOTAL A RECEBER:</span><span>${fmt(totalReceber)}</span></div>` : ''}
${cancLines.length > 0 ? `<div class="section-title">CANCELAMENTOS</div>
${cancLines.map(l => `<div class="row"><span>${l.label}</span><span>${fmt(l.valor)}</span></div>`).join('')}
<div class="row total-row"><span>TOTAL:</span><span>${fmt(totalCancel)}</span></div>` : ''}
<div class="section-title">DETALHAMENTO DE CARTÕES</div>
<table class="cart-table">
<thead>
<tr>
<th>CRÉDITO</th><th>DÉBITO</th><th>ALIMENT.</th><th>PIX</th>
</tr>
</thead>
<tbody>
${cartTableRows}
</tbody>
</table>
<div class="subtotal"><div class="row total-row"><span>Soma Cartões:</span><span>${fmt(totalCartoes)}</span></div></div>
<div class="section-title">SALDO DE CAIXA (SISTEMA) — RESUMO</div>
<div class="row"><span>Troco</span><span>${fmt(data.saldo_troco||0)}</span></div>
<div class="row"><span>Crédito</span><span>${fmt(cartCred)}</span></div>
<div class="row"><span>Débito</span><span>${fmt(cartDeb)}</span></div>
<div class="row"><span>Alimentação</span><span>${fmt(cartAli)}</span></div>
<div class="row"><span>Vales</span><span>${fmt(totalVales)}</span></div>
<div class="row"><span>À Receber</span><span>${fmt(totalReceber)}</span></div>
<div class="row"><span>PIX Cartão</span><span>${fmt(cartPix)}</span></div>
<div class="row"><span>PIX CNPJ</span><span>${fmt(totalPixCnpj)}</span></div>
<div class="row total-row"><span>TOTAL SISTEMA</span><span>${fmt(sysTotal)}</span></div>
<div class="row"><span>Saldo Esperado</span><span>${fmt(saldoEsperado)}</span></div>
<div class="row"><span>DIFERENÇA</span><span class="${diffOk ? 'diff-ok' : 'diff-bad'}">${diffStr}${diffOk ? ' ✓ OK' : ''}</span></div>
<div class="section-title">FECHAMENTO (CONTADO)</div>
<div class="row"><span>Dinheiro:</span><span>${fmt(fdin)}</span></div>
<div class="row"><span>Cartões:</span><span>${fmt(fcarts)}</span></div>
<div class="row"><span>À Receber:</span><span>${fmt(frec)}</span></div>
<div class="row total-row"><span>TOTAL:</span><span>${fmt(ftotal)}</span></div>
${data.observacoes ? `<div class="obs">${data.observacoes}</div>` : ''}
<div class="footer">Gerado em ${new Date().toLocaleString('pt-BR')}</div>
</body></html>`;
return html;
}
function openPrintPreview(data, mode) {
const html = mode === 'confirm' ? buildConfirmHTML(data) : buildPrintHTML(data);
// 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 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>
`;
const fullHtml = `<!DOCTYPE html><html><head><meta charset="UTF-8"><title>Recibo</title>${previewStyle}</head><body>${html}</body></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(fullHtml);
iframeDoc.close();
} }
function triggerWindowPrint() { function triggerWindowPrint() {
@@ -1297,8 +1687,9 @@ function triggerWindowPrint() {
} }
// ─── Recibo 80mm via Tauri (ESC/POS USB) ───────────────────────────────── // ─── Recibo 80mm via Tauri (ESC/POS USB) ─────────────────────────────────
async function tauriRecibo() { // ─── Recibo 80mm via Tauri (ESC/POS USB) ─────────────────────────────────
// Build flat totals for the Rust ReciboData struct // Build data object shared by both ESC/POS and preview
function buildReciboData() {
const cartCred = estado.cartoes.credito.reduce((s,r) => s+(r.valor||0), 0); 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 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 cartAli = estado.cartoes.alimentacao.reduce((s,r) => s+(r.valor||0), 0);
@@ -1311,27 +1702,24 @@ async function tauriRecibo() {
const cancelTotal = estado.cancelamentos.reduce((s,r) => s+(parseNum(r.valor||0)||0), 0); const cancelTotal = estado.cancelamentos.reduce((s,r) => s+(parseNum(r.valor||0)||0), 0);
const fechamentoCounted = parseNum(val('f-fech-dinheiro')) + parseNum(val('f-fech-cartoes')) + parseNum(val('f-fech-receber')); const fechamentoCounted = parseNum(val('f-fech-dinheiro')) + parseNum(val('f-fech-cartoes')) + parseNum(val('f-fech-receber'));
const saldoEsp = parseNum(val('f-saldo-esperado')); const saldoEsp = parseNum(val('f-saldo-esperado'));
const sysTroco = parseNum(val('sys-troco')); // SYS total = troco + crédito + débito + alimentação + vales + receber + pixCartão + pixCnpj
const sysTotal = parseNum(val('sys-troco')) + cartCred + cartDeb + cartAli + valesTotal + receberTotal + cartPix + pixCnpjTotal;
const data = { return {
uuid: estado.uuid, uuid: estado.uuid,
id_local: estado.id_local, id_local: estado.id_local,
loja: estado.loja, loja: estado.loja,
data: estado.data, data: estado.data,
operador: estado.operador, operador: estado.operador,
turno: estado.turno, turno: estado.turno,
saldo_troco: sysTroco, saldo_troco: parseNum(val('sys-troco')),
saldo_esperado: saldoEsp, saldo_esperado: saldoEsp,
fechamento: fechamentoCounted, fechamento: fechamentoCounted,
diferenca: sysTroco - saldoEsp, diferenca: sysTotal - saldoEsp,
despesas: estado.despesas.map(r => ({ desc: r.desc||'', obs: r.obs||'', valor: r.valor||0 })), sys_total: sysTotal,
sangrias: estado.sangrias.map(r => ({ hora: r.hora||'', retirada: parseNum(r.retirada)||0, gerente: r.gerente||'' })), fechamento_dinheiro: parseNum(val('f-fech-dinheiro')),
pixcnpj: estado.pixcnpj.map(r => ({ nome: r.nome||'', valor: r.valor||0 })), fechamento_cartoes: parseNum(val('f-fech-cartoes')),
vales: estado.vales.map(r => ({ nome: r.nome||'', obs: r.obs||'', valor: r.valor||0 })), fechamento_receber: parseNum(val('f-fech-receber')),
receber: estado.receber.map(r => ({ nome: r.nome||'', valor: r.valor||0 })), // Flat totals for ReciboData struct (Rust) — must be numbers
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)
credito: cartCred, credito: cartCred,
debito: cartDeb, debito: cartDeb,
alimentacao: cartAli, alimentacao: cartAli,
@@ -1342,17 +1730,31 @@ async function tauriRecibo() {
vales: valesTotal, vales: valesTotal,
areceber: receberTotal, areceber: receberTotal,
cancelamentos: cancelTotal, cancelamentos: cancelTotal,
// Full arrays for preview
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: estado.cartoes.credito.map(r => r.valor || 0),
cartoes_debito: estado.cartoes.debito.map(r => r.valor || 0),
cartoes_alimentacao: estado.cartoes.alimentacao.map(r => r.valor || 0),
cartoes_pix: estado.cartoes.pix.map(r => r.valor || 0),
observacoes: estado.observacoes, observacoes: estado.observacoes,
sync_status: estado.sync_status, sync_status: estado.sync_status,
}; };
}
// Tenta ESC/POS via Tauri primeiro; se falhar, abre preview async function tauriRecibo() {
const data = buildReciboData();
// Tenta ESC/POS via Tauri primeiro; se falhar, so lanca erro (preview tratado pelo caller)
try { try {
return await window.__TAURI__.core.invoke('imprimir_recibo', { estado: JSON.stringify(data) }); return await window.__TAURI__.core.invoke('imprimir_recibo', { data: JSON.stringify(data) });
} catch(e) { } catch(e) {
console.warn('ESC/POS falhou, abrindo preview:', e); console.warn('ESC/POS falhou:', e);
openPrintPreview(data); throw e; // re-lanca para o caller tratar o preview
throw e;
} }
} }
@@ -1367,13 +1769,17 @@ async function tauriCarregarRascunho() {
// ─── Confirm Modal ──────────────────────────────────────────────────────── // ─── Confirm Modal ────────────────────────────────────────────────────────
function openConfirmModal() { function openConfirmModal() {
console.log('[DEBUG] openConfirmModal chamado');
const operador = val('f-operador').trim(); const operador = val('f-operador').trim();
const turno = val('f-turno').trim(); const turno = val('f-turno').trim();
const loja = val('f-loja').trim(); const loja = val('f-loja').trim();
console.log('[DEBUG] openConfirmModal campos:', {operador, turno, loja});
if (!operador || !turno || !loja) { if (!operador || !turno || !loja) {
console.log('[DEBUG] openConfirmModal: campos faltando, alert');
alert('⚠️ Preencha Operador, Turno e Loja antes de enviar.'); alert('⚠️ Preencha Operador, Turno e Loja antes de enviar.');
return; return;
} }
console.log('[DEBUG] openConfirmModal: campos OK, montando modal');
// Build summary // Build summary
const cartCred = estado.cartoes.credito.reduce((s,r) => s+(r.valor||0), 0); 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 cartDeb = estado.cartoes.debito.reduce((s,r) => s+(r.valor||0), 0);
@@ -1387,7 +1793,8 @@ function openConfirmModal() {
const pixcnpj = estado.pixcnpj.reduce((s,r) => s+(parseNum(r.valor||0)||0), 0); const pixcnpj = estado.pixcnpj.reduce((s,r) => s+(parseNum(r.valor||0)||0), 0);
const fechamento = parseNum(val('f-fech-dinheiro')) + parseNum(val('f-fech-cartoes')) + parseNum(val('f-fech-receber')); const fechamento = parseNum(val('f-fech-dinheiro')) + parseNum(val('f-fech-cartoes')) + parseNum(val('f-fech-receber'));
const saldoEsp = parseNum(val('f-saldo-esperado')); const saldoEsp = parseNum(val('f-saldo-esperado'));
const diferenca = (parseNum(val('sys-troco')) || 0) - saldoEsp; const sysTotal = (parseNum(val('sys-troco')) || 0) + cartCred + cartDeb + cartAli + cartPix + vales + receber + pixcnpj;
const diferenca = sysTotal - saldoEsp;
const diffClass = diferenca === 0 ? '#2e7d32' : (diferenca > 0 ? '#e65100' : '#c0272d'); const diffClass = diferenca === 0 ? '#2e7d32' : (diferenca > 0 ? '#e65100' : '#c0272d');
document.getElementById('confirm-summary').innerHTML = document.getElementById('confirm-summary').innerHTML =
`<div style="margin-bottom:6px"><strong>📅 Data:</strong> ${estado.data} &nbsp; <strong>👤 Operador:</strong> ${operador} &nbsp; <strong>⏰ Turno:</strong> ${turno} &nbsp; <strong>🏪 Loja:</strong> ${loja}</div> `<div style="margin-bottom:6px"><strong>📅 Data:</strong> ${estado.data} &nbsp; <strong>👤 Operador:</strong> ${operador} &nbsp; <strong>⏰ Turno:</strong> ${turno} &nbsp; <strong>🏪 Loja:</strong> ${loja}</div>
@@ -1401,11 +1808,13 @@ function openConfirmModal() {
<div>💳 Débito: <strong>${fmt(cartDeb)}</strong></div> <div>💳 Débito: <strong>${fmt(cartDeb)}</strong></div>
<div>💳 Alimentação: <strong>${fmt(cartAli)}</strong></div> <div>💳 Alimentação: <strong>${fmt(cartAli)}</strong></div>
<div>❌ Cancelamentos: <strong>${fmt(cancelamentos)}</strong></div> <div>❌ Cancelamentos: <strong>${fmt(cancelamentos)}</strong></div>
<div>📋 Fechamento: <strong>${fmt(fechamento)}</strong></div> <div>📋 Fechamento (contado): <strong>${fmt(fechamento)}</strong></div>
<div>🖥️ Total Sistema: <strong>${fmt(sysTotal)}</strong></div>
<hr style="border:none;border-top:1px dashed #b3d9f7;margin:6px 0"> <hr style="border:none;border-top:1px dashed #b3d9f7;margin:6px 0">
<div>🎯 Saldo Esperado: <strong>${fmt(saldoEsp)}</strong></div> <div>🎯 Saldo Esperado (contado): <strong>${fmt(saldoEsp)}</strong></div>
<div style="margin-top:4px">📐 Diferença: <strong style="color:${diffClass}">${fmt(diferenca)}</strong></div> <div style="margin-top:4px">📐 Diferença: <strong style="color:${diffClass}">${fmt(diferenca)}</strong></div>
<div style="margin-top:4px;font-size:10px;color:#888">ID: ${estado.loja}_${estado.data}_${estado.operador}</div>`; <div style="margin-top:4px;font-size:10px;color:#888">ID: ${estado.loja}_${estado.data}_${estado.operador}</div>`;
console.log('[DEBUG] openConfirmModal: tentando abrir modal');
document.getElementById('modal-confirm').classList.add('open'); document.getElementById('modal-confirm').classList.add('open');
} }
@@ -1417,30 +1826,20 @@ document.getElementById('confirm-close').addEventListener('click', closeConfirmM
document.getElementById('confirm-cancel').addEventListener('click', closeConfirmModal); document.getElementById('confirm-cancel').addEventListener('click', closeConfirmModal);
document.getElementById('modal-confirm').addEventListener('click', e => { if (e.target.id === 'modal-confirm') closeConfirmModal(); }); document.getElementById('modal-confirm').addEventListener('click', e => { if (e.target.id === 'modal-confirm') closeConfirmModal(); });
// Print from confirm modal — abre preview no navegador // Print from confirm modal — abre preview idêntico ao modal de confirmação
document.getElementById('confirm-print').addEventListener('click', async () => { document.getElementById('confirm-print').addEventListener('click', async () => {
// Usa buildReciboData para não duplicar lógica e ter todos os campos certos
let data;
try {
data = buildReciboData();
} catch(e) {
console.error('Erro ao montar dados do recibo:', e);
alert('Erro ao montar dados do recibo.');
return;
}
// Fecha modal APÓS gerar preview (não antes)
closeConfirmModal(); closeConfirmModal();
// Reconstrói os mesmos dados do tauriRecibo e abre preview openPrintPreview(data, 'confirm');
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 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')),
despesas: estado.despesas.map(r => ({ desc: r.desc||'', obs: r.obs||'', valor: r.valor||0 })),
sangrias: estado.sangrias.map(r => ({ hora: r.hora||'', retirada: parseNum(r.retirada)||0, gerente: r.gerente||'' })),
pixcnpj: estado.pixcnpj.map(r => ({ nome: r.nome||'', valor: r.valor||0 })),
vales: estado.vales.map(r => ({ nome: r.nome||'', obs: r.obs||'', valor: r.valor||0 })),
receber: estado.receber.map(r => ({ nome: r.nome||'', valor: r.valor||0 })),
cancelamentos: estado.cancelamentos.map(r => ({ numero: r.numero||'', valor: r.valor||0, motivo: r.motivo||'' })),
credito: cartCred, debito: cartDeb, alimentacao: cartAli,
cartoes: { pix: cartPix },
observacoes: estado.observacoes,
};
openPrintPreview(data);
}); });
// Confirm send // Confirm send
@@ -1453,10 +1852,17 @@ document.getElementById('confirm-send').addEventListener('click', async () => {
const r = await tauriEnviar(); const r = await tauriEnviar();
estado.sync_status = 'synced'; estado.sync_status = 'synced';
saveState(); saveState();
alert('✅ Enviado com sucesso!'); alert('✅ Enviado com sucesso!\n\n📋 Tabela: fechamentos_web\n🌐 Servidor: supabase.ofrangao.com.br\n\nAbra o Supabase Studio para verificar.');
} catch(e) { } catch(e) {
await tauriSaveLocal(); await tauriSaveLocal();
alert('Erro ao enviar: ' + (e.message || e) + '\n(Dados salvos localmente)'); const msg = e?.message || e?.toString?.() || String(e);
alert(
'⚠️ Erro ao enviar para o Supabase.\n\n' +
'📋 Tabela: fechamentos_web\n' +
'🌐 Servidor: supabase.ofrangao.com.br\n\n' +
'O servidor pode estar offline. Dados salvos localmente.\n\n' +
'Erro: ' + msg
);
} finally { } finally {
btn.disabled = false; btn.disabled = false;
btn.textContent = '💾 Enviar Fechamento'; btn.textContent = '💾 Enviar Fechamento';
@@ -1465,6 +1871,11 @@ document.getElementById('confirm-send').addEventListener('click', async () => {
// ─── Buttons ─────────────────────────────────────────────────────────────── // ─── Buttons ───────────────────────────────────────────────────────────────
document.getElementById('btn-enviar').addEventListener('click', () => { document.getElementById('btn-enviar').addEventListener('click', () => {
console.log('[DEBUG] btn-enviar clicado');
const operador = document.getElementById('f-operador')?.value;
const turno = document.getElementById('f-turno')?.value;
const loja = document.getElementById('f-loja')?.value;
console.log('[DEBUG] operador:', operador, 'turno:', turno, 'loja:', loja);
openConfirmModal(); openConfirmModal();
}); });
@@ -1472,13 +1883,19 @@ document.getElementById('btn-recibo').addEventListener('click', async () => {
const btn = document.getElementById('btn-recibo'); const btn = document.getElementById('btn-recibo');
btn.disabled = true; btn.disabled = true;
try { try {
// Tenta ESC/POS primeiro; se falhar, abre preview no navegador // Tenta ESC/POS primeiro; se falhar, nada acontece visualmente
// — siempre abre preview depois para o usuário ver algo
await tauriRecibo(); await tauriRecibo();
// Se chegou aqui sem erro, ESC/POS funcionou
} catch(e) { } catch(e) {
// ESC/POS falhou — openPrintPreview já foi chamado dentro de tauriRecibo console.warn('ESC/POS falhou:', e);
// Só mostra alerta se nem o preview abriu }
console.warn('Recibo:', e); // Sempre abre preview depois da tentativa ESC/POS
try {
const data = buildReciboData();
openPrintPreview(data, 'recibo');
} catch(e2) {
console.error('Preview falhou:', e2);
alert('Erro ao gerar preview do recibo.');
} finally { } finally {
btn.disabled = false; btn.disabled = false;
} }
@@ -1487,61 +1904,121 @@ document.getElementById('btn-recibo').addEventListener('click', async () => {
document.getElementById('btn-limpar').addEventListener('click', () => { document.getElementById('btn-limpar').addEventListener('click', () => {
if (!confirm('Limpar rascunho? Esta acção não pode ser revertida.')) return; if (!confirm('Limpar rascunho? Esta acção não pode ser revertida.')) return;
localStorage.removeItem(LS_KEY); localStorage.removeItem(LS_KEY);
location.reload(); // Reseta estado e re-renderiza sem reload
estado = {
uuid: crypto.randomUUID ? crypto.randomUUID() : Date.now().toString(),
loja: estado.loja, data: estado.data, operador: '', turno: '',
saldo_troco: 0, fechamento: 0, saldo_esperado: 0, diferenca: 0,
despesas: [], sangrias: [], pixcnpj: [], vales: [], receber: [], cancelamentos: [],
cartoes: { credito: [], debito: [], alimentacao: [], pix: [] },
fechamento_contado: { dinheiro: 0, cartoes: 0, receber: 0 },
observacoes: '', id_local: null, sync_status: 'local'
};
setVal('f-operador', ''); setVal('f-turno', '');
setVal('f-fech-dinheiro', ''); setVal('f-fech-cartoes', ''); setVal('f-fech-receber', '');
setVal('f-saldo-esperado', ''); setVal('f-obs', '');
setVal('sys-troco', '');
calcular();
renderAllTables();
syncUI();
showToast('Rascunho limpo.', 'info');
}); });
document.getElementById('btn-anterior').addEventListener('click', async () => { const btnAnterior = document.getElementById('btn-anterior');
if (btnAnterior) {
btnAnterior.addEventListener('click', async () => {
console.log('[DEBUG] btn-anterior clicado');
const loja = val('f-loja') || 'União'; const loja = val('f-loja') || 'União';
console.log('[DEBUG] btn-anterior loja:', loja);
// Carrega lista de datas recentes do Supabase // Salva rascunho atual antes de tudo
let datas = []; await tauriSaveLocal();
try {
const lista = await window.__TAURI__.core.invoke('sb_listar_recentes', { loja, limite: 20 }); // Calcula dia anterior
if (lista && lista.length > 0) { const prevDate = new Date(estado.data);
// Extrai datas únicas e já ordenadas prevDate.setDate(prevDate.getDate() - 1);
datas = [...new Set(lista.map(l => l.data).filter(Boolean))].sort().reverse(); const prevDateStr = prevDate.toISOString().split('T')[0];
// Confirmação explícita antes de trocar de dia
const confirmMsg = `ATENÇÃO: vai carregar o fechamento de ${prevDateStr}.\n\nO formulário atual será LIMPO caso não tenha enviado.\n\nContinuar?`;
if (!confirm(confirmMsg)) {
console.log('[DEBUG] btn-anterior cancelado pelo usuário');
return;
} }
// Carrega lista de fechamentos recentes do Supabase com uuid
let recentList = [];
try {
recentList = await window.__TAURI__.core.invoke('sb_listar_recentes', { loja, limite: 20 });
console.log('[DEBUG] sb_listar_recentes retornou:', recentList?.length, 'itens');
} catch(e) { } catch(e) {
console.warn('sb_listar_recentes falhou:', 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();
}
console.log('[DEBUG] dateList:', dateList);
let selectedDate = null; let selectedDate = null;
if (datas.length > 0) { let selectedId = null;
// Mostra seletor com as datas disponíveis if (dateList.length > 0) {
const msg = datas.map((d, i) => `${i + 1}) ${d}`).join('\n'); 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)`); 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() !== '') { if (escolha !== null && escolha.trim() !== '') {
const idx = parseInt(escolha) - 1; 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) { if (!selectedDate) {
const d = new Date(estado.data); d.setDate(d.getDate() - 1); selectedDate = prevDateStr;
selectedDate = d.toISOString().split('T')[0];
} }
console.log('[DEBUG] selectedDate:', selectedDate, 'selectedId:', selectedId);
await tauriSaveLocal();
setVal('f-data', selectedDate); setVal('f-data', selectedDate);
estado.data = selectedDate; estado.data = selectedDate;
estado.uuid = crypto.randomUUID ? crypto.randomUUID() : Date.now().toString(); estado.uuid = crypto.randomUUID ? crypto.randomUUID() : Date.now().toString();
estado.id_local = null; estado.id_local = null;
syncHeader(); syncHeader();
// Tenta Supabase primeiro, senão local // Tenta Supabase primeiro (com id se disponível), senão local
let rascunho = null; let rascunho = null;
if (selectedId) {
try {
rascunho = await window.__TAURI__.core.invoke('sb_buscar_por_id', { id: selectedId });
console.log('[DEBUG] sb_buscar_por_id retornou:', !!rascunho);
} catch(e) { console.warn('sb_buscar_por_id falhou:', e); }
}
if (!rascunho) {
try { try {
rascunho = await window.__TAURI__.core.invoke('sb_carregar_rascunho', { loja, data: selectedDate }); rascunho = await window.__TAURI__.core.invoke('sb_carregar_rascunho', { loja, data: selectedDate });
console.log('[DEBUG] sb_carregar_rascunho retornou:', !!rascunho);
} catch(e) { console.warn('sb_carregar_rascunho falhou:', e); } } catch(e) { console.warn('sb_carregar_rascunho falhou:', e); }
}
if (!rascunho) { if (!rascunho) {
try { try {
rascunho = await window.__TAURI__.core.invoke('carregar_rascunho', { loja, data: selectedDate }); rascunho = await window.__TAURI__.core.invoke('carregar_rascunho', { loja, data: selectedDate });
console.log('[DEBUG] carregar_rascunho local retornou:', !!rascunho);
} catch(e) { console.warn('carregar_rascunho local falhou:', e); } } catch(e) { console.warn('carregar_rascunho local falhou:', e); }
} }
if (rascunho) { if (rascunho) {
console.log('[DEBUG] rascunho carregado com sucesso');
if (rascunho.operador) { setVal('f-operador', rascunho.operador); estado.operador = rascunho.operador; } if (rascunho.operador) { setVal('f-operador', rascunho.operador); estado.operador = rascunho.operador; }
if (rascunho.turno) { setVal('f-turno', rascunho.turno); estado.turno = rascunho.turno; } if (rascunho.turno) { setVal('f-turno', rascunho.turno); estado.turno = rascunho.turno; }
if (rascunho.saldo_troco) setVal('sys-troco', fmtNum(rascunho.saldo_troco)); if (rascunho.saldo_troco) setVal('sys-troco', fmtNum(rascunho.saldo_troco));
@@ -1566,26 +2043,31 @@ document.getElementById('btn-anterior').addEventListener('click', async () => {
estado.cancelamentos = rascunho.cancelamentos.map(c => ({ numero: c.numero || '', valor: parseNum(c.valor) || 0, motivo: c.motivo || '' })); estado.cancelamentos = rascunho.cancelamentos.map(c => ({ numero: c.numero || '', valor: parseNum(c.valor) || 0, motivo: c.motivo || '' }));
} }
if (rascunho.cartoes) { if (rascunho.cartoes) {
estado.cartoes.credito = (rascunho.cartoes.credito || []).map(v => ({ valor: parseNum(v) || 0 })); const norm = (arr) => Array.isArray(arr) ? arr.map(v => ({ valor: (typeof v === 'object' && v !== null) ? (parseNum(v.valor) || 0) : (parseNum(v) || 0) })) : [];
estado.cartoes.debito = (rascunho.cartoes.debito || []).map(v => ({ valor: parseNum(v) || 0 })); estado.cartoes.credito = norm(rascunho.cartoes.credito);
estado.cartoes.alimentacao = (rascunho.cartoes.alimentacao || []).map(v => ({ valor: parseNum(v) || 0 })); estado.cartoes.debito = norm(rascunho.cartoes.debito);
estado.cartoes.pix = (rascunho.cartoes.pix || []).map(v => ({ valor: parseNum(v) || 0 })); estado.cartoes.alimentacao = norm(rascunho.cartoes.alimentacao);
estado.cartoes.pix = norm(rascunho.cartoes.pix);
} }
} else { } else {
console.log('[DEBUG] nenhum rascunho encontrado para esta data');
estado.despesas = []; estado.sangrias = []; estado.pixcnpj = []; estado.despesas = []; estado.sangrias = []; estado.pixcnpj = [];
estado.vales = []; estado.receber = []; estado.cancelamentos = []; estado.vales = []; estado.receber = []; estado.cancelamentos = [];
estado.cartoes = { credito: [], debito: [], alimentacao: [], pix: [] }; estado.cartoes = { credito: [], debito: [], alimentacao: [], pix: [] };
setVal('sys-troco', ''); setVal('f-saldo-esperado', ''); setVal('f-obs', ''); setVal('sys-troco', ''); setVal('f-saldo-esperado', ''); setVal('f-obs', '');
} }
// Marcar que este é um rascunho carregado (não novo)
estado.sync_status = rascunho ? 'loaded' : 'local'; estado.sync_status = rascunho ? 'loaded' : 'local';
PRE_FILL_TABLES.forEach(ensureOneRow); PRE_FILL_TABLES.forEach(ensureOneRow);
Object.keys(TABLES).forEach(renderTable); Object.keys(TABLES).forEach(renderTable);
updateTotals(); updateTotals();
scheduleSave(); scheduleSave();
console.log('[DEBUG] btn-anterior concluído');
}); });
} else {
console.error('[DEBUG] btn-anterior NÃO encontrado no DOM!');
}
document.getElementById('f-obs').addEventListener('input', e => { estado.observacoes = e.target.value; scheduleSave(); }); document.getElementById('f-obs').addEventListener('input', e => { estado.observacoes = e.target.value; scheduleSave(); });