Compare commits
17 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| fea957dfb9 | |||
| 130b2b6da7 | |||
| b47d7766ad | |||
| 78fa778197 | |||
| 5e4340adca | |||
| c01133f39c | |||
| a6b295a6d0 | |||
| f850831e98 | |||
| e70f31e00f | |||
| 79e1dbd645 | |||
| cce8f3df60 | |||
| 318d44e934 | |||
| bfbcd89be9 | |||
| 1848c1aa35 | |||
| 9176b764fd | |||
| 5cab38a27c | |||
| 5b1f3a2691 |
+13
@@ -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
|
||||||
|
|||||||
@@ -0,0 +1,140 @@
|
|||||||
|
# Fechamento de Caixa — O Frangão
|
||||||
|
|
||||||
|
App desktop nativo para fechamento de caixa com impressão em impressora térmica 80mm.
|
||||||
|
|
||||||
|
## Stack
|
||||||
|
|
||||||
|
- **Backend:** Tauri 2 + Rust
|
||||||
|
- **Frontend:** HTML + CSS + JavaScript (vanilla, ~1700 linhas)
|
||||||
|
- **Storage local:** SQLite (rusqlite) — funciona offline
|
||||||
|
- **Sync:** Supabase REST API
|
||||||
|
- **Impressão:** ESC/POS via USB (TM-T20 e compatíveis, Windows)
|
||||||
|
|
||||||
|
## Estrutura do Projeto
|
||||||
|
|
||||||
|
```
|
||||||
|
fechamento-caixa/
|
||||||
|
├── src/
|
||||||
|
│ └── index.html # Frontend completo (HTML + CSS + JS inline)
|
||||||
|
├── src-tauri/
|
||||||
|
│ ├── Cargo.toml # Dependências Rust
|
||||||
|
│ ├── tauri.conf.json # Config do app Tauri
|
||||||
|
│ └── src/
|
||||||
|
│ ├── main.rs # Entry point — registra comandos Tauri
|
||||||
|
│ └── plugins/
|
||||||
|
│ ├── supabase.rs # Integração Supabase (REST API)
|
||||||
|
│ ├── printer.rs # Geração ESC/POS + envio USB
|
||||||
|
│ ├── storage.rs # SQLite local
|
||||||
|
│ ├── app.rs # Config da app (loja, operador default)
|
||||||
|
│ └── escpos.rs # Helpers ESC/POS (INIT, CUT, divider, etc.)
|
||||||
|
└── README.md
|
||||||
|
```
|
||||||
|
|
||||||
|
## Pré-requisitos
|
||||||
|
|
||||||
|
- Rust 1.70+ (`rustup install stable`)
|
||||||
|
- Node.js 18+ (para build do frontend Tauri)
|
||||||
|
- Windows 10/11 (impressão USB só funciona no Windows)
|
||||||
|
|
||||||
|
## Setup
|
||||||
|
|
||||||
|
### 1. Clonar o repositório
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone https://git.ofrangao.com.br/filipe/fechamento-caixa.git
|
||||||
|
cd fechamento-caixa
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Configurar chaves do Supabase
|
||||||
|
|
||||||
|
Crie o arquivo `config.json` em `%LOCALAPPDATA%\FechamentoCaixa\config.json` (Windows):
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"SUPABASE_ANON_KEY": "sua_chave_anon_aqui",
|
||||||
|
"SUPABASE_SERVICE_KEY": "sua_chave_service_role_aqui"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
O app procura esse arquivo na inicialização. Sem ele, tenta ler das variáveis de ambiente `SUPABASE_ANON_KEY` e `SUPABASE_SERVICE_KEY`.
|
||||||
|
|
||||||
|
### 3. Build
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd src-tauri
|
||||||
|
cargo build --release --target x86_64-pc-windows-gnu
|
||||||
|
```
|
||||||
|
|
||||||
|
O executável fica em:
|
||||||
|
```
|
||||||
|
src-tauri/target/x86_64-pc-windows-gnu/release/fechamento-caixa.exe
|
||||||
|
```
|
||||||
|
|
||||||
|
Para gerar o pacote de distribuição:
|
||||||
|
```bash
|
||||||
|
cp src-tauri/target/x86_64-pc-windows-gnu/release/fechamento-caixa.exe .
|
||||||
|
cp src-tauri/target/x86_64-pc-windows-gnu/release/WebView2Loader.dll .
|
||||||
|
tar -czf fechamento-caixa-vX.X.X.tar.gz fechamento-caixa.exe WebView2Loader.dll
|
||||||
|
```
|
||||||
|
|
||||||
|
## Códigos de comando Tauri
|
||||||
|
|
||||||
|
O frontend chama esses comandos via `window.__TAURI__.core.invoke()`:
|
||||||
|
|
||||||
|
| Comando | Arquivo | Descrição |
|
||||||
|
|---------|---------|-----------|
|
||||||
|
| `salvar_fechamento` | storage.rs | Salva no SQLite local |
|
||||||
|
| `carregar_rascunho` | storage.rs | Busca rascunho por loja+data |
|
||||||
|
| `listar_fechamentos` | storage.rs | Lista histórico do SQLite |
|
||||||
|
| `buscar_fechamento_por_id` | storage.rs | Busca por ID no SQLite |
|
||||||
|
| `sb_salvar_fechamento` | supabase.rs | Envia para Supabase |
|
||||||
|
| `sb_carregar_rascunho` | supabase.rs | Busca rascunho no Supabase |
|
||||||
|
| `sb_listar_recentes` | supabase.rs | Lista fechamentos recentes |
|
||||||
|
| `sb_buscar_por_id` | supabase.rs | Busca por ID no Supabase |
|
||||||
|
| `sb_salvar_listas` | supabase.rs | Salva operadores/gerentes |
|
||||||
|
| `sb_carregar_listas` | supabase.rs | Carrega listas do servidor |
|
||||||
|
| `sb_online` | supabase.rs | Verifica conectividade |
|
||||||
|
| `imprimir_recibo` | printer.rs | Envia bytes ESC/POS pra USB |
|
||||||
|
| `detectar_impressora` | printer.rs | Detecta porta USB |
|
||||||
|
| `configurar_impressora` | printer.rs | Define porta manual |
|
||||||
|
| `teste_impressora` | printer.rs | Imprime página de teste |
|
||||||
|
| `get_loja` / `set_loja` | app.rs | Loja atual |
|
||||||
|
| `get_config` / `set_config` | app.rs | Config completo |
|
||||||
|
|
||||||
|
## Fluxo de dados
|
||||||
|
|
||||||
|
```
|
||||||
|
[Usuário preenche formulário]
|
||||||
|
│
|
||||||
|
▼ (auto-save a cada 1.5s)
|
||||||
|
SQLite local (fechamento.db)
|
||||||
|
│
|
||||||
|
├── [Botão Enviar] ──► Supabase (fechamentos_web)
|
||||||
|
│
|
||||||
|
└── [Botão Recibo] ──► printer.rs ──► to_escpos() ──► USB
|
||||||
|
│
|
||||||
|
├── Sucesso: imprime
|
||||||
|
└── Falha: openPrintPreview() ──► iframe ──► window.print()
|
||||||
|
```
|
||||||
|
|
||||||
|
## Campos do formulário
|
||||||
|
|
||||||
|
| Campo | Tabela Supabase | Notas |
|
||||||
|
|-------|----------------|-------|
|
||||||
|
| uuid, loja, data, operador, turno | fechamentos_web | PK composto |
|
||||||
|
| saldo_troco, saldo_esperado, fechamento, diferenca | fechamentos_web | diferenca = fechamento - saldo_esperado |
|
||||||
|
| despesas[], sangrias[], vales[], receber[], pixcnpj[], cancelamentos[] | dados (JSON) | Arrays de {desc/nome, valor, ...} |
|
||||||
|
| cartoes{credito[],debito[],alimentacao[],pix[]} | dados (JSON) | Arrays de {valor} |
|
||||||
|
| operadores[], gerentes[], despesas_custom[], clientes[] | listas_personalizadas | Listas por loja |
|
||||||
|
|
||||||
|
## Problemas conhecidos
|
||||||
|
|
||||||
|
- Impressão USB só funciona no Windows
|
||||||
|
- `service_role_key` no cliente desktop é risco de segurança — usar apenas anon_key + RLS
|
||||||
|
- Sem testes automatizados
|
||||||
|
|
||||||
|
## Version History
|
||||||
|
|
||||||
|
- **v1.2.7** — 2026-06-24: 4 bugs críticos concertados (impressão, enviar, dia anterior, localStorage)
|
||||||
|
- **v1.2.6** — 2026-06-24: WebView2Loader.dll incluso; init() restaura operador+turno
|
||||||
|
- **v1.2.5** — 2026-06-24: diferenca = fechamento - esperado (não mais saldo_troco - esperado)
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Generated
-35
@@ -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"
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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"
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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,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/*": "./"
|
||||||
|
|||||||
+541
-151
@@ -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>
|
||||||
@@ -1229,68 +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 diferenca = (data.fechamento || 0) - (data.saldo_esperado || 0);
|
const fmtNeg = v => (v < 0 ? '-' : '') + 'R$ ' + Math.abs(v || 0).toFixed(2).replace('.', ',');
|
||||||
const diffStr = diferenca >= 0 ? `+${fmt(diferenca)}` : fmt(diferenca);
|
const diferenca = data.diferenca !== undefined ? data.diferenca
|
||||||
const diffColor = diferenca === 0 ? '#2e7d32' : (diferenca > 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 : [];
|
||||||
// Cancelamentos (sum the array first — single cancelamentos is a float total, array is from confirm-print)
|
const despesas_arr = Array.isArray(data.despesas_arr) ? data.despesas_arr : [];
|
||||||
const cancelTotal = Array.isArray(data.cancelamentos)
|
const pixcnpj_arr = Array.isArray(data.pixcnpj_arr) ? data.pixcnpj_arr : [];
|
||||||
? data.cancelamentos.reduce((s, c) => s + (parseNum(c.valor || 0) || 0), 0)
|
const vales_arr = Array.isArray(data.vales_arr) ? data.vales_arr : [];
|
||||||
: (data.cancelamentos || 0);
|
const receber_arr = Array.isArray(data.receber_arr) ? data.receber_arr : [];
|
||||||
// PIX total (flat number from tauriRecibo; pixcnpj is also flat total)
|
const cancelamentos_arr = Array.isArray(data.cancelamentos_arr) ? data.cancelamentos_arr : [];
|
||||||
const pixTotal = data.pix || data.voucher || 0;
|
|
||||||
const pixCnpjTotal = typeof data.pixcnpj === 'number' ? data.pixcnpj : 0;
|
const cartCred_arr = Array.isArray(data.cartoes_credito) ? data.cartoes_credito : [];
|
||||||
// Sangrias
|
const cartDeb_arr = Array.isArray(data.cartoes_debito) ? data.cartoes_debito : [];
|
||||||
if (data.sangrias && data.sangrias.length > 0) {
|
const cartAli_arr = Array.isArray(data.cartoes_alimentacao) ? data.cartoes_alimentacao : [];
|
||||||
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 cartPix_arr = Array.isArray(data.cartoes_pix) ? data.cartoes_pix : [];
|
||||||
}
|
|
||||||
// Despesas
|
const totalSangrias = sangrias_arr.reduce((s,r) => s+(r.retirada||0)+(r.valor||0), 0);
|
||||||
if (data.despesas && data.despesas.length > 0) {
|
const totalDespesas = despesas_arr.reduce((s,r) => s+(r.valor||0), 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 totalPixCnpj = pixcnpj_arr.reduce((s,r) => s+(r.valor||0), 0);
|
||||||
}
|
const totalVales = vales_arr.reduce((s,r) => s+(r.valor||0), 0);
|
||||||
// Vales
|
const totalReceber = receber_arr.reduce((s,r) => s+(r.valor||0), 0);
|
||||||
if (data.vales && data.vales.length > 0) {
|
const totalCancel = cancelamentos_arr.reduce((s,r) => s+(r.valor||0), 0);
|
||||||
data.vales.forEach(v => { if (v.nome || v.valor) rows += `<tr><td>${v.nome||''}</td><td>${fmt(v.valor)}</td><td>${v.obs||''}</td></tr>`; });
|
|
||||||
}
|
const cartCred = cartCred_arr.reduce((s,v) => s+(v||0), 0);
|
||||||
// Receber
|
const cartDeb = cartDeb_arr.reduce((s,v) => s+(v||0), 0);
|
||||||
if (data.receber && data.receber.length > 0) {
|
const cartAli = cartAli_arr.reduce((s,v) => s+(v||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 cartPix = cartPix_arr.reduce((s,v) => s+(v||0), 0);
|
||||||
}
|
const totalCartoes = cartCred + cartDeb + cartAli + cartPix;
|
||||||
// PIX CNPJ (flat total, not array)
|
|
||||||
if (pixCnpjTotal > 0) {
|
const fdin = data.fechamento_dinheiro || 0;
|
||||||
rows += `<tr><td>PIX CNPJ</td><td>${fmt(pixCnpjTotal)}</td><td></td></tr>`;
|
const fcarts = data.fechamento_cartoes || 0;
|
||||||
}
|
const frec = data.fechamento_receber || 0;
|
||||||
// Cancelamentos
|
const ftotal = data.fechamento || (fdin + fcarts + frec);
|
||||||
if (cancelTotal > 0) {
|
const sysTotal = data.sys_total || 0;
|
||||||
rows += `<tr><td>Cancelamentos</td><td>${fmt(cancelTotal)}</td><td></td></tr>`;
|
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; } }
|
||||||
@@ -1305,61 +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||0)}</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||0)}</span></div>
|
|
||||||
<div class="row"><span>Alimentação:</span><span>${fmt(data.alimentacao||0)}</span></div>
|
${receber_arr.length > 0 ? `<div class="section-title">A RECEBER</div>
|
||||||
<div class="row"><span>PIX Cartão:</span><span>${fmt(pixTotal)}</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(cancelTotal)}</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 ──────────────────────────────────────────────────
|
||||||
// Gera HTML e injeta no body de uma div temporária para window.print()
|
function buildPrintHTML(data) {
|
||||||
const html = buildPrintHTML(data);
|
const fmt = v => 'R$ ' + (v || 0).toFixed(2).replace('.', ',');
|
||||||
const style = document.createElement('style');
|
const fmtNeg = v => (v < 0 ? '-' : '') + 'R$ ' + Math.abs(v || 0).toFixed(2).replace('.', ',');
|
||||||
style.textContent = `
|
const diferenca = data.diferenca !== undefined ? data.diferenca
|
||||||
@media print {
|
: ((data.sys_total || 0) - (data.saldo_esperado || 0));
|
||||||
body { width: 80mm; margin: 0 auto; font-family: 'Courier New', monospace; font-size: 13px; }
|
const diffStr = diferenca >= 0 ? `+${fmt(diferenca)}` : fmtNeg(diferenca);
|
||||||
.no-print { display: none !important; }
|
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;
|
||||||
}
|
}
|
||||||
.print-actions { position: fixed; top: 10px; right: 10px; z-index: 9999; }
|
|
||||||
.print-actions button { padding: 8px 16px; cursor: pointer; background: #2563eb; color: #fff; border: none; border-radius: 4px; font-size: 14px; }
|
// Sangrias rows
|
||||||
.print-actions button:hover { background: #1d4ed8; }
|
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 actions = document.createElement('div');
|
|
||||||
actions.className = 'print-actions no-print';
|
|
||||||
actions.innerHTML = '<button onclick="window.print()">🖨️ Imprimir (Ctrl+P)</button>';
|
|
||||||
|
|
||||||
// Guarda conteúdo original
|
const previewStyle = `
|
||||||
const originalBody = document.body.innerHTML;
|
<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>
|
||||||
|
`;
|
||||||
|
|
||||||
document.body.innerHTML = html;
|
const fullHtml = `<!DOCTYPE html><html><head><meta charset="UTF-8"><title>Recibo</title>${previewStyle}</head><body>${html}</body></html>`;
|
||||||
document.head.appendChild(style);
|
|
||||||
document.body.insertBefore(actions, document.body.firstChild);
|
|
||||||
|
|
||||||
// Quando fechar print dialog, restaura
|
const iframe = document.createElement('iframe');
|
||||||
const onAfterPrint = () => {
|
iframe.style.cssText = 'width:360px; height:600px; border:2px solid #555; margin-top:16px; background:#fff;';
|
||||||
document.body.innerHTML = originalBody;
|
iframe.id = 'print-iframe';
|
||||||
window.removeEventListener('afterprint', onAfterPrint);
|
|
||||||
// Re-carrega scripts essenciais
|
const btnFechar = document.createElement('button');
|
||||||
location.reload();
|
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);
|
||||||
};
|
};
|
||||||
window.addEventListener('afterprint', onAfterPrint);
|
|
||||||
|
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() {
|
||||||
@@ -1368,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);
|
||||||
@@ -1382,18 +1702,23 @@ 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 data = {
|
const sysTotal = parseNum(val('sys-troco')) + cartCred + cartDeb + cartAli + valesTotal + receberTotal + cartPix + pixCnpjTotal;
|
||||||
|
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: fechamentoCounted - saldoEsp,
|
diferenca: sysTotal - saldoEsp,
|
||||||
|
sys_total: sysTotal,
|
||||||
|
fechamento_dinheiro: parseNum(val('f-fech-dinheiro')),
|
||||||
|
fechamento_cartoes: parseNum(val('f-fech-cartoes')),
|
||||||
|
fechamento_receber: parseNum(val('f-fech-receber')),
|
||||||
// Flat totals for ReciboData struct (Rust) — must be numbers
|
// Flat totals for ReciboData struct (Rust) — must be numbers
|
||||||
credito: cartCred,
|
credito: cartCred,
|
||||||
debito: cartDeb,
|
debito: cartDeb,
|
||||||
@@ -1405,25 +1730,31 @@ async function tauriRecibo() {
|
|||||||
vales: valesTotal,
|
vales: valesTotal,
|
||||||
areceber: receberTotal,
|
areceber: receberTotal,
|
||||||
cancelamentos: cancelTotal,
|
cancelamentos: cancelTotal,
|
||||||
// Full arrays for backend/storage
|
// Full arrays for preview
|
||||||
despesas_arr: estado.despesas.map(r => ({ desc: r.desc||'', obs: r.obs||'', valor: r.valor||0 })),
|
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||'' })),
|
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 })),
|
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 })),
|
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 })),
|
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||'' })),
|
cancelamentos_arr: estado.cancelamentos.map(r => ({ numero: r.numero||'', valor: r.valor||0, motivo: r.motivo||'' })),
|
||||||
cartoes: { credito: cartCred, debito: cartDeb, alimentacao: cartAli, pix: cartPix },
|
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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1438,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);
|
||||||
@@ -1458,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} <strong>👤 Operador:</strong> ${operador} <strong>⏰ Turno:</strong> ${turno} <strong>🏪 Loja:</strong> ${loja}</div>
|
`<div style="margin-bottom:6px"><strong>📅 Data:</strong> ${estado.data} <strong>👤 Operador:</strong> ${operador} <strong>⏰ Turno:</strong> ${turno} <strong>🏪 Loja:</strong> ${loja}</div>
|
||||||
@@ -1472,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');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1488,34 +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();
|
||||||
const cartCred = estado.cartoes.credito.reduce((s,r) => s+(r.valor||0), 0);
|
openPrintPreview(data, 'confirm');
|
||||||
const cartDeb = estado.cartoes.debito.reduce((s,r) => s+(r.valor||0), 0);
|
|
||||||
const cartAli = estado.cartoes.alimentacao.reduce((s,r) => s+(r.valor||0), 0);
|
|
||||||
const cartPix = estado.cartoes.pix.reduce((s,r) => s+(r.valor||0), 0);
|
|
||||||
const cancelTotal = estado.cancelamentos.reduce((s,r) => s+(parseNum(r.valor||0)||0), 0);
|
|
||||||
const fechamentoCounted = parseNum(val('f-fech-dinheiro')) + parseNum(val('f-fech-cartoes')) + parseNum(val('f-fech-receber'));
|
|
||||||
const saldoEsp = parseNum(val('f-saldo-esperado'));
|
|
||||||
const data = {
|
|
||||||
loja: estado.loja, data: estado.data, operador: estado.operador, turno: estado.turno,
|
|
||||||
saldo_troco: parseNum(val('sys-troco')) || 0,
|
|
||||||
saldo_esperado: saldoEsp,
|
|
||||||
fechamento: fechamentoCounted,
|
|
||||||
diferenca: fechamentoCounted - saldoEsp,
|
|
||||||
despesas: estado.despesas.map(r => ({ desc: r.desc||'', obs: r.obs||'', valor: r.valor||0 })),
|
|
||||||
sangrias: estado.sangrias.map(r => ({ hora: r.hora||'', retirada: parseNum(r.retirada)||0, gerente: r.gerente||'' })),
|
|
||||||
pixcnpj: estado.pixcnpj.map(r => ({ nome: r.nome||'', valor: r.valor||0 })),
|
|
||||||
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,
|
|
||||||
pix: cartPix, // flat float for buildPrintHTML
|
|
||||||
cartoes: { credito: estado.cartoes.credito, debito: estado.cartoes.debito, alimentacao: estado.cartoes.alimentacao, pix: estado.cartoes.pix },
|
|
||||||
observacoes: estado.observacoes,
|
|
||||||
};
|
|
||||||
openPrintPreview(data);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Confirm send
|
// Confirm send
|
||||||
@@ -1547,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();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -1554,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;
|
||||||
}
|
}
|
||||||
@@ -1569,19 +1904,63 @@ 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);
|
||||||
|
const hasTauri = !!(window.__TAURI__ && window.__TAURI__.core);
|
||||||
|
|
||||||
|
// Salva rascunho atual antes de tudo
|
||||||
|
if (hasTauri) {
|
||||||
|
try { await window.__TAURI__.core.invoke('salvar_rascunho_local', {}); } catch(e) { console.warn('salvar_rascunho_local:', e); }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calcula dia anterior
|
||||||
|
const prevDate = new Date(estado.data);
|
||||||
|
prevDate.setDate(prevDate.getDate() - 1);
|
||||||
|
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
|
// Carrega lista de fechamentos recentes do Supabase com uuid
|
||||||
let recentList = [];
|
let recentList = [];
|
||||||
|
if (hasTauri) {
|
||||||
try {
|
try {
|
||||||
recentList = await window.__TAURI__.core.invoke('sb_listar_recentes', { loja, limite: 20 });
|
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);
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
console.warn('Tauri não disponível, pulando sb_listar_recentes');
|
||||||
|
}
|
||||||
|
|
||||||
// Extrai datas únicas ordenadas + mapeamento data→id
|
// Extrai datas únicas ordenadas + mapeamento data→id
|
||||||
const dateIdMap = {};
|
const dateIdMap = {};
|
||||||
@@ -1595,6 +1974,7 @@ document.getElementById('btn-anterior').addEventListener('click', async () => {
|
|||||||
});
|
});
|
||||||
dateList.sort().reverse();
|
dateList.sort().reverse();
|
||||||
}
|
}
|
||||||
|
console.log('[DEBUG] dateList:', dateList);
|
||||||
|
|
||||||
let selectedDate = null;
|
let selectedDate = null;
|
||||||
let selectedId = null;
|
let selectedId = null;
|
||||||
@@ -1612,11 +1992,10 @@ document.getElementById('btn-anterior').addEventListener('click', async () => {
|
|||||||
|
|
||||||
// Se não escolheu data, usa dia anterior automático (sem id — carrega por data)
|
// 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();
|
||||||
@@ -1625,25 +2004,31 @@ document.getElementById('btn-anterior').addEventListener('click', async () => {
|
|||||||
|
|
||||||
// Tenta Supabase primeiro (com id se disponível), senão local
|
// Tenta Supabase primeiro (com id se disponível), senão local
|
||||||
let rascunho = null;
|
let rascunho = null;
|
||||||
|
if (hasTauri) {
|
||||||
if (selectedId) {
|
if (selectedId) {
|
||||||
// Usa sb_buscar_por_id para buscar por id numérico do Supabase
|
|
||||||
try {
|
try {
|
||||||
rascunho = await window.__TAURI__.core.invoke('sb_buscar_por_id', { id: selectedId });
|
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); }
|
} catch(e) { console.warn('sb_buscar_por_id falhou:', e); }
|
||||||
}
|
}
|
||||||
if (!rascunho) {
|
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); }
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
console.warn('Tauri não disponível, não foi possível carregar rascunho');
|
||||||
|
}
|
||||||
|
|
||||||
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));
|
||||||
@@ -1668,26 +2053,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(); });
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user