Compare commits
26 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5e4340adca | |||
| c01133f39c | |||
| a6b295a6d0 | |||
| f850831e98 | |||
| e70f31e00f | |||
| 79e1dbd645 | |||
| cce8f3df60 | |||
| 318d44e934 | |||
| bfbcd89be9 | |||
| 1848c1aa35 | |||
| 9176b764fd | |||
| 5cab38a27c | |||
| 5b1f3a2691 | |||
| 7e57d30863 | |||
| 7a93a30c22 | |||
| 230304e878 | |||
| d185f82bd5 | |||
| 2253d45d00 | |||
| 60e15dbed3 | |||
| 53767df80c | |||
| 208eede526 | |||
| fa1e973f8c | |||
| d623d11e23 | |||
| f13b059ce7 | |||
| 8660ac4755 | |||
| 72c534a813 |
+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)
|
||||||
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"
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -105,6 +105,7 @@ fn main() {
|
|||||||
plugins::printer::caminho_impressora,
|
plugins::printer::caminho_impressora,
|
||||||
plugins::printer::imprimir_recibo,
|
plugins::printer::imprimir_recibo,
|
||||||
plugins::printer::teste_impressora,
|
plugins::printer::teste_impressora,
|
||||||
|
plugins::printer::ativar_modo_teste_impressao,
|
||||||
// App
|
// App
|
||||||
plugins::app::get_loja,
|
plugins::app::get_loja,
|
||||||
plugins::app::set_loja,
|
plugins::app::set_loja,
|
||||||
|
|||||||
@@ -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,18 +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,
|
||||||
|
#[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 {
|
||||||
@@ -57,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();
|
||||||
|
|
||||||
@@ -76,39 +155,186 @@ 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 {
|
||||||
out.extend(escpos::divider());
|
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());
|
||||||
|
}
|
||||||
|
|
||||||
// Débitos
|
// ── DESPESAS ──
|
||||||
out.extend(escpos::line_bold("DÉBITOS"));
|
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::divider());
|
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());
|
||||||
|
}
|
||||||
|
|
||||||
// Cartões
|
// ── PIX CNPJ ──
|
||||||
out.extend(escpos::line_bold("CARTÕES"));
|
if !self.pixcnpj_arr.is_empty() {
|
||||||
out.extend(escpos::kv("Crédito:", &self.fmt_money(self.credito)));
|
out.extend(escpos::line_bold("PIX CNPJ"));
|
||||||
out.extend(escpos::kv("Débito:", &self.fmt_money(self.debito)));
|
for linha in &self.pixcnpj_arr {
|
||||||
out.extend(escpos::kv("Alimentação:", &self.fmt_money(self.alimentacao)));
|
if linha.valor.unwrap_or(0.0) > 0.0 {
|
||||||
out.extend(escpos::kv("Voucher:", &self.fmt_money(self.voucher)));
|
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("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("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 ──
|
||||||
|
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("DIFERENÇA:"));
|
|
||||||
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());
|
||||||
@@ -126,82 +352,144 @@ impl ReciboData {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── USB Printer (stub for cross-compilation) ─────────────────────────────────
|
|
||||||
// Real implementation requires native Windows build with correct windows crate API.
|
|
||||||
|
|
||||||
#[cfg(windows)]
|
|
||||||
pub mod usb_windows {
|
|
||||||
pub fn find_printer_device() -> Option<String> {
|
|
||||||
// STUB: returns None during cross-compilation
|
|
||||||
// Native Windows build will enumerate USB ports
|
|
||||||
None
|
|
||||||
}
|
|
||||||
pub fn open_printer(_path: &str) -> Result<isize, String> {
|
|
||||||
Err("Cross-compilation stub: printing needs native Windows build".into())
|
|
||||||
}
|
|
||||||
pub fn write_to(_handle: isize, _data: &[u8]) -> Result<usize, String> {
|
|
||||||
Err("Cross-compilation stub".into())
|
|
||||||
}
|
|
||||||
pub fn close_handle(_h: isize) {}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(not(windows))]
|
|
||||||
pub mod usb_windows {
|
|
||||||
pub fn find_printer_device() -> Option<String> {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
pub fn open_printer(_path: &str) -> Result<isize, String> {
|
|
||||||
Err("Not supported on this platform".into())
|
|
||||||
}
|
|
||||||
pub fn write_to(_handle: isize, _data: &[u8]) -> Result<usize, String> {
|
|
||||||
Err("Not supported on this platform".into())
|
|
||||||
}
|
|
||||||
pub fn close_handle(_h: isize) {}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Printer Manager ─────────────────────────────────────────────────────────
|
// ── Printer Manager ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
pub struct PrinterManager {
|
pub struct PrinterManager {
|
||||||
device_path: Mutex<Option<String>>,
|
device_path: Mutex<Option<String>>,
|
||||||
|
/// Modo teste: grava bytes num arquivo ao invés de enviar pra USB
|
||||||
|
test_mode: Mutex<bool>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for PrinterManager {
|
impl Default for PrinterManager {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
device_path: Mutex::new(None),
|
device_path: Mutex::new(None),
|
||||||
|
test_mode: Mutex::new(false),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PrinterManager {
|
impl PrinterManager {
|
||||||
|
/// Detecta impressora USB conectada
|
||||||
pub fn detectar(&self) -> Result<String, String> {
|
pub fn detectar(&self) -> Result<String, String> {
|
||||||
usb_windows::find_printer_device()
|
#[cfg(windows)]
|
||||||
.ok_or_else(|| "Nenhuma impressora USB encontrada".into())
|
{
|
||||||
.map(|p| {
|
// Tenta encontrar porta USB virtual via WMI/registry
|
||||||
*self.device_path.lock().unwrap() = Some(p.clone());
|
// Common TM-T20 USB port names on Windows
|
||||||
p
|
let candidates = [
|
||||||
})
|
r"\\.\USB001", // TM-T20 USB Printing Support
|
||||||
|
r"\\.\USB002",
|
||||||
|
r"\\.\GPR1",
|
||||||
|
r"\\.\COM3",
|
||||||
|
r"\\.\COM4",
|
||||||
|
];
|
||||||
|
for port in &candidates {
|
||||||
|
if std::path::Path::new(port).exists() || self.test_device(port) {
|
||||||
|
*self.device_path.lock().unwrap() = Some(port.to_string());
|
||||||
|
return Ok(port.to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err("Nenhuma impressora USB encontrada. Verifique o cabo e se a impressora está ligada.".into())
|
||||||
|
}
|
||||||
|
#[cfg(not(windows))]
|
||||||
|
{
|
||||||
|
Err("Deteccao USB s-para apenas no Windows.".into())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(windows)]
|
||||||
|
fn test_device(&self, path: &str) -> bool {
|
||||||
|
use std::process::Command;
|
||||||
|
let out = Command::new("cmd")
|
||||||
|
.args(["/C", &format!("type {} < nul 2>&1", path)])
|
||||||
|
.output();
|
||||||
|
out.map(|o| o.status.success()).unwrap_or(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn configurar(&self, path: String) -> Result<(), String> {
|
pub fn configurar(&self, path: String) -> Result<(), String> {
|
||||||
if path.is_empty() {
|
if path.is_empty() {
|
||||||
return Err("Caminho vazio".into());
|
return Err("Caminho vazio".into());
|
||||||
}
|
}
|
||||||
*self.device_path.lock().unwrap() = Some(path);
|
*self.device_path.lock().unwrap() = Some(path.clone());
|
||||||
|
log::info!("Impressora configurada: {}", path);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Ativa modo teste: grava os bytes ESC/POS num arquivo em vez de imprimir
|
||||||
|
pub fn set_test_mode(&self, enabled: bool) {
|
||||||
|
*self.test_mode.lock().unwrap() = enabled;
|
||||||
|
log::info!("Printer test mode: {}", enabled);
|
||||||
|
}
|
||||||
|
|
||||||
pub fn imprimir_bytes(&self, data: &[u8]) -> Result<usize, String> {
|
pub fn imprimir_bytes(&self, data: &[u8]) -> Result<usize, String> {
|
||||||
|
// Modo teste: grava arquivo
|
||||||
|
if *self.test_mode.lock().unwrap() {
|
||||||
|
let test_file = std::env::temp_dir().join("fechamento_recibo_escpos.bin");
|
||||||
|
std::fs::write(&test_file, data)
|
||||||
|
.map_err(|e| format!("Falha ao gravar arquivo de teste: {}", e))?;
|
||||||
|
log::info!("Bytes ESC/POS gravados em: {:?}", test_file);
|
||||||
|
return Ok(data.len());
|
||||||
|
}
|
||||||
|
|
||||||
let path = self
|
let path = self
|
||||||
.device_path
|
.device_path
|
||||||
.lock()
|
.lock()
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.clone()
|
.clone()
|
||||||
.ok_or_else(|| "Impressora nao configurada".to_string())?;
|
.ok_or_else(|| "Impressora nao configurada. Use o botao de detectar ou configure manualmente.".to_string())?;
|
||||||
let handle = usb_windows::open_printer(&path)?;
|
|
||||||
let n = usb_windows::write_to(handle, data)?;
|
#[cfg(windows)]
|
||||||
usb_windows::close_handle(handle);
|
{
|
||||||
Ok(n)
|
self.windows_write(&path, data)
|
||||||
|
}
|
||||||
|
#[cfg(not(windows))]
|
||||||
|
{
|
||||||
|
let _ = data;
|
||||||
|
Err("Impressao USB disponivel apenas no Windows.".into())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(windows)]
|
||||||
|
fn windows_write(&self, path: &str, data: &[u8]) -> Result<usize, String> {
|
||||||
|
use std::io::Write;
|
||||||
|
|
||||||
|
// Tenta via File primeiro (mais simples)
|
||||||
|
{
|
||||||
|
let file = std::fs::OpenOptions::new()
|
||||||
|
.write(true)
|
||||||
|
.create(false)
|
||||||
|
.open(path);
|
||||||
|
if let Ok(mut f) = file {
|
||||||
|
let r = f.write_all(data);
|
||||||
|
let _ = f.flush();
|
||||||
|
if r.is_ok() {
|
||||||
|
return Ok(data.len());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback: usa API Windows direta via std::process::Command + > arquivo
|
||||||
|
// Isso绕过 problemas de locking do OpenOptions
|
||||||
|
let tmp = std::env::temp_dir().join("escpos_tmp.bin");
|
||||||
|
std::fs::write(&tmp, data)
|
||||||
|
.map_err(|e| format!("Falha ao criar arquivo temporario: {}", e))?;
|
||||||
|
|
||||||
|
let status = std::process::Command::new("cmd")
|
||||||
|
.args(["/C", &format!("copy /B {} {} /Y > nul 2>&1", tmp.display(), path)])
|
||||||
|
.status();
|
||||||
|
|
||||||
|
let _ = std::fs::remove_file(&tmp);
|
||||||
|
|
||||||
|
if status.map(|s| s.success()).unwrap_or(false) {
|
||||||
|
log::info!("Escritos {} bytes para {}", data.len(), path);
|
||||||
|
Ok(data.len())
|
||||||
|
} else {
|
||||||
|
Err(format!(
|
||||||
|
"Falha ao enviar dados para {}. Verifique se a impressora USB está \
|
||||||
|
conectada e ligada. Tente configurar manualmente em Config > Impressora.",
|
||||||
|
path
|
||||||
|
))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -209,6 +497,7 @@ impl PrinterManager {
|
|||||||
|
|
||||||
static PRINTER: PrinterManager = PrinterManager {
|
static PRINTER: PrinterManager = PrinterManager {
|
||||||
device_path: Mutex::new(None),
|
device_path: Mutex::new(None),
|
||||||
|
test_mode: Mutex::new(false),
|
||||||
};
|
};
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
@@ -231,22 +520,31 @@ pub fn imprimir_recibo(data: String) -> Result<usize, String> {
|
|||||||
let r: ReciboData =
|
let r: ReciboData =
|
||||||
serde_json::from_str(&data).map_err(|e| format!("Erro ao analisar dados: {}", e))?;
|
serde_json::from_str(&data).map_err(|e| format!("Erro ao analisar dados: {}", e))?;
|
||||||
let bytes = r.to_escpos();
|
let bytes = r.to_escpos();
|
||||||
|
log::info!("Imprimindo recibo: {} bytes ESC/POS", bytes.len());
|
||||||
PRINTER.imprimir_bytes(&bytes)
|
PRINTER.imprimir_bytes(&bytes)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub fn teste_impressora() -> Result<usize, String> {
|
pub fn teste_impressora() -> Result<usize, String> {
|
||||||
// Simple test pattern
|
|
||||||
let mut test = Vec::new();
|
let mut test = Vec::new();
|
||||||
test.extend(escpos::INIT);
|
test.extend(escpos::INIT);
|
||||||
test.extend(escpos::blank_lines(1));
|
test.extend(escpos::blank_lines(1));
|
||||||
test.extend(escpos::title("TESTE DE IMPRESSORA"));
|
test.extend(escpos::title("TESTE DE IMPRESSORA"));
|
||||||
test.extend(escpos::line_center("O Frangao"));
|
test.extend(escpos::line_center("O Frangao"));
|
||||||
|
test.extend(escpos::line_center(&chrono::Local::now().format("%d/%m/%Y %H:%M").to_string()));
|
||||||
test.extend(escpos::blank_lines(2));
|
test.extend(escpos::blank_lines(2));
|
||||||
test.extend(escpos::FEED_CUT);
|
test.extend(escpos::FEED_CUT);
|
||||||
test.extend(escpos::CUT);
|
test.extend(escpos::CUT);
|
||||||
|
log::info!("Teste impressora: {} bytes", test.len());
|
||||||
PRINTER.imprimir_bytes(&test)
|
PRINTER.imprimir_bytes(&test)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub fn ativar_modo_teste_impressao() {
|
||||||
|
PRINTER.set_test_mode(true);
|
||||||
|
}
|
||||||
|
|
||||||
/// Called by Tauri on plugin init (no-op for this plugin)
|
/// Called by Tauri on plugin init (no-op for this plugin)
|
||||||
pub fn init() {}
|
pub fn init() {
|
||||||
|
log::info!("Printer plugin ready. Use detectar_impressora() ou configure manualmente.");
|
||||||
|
}
|
||||||
|
|||||||
@@ -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])?;
|
||||||
@@ -383,39 +385,98 @@ pub fn salvar_fechamento(
|
|||||||
let saldo_troco = estado.get("saldo_troco").and_then(|v| v.as_f64()).unwrap_or(0.0);
|
let saldo_troco = estado.get("saldo_troco").and_then(|v| v.as_f64()).unwrap_or(0.0);
|
||||||
let fechamento = estado.get("fechamento").and_then(|v| v.as_f64()).unwrap_or(0.0);
|
let fechamento = estado.get("fechamento").and_then(|v| v.as_f64()).unwrap_or(0.0);
|
||||||
let saldo_esperado = saldo_troco + fechamento;
|
let saldo_esperado = saldo_troco + fechamento;
|
||||||
let diferenca = estado.get("diferenca").and_then(|v| v.as_f64()).unwrap_or(0.0);
|
// Constrói TableData a partir de todas as tabelas do frontend
|
||||||
|
let despesas: Vec<serde_json::Value> = estado
|
||||||
// Constrói TableData a partir de despesas/receber arrays
|
.get("despesas")
|
||||||
let despesas: Vec<serde_json::Value> = estado.get("despesas")
|
|
||||||
.and_then(|v| v.as_array())
|
.and_then(|v| v.as_array())
|
||||||
.map(|arr| arr.to_vec())
|
.map(|arr| arr.to_vec())
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
let receber: Vec<serde_json::Value> = estado.get("receber")
|
let sangrias: Vec<serde_json::Value> = estado
|
||||||
|
.get("sangrias")
|
||||||
.and_then(|v| v.as_array())
|
.and_then(|v| v.as_array())
|
||||||
.map(|arr| arr.to_vec())
|
.map(|arr| arr.to_vec())
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
|
let pixcnpj: Vec<serde_json::Value> = estado
|
||||||
|
.get("pixcnpj")
|
||||||
|
.and_then(|v| v.as_array())
|
||||||
|
.map(|arr| arr.to_vec())
|
||||||
|
.unwrap_or_default();
|
||||||
|
let vales: Vec<serde_json::Value> = estado
|
||||||
|
.get("vales")
|
||||||
|
.and_then(|v| v.as_array())
|
||||||
|
.map(|arr| arr.to_vec())
|
||||||
|
.unwrap_or_default();
|
||||||
|
let receber: Vec<serde_json::Value> = estado
|
||||||
|
.get("receber")
|
||||||
|
.and_then(|v| v.as_array())
|
||||||
|
.map(|arr| arr.to_vec())
|
||||||
|
.unwrap_or_default();
|
||||||
|
let cancelamentos: Vec<serde_json::Value> = estado
|
||||||
|
.get("cancelamentos")
|
||||||
|
.and_then(|v| v.as_array())
|
||||||
|
.map(|arr| arr.to_vec())
|
||||||
|
.unwrap_or_default();
|
||||||
|
let cartoes_json = estado.get("cartoes");
|
||||||
|
|
||||||
let to_table_row = |v: &serde_json::Value| -> TableRow {
|
let to_table_row = |v: &serde_json::Value, _tipo: &str| -> TableRow {
|
||||||
TableRow {
|
TableRow {
|
||||||
descricao: v.get("desc").and_then(|s| s.as_str()).map(String::from),
|
descricao: v.get("desc").or_else(|| v.get("nome")).and_then(|s| s.as_str()).map(String::from),
|
||||||
|
retirada: v.get("retirada").and_then(|n| n.as_f64()),
|
||||||
valor: v.get("valor").and_then(|n| n.as_f64()),
|
valor: v.get("valor").and_then(|n| n.as_f64()),
|
||||||
|
motivo: v.get("motivo").and_then(|s| s.as_str()).map(String::from),
|
||||||
|
cliente: v.get("nome").and_then(|s| s.as_str()).map(String::from),
|
||||||
|
// hora, gerente, obs, numero vão para extra via flatten
|
||||||
|
extra: std::collections::HashMap::new(),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let tabelas = TableData {
|
let tabelas = TableData {
|
||||||
despesas: despesas.iter().map(to_table_row).collect(),
|
despesas: despesas.iter().map(|v| to_table_row(v, "despesas")).collect(),
|
||||||
areceber: receber.iter().map(to_table_row).collect(),
|
sangrias: sangrias.iter().map(|v| to_table_row(v, "sangrias")).collect(),
|
||||||
..Default::default()
|
pixcnpj: pixcnpj.iter().map(|v| to_table_row(v, "pixcnpj")).collect(),
|
||||||
|
vales: vales.iter().map(|v| to_table_row(v, "vales")).collect(),
|
||||||
|
areceber: receber.iter().map(|v| to_table_row(v, "receber")).collect(),
|
||||||
|
cancelamentos: cancelamentos.iter().map(|v| to_table_row(v, "cancelamentos")).collect(),
|
||||||
};
|
};
|
||||||
|
|
||||||
// Cartoes como scalars
|
|
||||||
let cartoes_json = estado.get("cartoes");
|
|
||||||
let cartoes = CartaoData {
|
let cartoes = CartaoData {
|
||||||
credito: vec![TableRow { valor: cartoes_json.and_then(|c| c.get("credito")).and_then(|v| v.as_f64()), ..Default::default() }],
|
credito: cartoes_json
|
||||||
debito: vec![TableRow { valor: cartoes_json.and_then(|c| c.get("debito")).and_then(|v| v.as_f64()), ..Default::default() }],
|
.and_then(|c| c.get("credito"))
|
||||||
alimentacao: vec![TableRow { valor: cartoes_json.and_then(|c| c.get("alimentacao")).and_then(|v| v.as_f64()), ..Default::default() }],
|
.and_then(|v| v.as_array())
|
||||||
voucher: vec![TableRow { valor: cartoes_json.and_then(|c| c.get("voucher")).and_then(|v| v.as_f64()), ..Default::default() }],
|
.map(|arr| {
|
||||||
|
arr.iter()
|
||||||
|
.map(|v| TableRow { valor: v.as_f64(), ..Default::default() })
|
||||||
|
.collect()
|
||||||
|
})
|
||||||
|
.unwrap_or_default(),
|
||||||
|
debito: cartoes_json
|
||||||
|
.and_then(|c| c.get("debito"))
|
||||||
|
.and_then(|v| v.as_array())
|
||||||
|
.map(|arr| {
|
||||||
|
arr.iter()
|
||||||
|
.map(|v| TableRow { valor: v.as_f64(), ..Default::default() })
|
||||||
|
.collect()
|
||||||
|
})
|
||||||
|
.unwrap_or_default(),
|
||||||
|
alimentacao: cartoes_json
|
||||||
|
.and_then(|c| c.get("alimentacao"))
|
||||||
|
.and_then(|v| v.as_array())
|
||||||
|
.map(|arr| {
|
||||||
|
arr.iter()
|
||||||
|
.map(|v| TableRow { valor: v.as_f64(), ..Default::default() })
|
||||||
|
.collect()
|
||||||
|
})
|
||||||
|
.unwrap_or_default(),
|
||||||
|
voucher: cartoes_json
|
||||||
|
.and_then(|c| c.get("pix"))
|
||||||
|
.and_then(|v| v.as_array())
|
||||||
|
.map(|arr| {
|
||||||
|
arr.iter()
|
||||||
|
.map(|v| TableRow { valor: v.as_f64(), ..Default::default() })
|
||||||
|
.collect()
|
||||||
|
})
|
||||||
|
.unwrap_or_default(),
|
||||||
};
|
};
|
||||||
|
|
||||||
let estado_rust = FechamentoEstado {
|
let estado_rust = FechamentoEstado {
|
||||||
@@ -449,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/*": "./"
|
||||||
|
|||||||
+655
-102
@@ -32,6 +32,7 @@
|
|||||||
color: #fff; padding: 8px 18px;
|
color: #fff; padding: 8px 18px;
|
||||||
display: flex; align-items: center; justify-content: space-between;
|
display: flex; align-items: center; justify-content: space-between;
|
||||||
flex-shrink: 0; gap: 14px;
|
flex-shrink: 0; gap: 14px;
|
||||||
|
position: sticky; top: 0; z-index: 100;
|
||||||
}
|
}
|
||||||
.header-left { display: flex; align-items: center; gap: 14px; flex: 1; }
|
.header-left { display: flex; align-items: center; gap: 14px; flex: 1; }
|
||||||
.header-title { font-size: 15px; font-weight: 700; white-space: nowrap; }
|
.header-title { font-size: 15px; font-weight: 700; white-space: nowrap; }
|
||||||
@@ -383,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>
|
||||||
@@ -391,7 +392,8 @@
|
|||||||
<div class="sys-row"><span class="lbl">Alimentação (+)</span><input type="text" id="sys-alimentacao" readonly tabindex="-1" /></div>
|
<div class="sys-row"><span class="lbl">Alimentação (+)</span><input type="text" id="sys-alimentacao" readonly tabindex="-1" /></div>
|
||||||
<div class="sys-row"><span class="lbl">Vales (+)</span><input type="text" id="sys-vales" readonly tabindex="-1" /></div>
|
<div class="sys-row"><span class="lbl">Vales (+)</span><input type="text" id="sys-vales" readonly tabindex="-1" /></div>
|
||||||
<div class="sys-row"><span class="lbl">À Receber (+)</span><input type="text" id="sys-receber" readonly tabindex="-1" /></div>
|
<div class="sys-row"><span class="lbl">À Receber (+)</span><input type="text" id="sys-receber" readonly tabindex="-1" /></div>
|
||||||
<div class="sys-row"><span class="lbl">PIX (+)</span><input type="text" id="sys-pix" readonly tabindex="-1" /></div>
|
<div class="sys-row"><span class="lbl">PIX Cartão (+)</span><input type="text" id="sys-pix" readonly tabindex="-1" /></div>
|
||||||
|
<div class="sys-row"><span class="lbl">PIX CNPJ (+)</span><input type="text" id="sys-pixcnpj" readonly tabindex="-1" /></div>
|
||||||
<div class="sys-row total"><span class="lbl">TOTAL</span><span class="val" id="sys-total">R$ 0,00</span></div>
|
<div class="sys-row total"><span class="lbl">TOTAL</span><span class="val" id="sys-total">R$ 0,00</span></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -403,11 +405,17 @@
|
|||||||
<label>Informe o saldo esperado (contado em caixa)</label>
|
<label>Informe o saldo esperado (contado em caixa)</label>
|
||||||
<input type="text" id="f-saldo-esperado" placeholder="0,00" />
|
<input type="text" id="f-saldo-esperado" placeholder="0,00" />
|
||||||
</div>
|
</div>
|
||||||
|
<!-- DIFERENÇA — dentro do Saldo Esperado, logo abaixo -->
|
||||||
|
<div class="dif-box" style="margin-top:8px">
|
||||||
|
<span class="dif-label">Diferença</span>
|
||||||
|
<span class="dif-value zero" id="diferenca">R$ 0,00</span>
|
||||||
|
<span id="dif-badge" style="font-size:11px;font-weight:700;padding:2px 8px;border-radius:10px;background:#e8f5e9;color:#2e7d32;display:none">OK</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 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>
|
||||||
@@ -419,14 +427,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- DIFERENÇA -->
|
|
||||||
<div class="section">
|
|
||||||
<div class="dif-box">
|
|
||||||
<span class="dif-label">Diferença</span>
|
|
||||||
<span class="dif-value zero" id="diferenca">R$ 0,00</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- OBS -->
|
<!-- OBS -->
|
||||||
<div class="section" style="flex:1;display:flex;flex-direction:column">
|
<div class="section" style="flex:1;display:flex;flex-direction:column">
|
||||||
<div class="section-header">📝 Observações</div>
|
<div class="section-header">📝 Observações</div>
|
||||||
@@ -577,28 +577,32 @@ let estado = {
|
|||||||
// ─── Helpers ──────────────────────────────────────────────────────────────
|
// ─── Helpers ──────────────────────────────────────────────────────────────
|
||||||
const fmt = v => 'R$ ' + (parseFloat(v) || 0).toLocaleString('pt-BR', { minimumFractionDigits: 2 });
|
const fmt = v => 'R$ ' + (parseFloat(v) || 0).toLocaleString('pt-BR', { minimumFractionDigits: 2 });
|
||||||
const parseNum = v => {
|
const parseNum = v => {
|
||||||
const s = String(v || '0');
|
// Already a number? Return directly (don't treat '.' as thousand separator)
|
||||||
// Handle empty or whitespace
|
if (typeof v === 'number') return isNaN(v) ? 0 : v;
|
||||||
if (!s.trim()) return 0;
|
const s = String(v || '0').trim();
|
||||||
|
if (!s) return 0;
|
||||||
// Brazilian format: 1.234,56 → 1234.56
|
// Brazilian format: 1.234,56 → 1234.56
|
||||||
// Strategy: remove thousand dots, replace decimal comma with dot
|
// Strategy: if string has comma, treat as BRL (decimal comma)
|
||||||
// "10,55" → "1055,00" (wrong) → fix: split on comma first
|
// If no comma but has dot, treat as US (decimal dot)
|
||||||
// "1.055,00" → parts=["1.055","00"] → intPart="1055", decPart="00" → 1055.00
|
|
||||||
const lastComma = s.lastIndexOf(',');
|
const lastComma = s.lastIndexOf(',');
|
||||||
if (lastComma === -1) {
|
const lastDot = s.lastIndexOf('.');
|
||||||
// No comma: just remove thousand separators (dots)
|
const hasCommaDecimal = lastComma > lastDot;
|
||||||
return parseFloat(s.replace(/,/g, '').replace(/\./g, '')) || 0;
|
if (hasCommaDecimal) {
|
||||||
|
const intPart = s.slice(0, lastComma).replace(/\./g, '').replace(/,/g, '');
|
||||||
|
const decPart = s.slice(lastComma + 1);
|
||||||
|
return parseFloat(intPart + '.' + decPart) || 0;
|
||||||
}
|
}
|
||||||
const intPart = s.slice(0, lastComma).replace(/\./g, '').replace(/,/g, '');
|
// No comma: US-style number, just strip any dots (thousand sep)
|
||||||
const decPart = s.slice(lastComma + 1);
|
return parseFloat(s.replace(/,/g, '').replace(/\./g, '')) || 0;
|
||||||
return parseFloat(intPart + '.' + decPart) || 0;
|
|
||||||
};
|
};
|
||||||
const setText = (id, t) => { const e = document.getElementById(id); if (e) e.textContent = t; };
|
const setText = (id, t) => { const e = document.getElementById(id); if (e) e.textContent = t; };
|
||||||
const setVal = (id, v) => { const e = document.getElementById(id); if (e) e.value = v; };
|
const setVal = (id, v) => { const e = document.getElementById(id); if (e) e.value = v; };
|
||||||
const val = id => { const e = document.getElementById(id); return e ? e.value : ''; };
|
const val = id => { const e = document.getElementById(id); return e ? e.value : ''; };
|
||||||
const fmtNum = v => {
|
const fmtNum = v => {
|
||||||
const n = typeof v === 'number' ? v : parseNum(v);
|
const n = typeof v === 'number' ? v : parseNum(v);
|
||||||
return isNaN(n) ? '' : n.toLocaleString('pt-BR', { minimumFractionDigits: 2 });
|
if (isNaN(n)) return '';
|
||||||
|
// Always 2 decimal places for money
|
||||||
|
return n.toLocaleString('pt-BR', { minimumFractionDigits: 2, maximumFractionDigits: 2 });
|
||||||
};
|
};
|
||||||
|
|
||||||
// Format time: 4 digits → HH:MM
|
// Format time: 4 digits → HH:MM
|
||||||
@@ -637,17 +641,23 @@ function makeRow(tblId, rowIndex, rowData) {
|
|||||||
if (col === 'valor' || col === 'retirada') td.className = 'num';
|
if (col === 'valor' || col === 'retirada') td.className = 'num';
|
||||||
const inp = document.createElement('input');
|
const inp = document.createElement('input');
|
||||||
inp.type = 'text';
|
inp.type = 'text';
|
||||||
inp.value = rowData[col] !== undefined ? (rowData[col] === 0 ? '' : rowData[col]) : '';
|
// For numeric cols that hold money, format with 2 decimals
|
||||||
|
// For text cols (hora, desc, obs, nome, gerente, motivo, numero) keep as-is
|
||||||
|
const numericCols = ['valor', 'retirada'];
|
||||||
|
inp.value = rowData[col] !== undefined
|
||||||
|
? (numericCols.includes(col) ? (rowData[col] === 0 ? '' : fmtNum(rowData[col])) : (rowData[col] || ''))
|
||||||
|
: '';
|
||||||
inp.placeholder = col === 'valor' || col === 'retirada' ? '0,00' : (col === 'hora' ? 'HH:MM' : '');
|
inp.placeholder = col === 'valor' || col === 'retirada' ? '0,00' : (col === 'hora' ? 'HH:MM' : '');
|
||||||
inp.dataset.tbl = tblId;
|
inp.dataset.tbl = tblId;
|
||||||
inp.dataset.row = rowIndex;
|
inp.dataset.row = rowIndex;
|
||||||
inp.dataset.col = col;
|
inp.dataset.col = col;
|
||||||
|
// datalist only for fields that should offer suggestions from config
|
||||||
if (col === 'gerente') inp.setAttribute('list', 'dl-gerente');
|
if (col === 'gerente') inp.setAttribute('list', 'dl-gerente');
|
||||||
if (col === 'desc') inp.setAttribute('list', 'dl-despesa');
|
if (col === 'desc') inp.setAttribute('list', 'dl-despesa');
|
||||||
// tbl-receber: clientes free-text (sem datalist)
|
|
||||||
// tbl-vales: usa dl-vales
|
|
||||||
if (col === 'nome' && tblId === 'tbl-vales') inp.setAttribute('list', 'dl-vales');
|
if (col === 'nome' && tblId === 'tbl-vales') inp.setAttribute('list', 'dl-vales');
|
||||||
if (col === 'nome' && tblId === 'tbl-pix') inp.setAttribute('list', 'dl-generico');
|
// tbl-pix nome = free text (no datalist)
|
||||||
|
// tbl-receber nome = free text (no datalist)
|
||||||
|
// tbl-cancelamentos numero + motivo = free text (no datalist)
|
||||||
inp.addEventListener('input', onTableInput);
|
inp.addEventListener('input', onTableInput);
|
||||||
inp.addEventListener('blur', onCellBlur);
|
inp.addEventListener('blur', onCellBlur);
|
||||||
inp.addEventListener('keydown', onCellKeydown);
|
inp.addEventListener('keydown', onCellKeydown);
|
||||||
@@ -685,6 +695,7 @@ function onTableInput(e) {
|
|||||||
if (formatted !== e.target.value) e.target.value = formatted;
|
if (formatted !== e.target.value) e.target.value = formatted;
|
||||||
field[row][col] = formatted;
|
field[row][col] = formatted;
|
||||||
} else {
|
} else {
|
||||||
|
// text fields: nome, desc, obs, gerente, motivo, numero — keep as string
|
||||||
field[row][col] = e.target.value;
|
field[row][col] = e.target.value;
|
||||||
}
|
}
|
||||||
updateTotals();
|
updateTotals();
|
||||||
@@ -698,6 +709,7 @@ function onCellBlur(e) {
|
|||||||
e.target.value = isNaN(v) || v === 0 ? '' : fmtNum(v);
|
e.target.value = isNaN(v) || v === 0 ? '' : fmtNum(v);
|
||||||
updateTotals();
|
updateTotals();
|
||||||
}
|
}
|
||||||
|
// For text cols (hora, nome, desc, obs, gerente, motivo, numero) — keep as-is
|
||||||
}
|
}
|
||||||
|
|
||||||
function onCellKeydown(e) {
|
function onCellKeydown(e) {
|
||||||
@@ -731,8 +743,9 @@ function focusInput(tblId, rowIndex, col) {
|
|||||||
function addRowAndFocus(tblId, focusCol) {
|
function addRowAndFocus(tblId, focusCol) {
|
||||||
const cfg = TABLES[tblId];
|
const cfg = TABLES[tblId];
|
||||||
const field = getField(tblId);
|
const field = getField(tblId);
|
||||||
|
const numericCols = ['valor', 'retirada'];
|
||||||
const empty = {};
|
const empty = {};
|
||||||
cfg.cols.forEach(c => { empty[c] = c === 'valor' || c === 'retirada' ? 0 : ''; });
|
cfg.cols.forEach(c => { empty[c] = numericCols.includes(c) ? 0 : ''; });
|
||||||
field.push(empty);
|
field.push(empty);
|
||||||
renderTable(tblId);
|
renderTable(tblId);
|
||||||
focusInput(tblId, field.length - 1, focusCol);
|
focusInput(tblId, field.length - 1, focusCol);
|
||||||
@@ -773,7 +786,7 @@ document.querySelectorAll('.add-btn[data-tbl]').forEach(btn => {
|
|||||||
// ─── Totals ────────────────────────────────────────────────────────────────
|
// ─── Totals ────────────────────────────────────────────────────────────────
|
||||||
function updateTotals() {
|
function updateTotals() {
|
||||||
// ── Sangrias ──
|
// ── Sangrias ──
|
||||||
const sangriasTotal = estado.sangrias.reduce((s, r) => s + (parseFloat(r['retirada'] || 0) || 0), 0);
|
const sangriasTotal = estado.sangrias.reduce((s, r) => s + (parseNum(r['retirada'] || 0) || 0), 0);
|
||||||
setText('total-sangrias', fmt(sangriasTotal));
|
setText('total-sangrias', fmt(sangriasTotal));
|
||||||
|
|
||||||
// ── Tabelas de valor ──
|
// ── Tabelas de valor ──
|
||||||
@@ -782,7 +795,7 @@ function updateTotals() {
|
|||||||
'tbl-receber':'receber', 'tbl-cancelamentos':'cancelamentos',
|
'tbl-receber':'receber', 'tbl-cancelamentos':'cancelamentos',
|
||||||
};
|
};
|
||||||
Object.entries(map).forEach(([tblId, fieldName]) => {
|
Object.entries(map).forEach(([tblId, fieldName]) => {
|
||||||
const total = estado[fieldName].reduce((s, r) => s + (parseFloat(r['valor'] || 0) || 0), 0);
|
const total = estado[fieldName].reduce((s, r) => s + (parseNum(r['valor'] || 0) || 0), 0);
|
||||||
setText(tblId.replace('tbl-','total-'), fmt(total));
|
setText(tblId.replace('tbl-','total-'), fmt(total));
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -807,17 +820,19 @@ function updateTotals() {
|
|||||||
document.getElementById('sys-debito').value = cartDeb > 0 ? fmtNum(cartDeb) : '';
|
document.getElementById('sys-debito').value = cartDeb > 0 ? fmtNum(cartDeb) : '';
|
||||||
// Alimentação = total cartão alimentação
|
// Alimentação = total cartão alimentação
|
||||||
document.getElementById('sys-alimentacao').value = cartAli > 0 ? fmtNum(cartAli) : '';
|
document.getElementById('sys-alimentacao').value = cartAli > 0 ? fmtNum(cartAli) : '';
|
||||||
// PIX = total cartão PIX + total PIX CNPJ
|
// PIX Cartão = total cartão PIX
|
||||||
const pixCnpjTotal = estado.pixcnpj.reduce((s, r) => s + (parseFloat(r['valor'] || 0) || 0), 0);
|
document.getElementById('sys-pix').value = cartPix > 0 ? fmtNum(cartPix) : '';
|
||||||
document.getElementById('sys-pix').value = (cartPix + pixCnpjTotal) > 0 ? fmtNum(cartPix + pixCnpjTotal) : '';
|
// PIX CNPJ = total PIX CNPJ
|
||||||
|
const pixCnpjTotal = estado.pixcnpj.reduce((s, r) => s + (parseNum(r['valor'] || 0) || 0), 0);
|
||||||
|
document.getElementById('sys-pixcnpj').value = pixCnpjTotal > 0 ? fmtNum(pixCnpjTotal) : '';
|
||||||
// Vales = total vales
|
// Vales = total vales
|
||||||
const valesTotal = estado.vales.reduce((s, r) => s + (parseFloat(r['valor'] || 0) || 0), 0);
|
const valesTotal = estado.vales.reduce((s, r) => s + (parseNum(r['valor'] || 0) || 0), 0);
|
||||||
document.getElementById('sys-vales').value = valesTotal > 0 ? fmtNum(valesTotal) : '';
|
document.getElementById('sys-vales').value = valesTotal > 0 ? fmtNum(valesTotal) : '';
|
||||||
// À Receber = total receber
|
// À Receber = total receber
|
||||||
const receberTotal = estado.receber.reduce((s, r) => s + (parseFloat(r['valor'] || 0) || 0), 0);
|
const receberTotal = estado.receber.reduce((s, r) => s + (parseNum(r['valor'] || 0) || 0), 0);
|
||||||
document.getElementById('sys-receber').value = receberTotal > 0 ? fmtNum(receberTotal) : '';
|
document.getElementById('sys-receber').value = receberTotal > 0 ? fmtNum(receberTotal) : '';
|
||||||
|
|
||||||
// SYS total
|
// SYS total = troco + crédito + débito + alimentação + vales + receber + pixCartão + pixCnpj
|
||||||
const sysTotal = parseNum(val('sys-troco')) + cartCred + cartDeb + cartAli + valesTotal + receberTotal + cartPix + pixCnpjTotal;
|
const sysTotal = parseNum(val('sys-troco')) + cartCred + cartDeb + cartAli + valesTotal + receberTotal + cartPix + pixCnpjTotal;
|
||||||
setText('sys-total', fmt(sysTotal));
|
setText('sys-total', fmt(sysTotal));
|
||||||
|
|
||||||
@@ -835,19 +850,37 @@ function updateTotals() {
|
|||||||
const fr = receberTotal;
|
const fr = receberTotal;
|
||||||
setText('fech-total', fmt(fd + fc + fr));
|
setText('fech-total', fmt(fd + fc + fr));
|
||||||
|
|
||||||
// Diferença = Saldo Esperado − Fechamento
|
// Diferença = Saldo Caixa − Saldo Esperado
|
||||||
const diff = parseNum(val('f-saldo-esperado')) - (fd + fc + fr);
|
const saldoCaixa = sysTotal;
|
||||||
|
const diff = saldoCaixa - parseNum(val('f-saldo-esperado'));
|
||||||
const difEl = document.getElementById('diferenca');
|
const difEl = document.getElementById('diferenca');
|
||||||
|
const badgeEl = document.getElementById('dif-badge');
|
||||||
if (difEl) {
|
if (difEl) {
|
||||||
difEl.textContent = fmt(diff);
|
difEl.textContent = fmt(diff);
|
||||||
if (diff === 0) {
|
if (diff === 0) {
|
||||||
difEl.className = 'dif-value zero';
|
difEl.className = 'dif-value zero';
|
||||||
|
if (badgeEl) { badgeEl.textContent = '✓ OK'; badgeEl.style.background='#e8f5e9'; badgeEl.style.color='#2e7d32'; badgeEl.style.display='inline'; }
|
||||||
} else if (diff < 0 && diff >= -5) {
|
} else if (diff < 0 && diff >= -5) {
|
||||||
difEl.className = 'dif-value neg'; // OK − até −5
|
difEl.className = 'dif-value neg'; // OK − até −5 de tolerância
|
||||||
|
if (badgeEl) { badgeEl.textContent = '✓ OK'; badgeEl.style.background='#e8f5e9'; badgeEl.style.color='#2e7d32'; badgeEl.style.display='inline'; }
|
||||||
} else if (diff > 0 && diff <= 3) {
|
} else if (diff > 0 && diff <= 3) {
|
||||||
difEl.className = 'dif-value zero'; // OK + até 3
|
difEl.className = 'dif-value zero'; // OK + até 3
|
||||||
|
if (badgeEl) { badgeEl.textContent = '✓ OK'; badgeEl.style.background='#e8f5e9'; badgeEl.style.color='#2e7d32'; badgeEl.style.display='inline'; }
|
||||||
} else {
|
} else {
|
||||||
difEl.className = 'dif-value pos'; // Sobra > +3
|
// Sobra mais de R$3 ou falta mais de R$5
|
||||||
|
difEl.className = diff > 0 ? 'dif-value pos' : 'dif-value neg';
|
||||||
|
if (badgeEl) {
|
||||||
|
if (diff > 0) {
|
||||||
|
badgeEl.textContent = '⚠ SOBRANDO';
|
||||||
|
badgeEl.style.background='#fff3e0';
|
||||||
|
badgeEl.style.color='#e65100';
|
||||||
|
} else {
|
||||||
|
badgeEl.textContent = '⚠ FALTANDO';
|
||||||
|
badgeEl.style.background='#ffebee';
|
||||||
|
badgeEl.style.color='#c0272d';
|
||||||
|
}
|
||||||
|
badgeEl.style.display='inline';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -989,7 +1022,9 @@ function init() {
|
|||||||
const today = new Date().toISOString().split('T')[0];
|
const today = new Date().toISOString().split('T')[0];
|
||||||
setVal('f-data', today);
|
setVal('f-data', today);
|
||||||
estado.data = today;
|
estado.data = today;
|
||||||
setVal('f-loja', estado.loja);
|
setVal('f-loja', estado.loja || 'União');
|
||||||
|
setVal('f-operador', estado.operador || '');
|
||||||
|
setVal('f-turno', estado.turno || '');
|
||||||
syncHeader();
|
syncHeader();
|
||||||
PRE_FILL_TABLES.forEach(ensureOneRow);
|
PRE_FILL_TABLES.forEach(ensureOneRow);
|
||||||
Object.keys(TABLES).forEach(renderTable);
|
Object.keys(TABLES).forEach(renderTable);
|
||||||
@@ -1012,7 +1047,6 @@ function buildEstadoForBackend() {
|
|||||||
saldo_troco: parseNum(val('sys-troco')), // troco do sistema
|
saldo_troco: parseNum(val('sys-troco')), // troco do sistema
|
||||||
saldo_esperado: parseNum(val('f-saldo-esperado')),
|
saldo_esperado: parseNum(val('f-saldo-esperado')),
|
||||||
fechamento: parseNum(val('f-fech-dinheiro')) + parseNum(val('f-fech-cartoes')) + parseNum(val('f-fech-receber')),
|
fechamento: parseNum(val('f-fech-dinheiro')) + parseNum(val('f-fech-cartoes')) + parseNum(val('f-fech-receber')),
|
||||||
diferenca: parseNum(val('f-saldo-esperado')) - (parseNum(val('f-fech-dinheiro')) + parseNum(val('f-fech-cartoes')) + parseNum(val('f-fech-receber'))),
|
|
||||||
despesas: estado.despesas.map(r => ({ desc: r.desc || '', obs: r.obs || '', valor: r.valor || 0 })),
|
despesas: estado.despesas.map(r => ({ desc: r.desc || '', obs: r.obs || '', valor: r.valor || 0 })),
|
||||||
sangrias: estado.sangrias.map(r => ({ hora: r.hora || '', retirada: r.retirada || 0, gerente: r.gerente || '' })),
|
sangrias: estado.sangrias.map(r => ({ hora: r.hora || '', retirada: r.retirada || 0, gerente: r.gerente || '' })),
|
||||||
pixcnpj: estado.pixcnpj.map(r => ({ nome: r.nome || '', valor: r.valor || 0 })),
|
pixcnpj: estado.pixcnpj.map(r => ({ nome: r.nome || '', valor: r.valor || 0 })),
|
||||||
@@ -1050,8 +1084,44 @@ function loadState() {
|
|||||||
const raw = localStorage.getItem(LS_KEY);
|
const raw = localStorage.getItem(LS_KEY);
|
||||||
if (!raw) return false;
|
if (!raw) return false;
|
||||||
const saved = JSON.parse(raw);
|
const saved = JSON.parse(raw);
|
||||||
// Merge carefully (cartoes may be in old format)
|
// Restore simple fields
|
||||||
Object.assign(estado, saved);
|
estado.uuid = saved.uuid || estado.uuid;
|
||||||
|
estado.id_local = saved.id_local !== undefined ? saved.id_local : null;
|
||||||
|
estado.loja = saved.loja || estado.loja;
|
||||||
|
estado.data = saved.data || estado.data;
|
||||||
|
estado.operador = saved.operador || estado.operador;
|
||||||
|
estado.turno = saved.turno || estado.turno;
|
||||||
|
estado.saldo_troco = saved.saldo_troco || 0;
|
||||||
|
estado.saldo_esperado = saved.saldo_esperado || 0;
|
||||||
|
estado.fechamento = saved.fechamento || 0;
|
||||||
|
estado.observacoes = saved.observacoes || '';
|
||||||
|
estado.sync_status = saved.sync_status || 'local';
|
||||||
|
// Restore tables (always as arrays of objects)
|
||||||
|
estado.despesas = Array.isArray(saved.despesas) ? saved.despesas : [];
|
||||||
|
estado.sangrias = Array.isArray(saved.sangrias) ? saved.sangrias : [];
|
||||||
|
estado.pixcnpj = Array.isArray(saved.pixcnpj) ? saved.pixcnpj : [];
|
||||||
|
estado.vales = Array.isArray(saved.vales) ? saved.vales : [];
|
||||||
|
estado.receber = Array.isArray(saved.receber) ? saved.receber : [];
|
||||||
|
estado.cancelamentos = Array.isArray(saved.cancelamentos) ? saved.cancelamentos : [];
|
||||||
|
// Restore cartoes — each entry must be { valor: number }
|
||||||
|
if (saved.cartoes) {
|
||||||
|
estado.cartoes.credito = Array.isArray(saved.cartoes.credito)
|
||||||
|
? saved.cartoes.credito.map(v => ({ valor: typeof v === 'object' && v !== null ? (v.valor || 0) : (parseNum(v) || 0) }))
|
||||||
|
: [];
|
||||||
|
estado.cartoes.debito = Array.isArray(saved.cartoes.debito)
|
||||||
|
? saved.cartoes.debito.map(v => ({ valor: typeof v === 'object' && v !== null ? (v.valor || 0) : (parseNum(v) || 0) }))
|
||||||
|
: [];
|
||||||
|
estado.cartoes.alimentacao = Array.isArray(saved.cartoes.alimentacao)
|
||||||
|
? saved.cartoes.alimentacao.map(v => ({ valor: typeof v === 'object' && v !== null ? (v.valor || 0) : (parseNum(v) || 0) }))
|
||||||
|
: [];
|
||||||
|
estado.cartoes.pix = Array.isArray(saved.cartoes.pix)
|
||||||
|
? saved.cartoes.pix.map(v => ({ valor: typeof v === 'object' && v !== null ? (v.valor || 0) : (parseNum(v) || 0) }))
|
||||||
|
: [];
|
||||||
|
}
|
||||||
|
// Restore fechamento_contado
|
||||||
|
if (saved.fechamento_contado) {
|
||||||
|
estado.fechamento_contado = saved.fechamento_contado;
|
||||||
|
}
|
||||||
setVal('f-data', estado.data || '');
|
setVal('f-data', estado.data || '');
|
||||||
setVal('f-operador', estado.operador || '');
|
setVal('f-operador', estado.operador || '');
|
||||||
setVal('f-turno', estado.turno || '');
|
setVal('f-turno', estado.turno || '');
|
||||||
@@ -1059,9 +1129,10 @@ function loadState() {
|
|||||||
setVal('f-obs', estado.observacoes || '');
|
setVal('f-obs', estado.observacoes || '');
|
||||||
setVal('f-saldo-esperado', estado.saldo_esperado === 0 ? '' : fmtNum(estado.saldo_esperado));
|
setVal('f-saldo-esperado', estado.saldo_esperado === 0 ? '' : fmtNum(estado.saldo_esperado));
|
||||||
setVal('sys-troco', estado.saldo_troco === 0 ? '' : fmtNum(estado.saldo_troco));
|
setVal('sys-troco', estado.saldo_troco === 0 ? '' : fmtNum(estado.saldo_troco));
|
||||||
setVal('f-fech-dinheiro', estado.fechamento_contado && estado.fechamento_contado.dinheiro ? fmtNum(estado.fechamento_contado.dinheiro) : '');
|
const fc = estado.fechamento_contado || {};
|
||||||
setVal('f-fech-cartoes', estado.fechamento_contado && estado.fechamento_contado.cartoes ? fmtNum(estado.fechamento_contado.cartoes) : '');
|
setVal('f-fech-dinheiro', fc.dinheiro ? fmtNum(fc.dinheiro) : '');
|
||||||
setVal('f-fech-receber', estado.fechamento_contado && estado.fechamento_contado.receber ? fmtNum(estado.fechamento_contado.receber) : '');
|
setVal('f-fech-cartoes', fc.cartoes ? fmtNum(fc.cartoes) : '');
|
||||||
|
setVal('f-fech-receber', fc.receber ? fmtNum(fc.receber) : '');
|
||||||
syncHeader();
|
syncHeader();
|
||||||
PRE_FILL_TABLES.forEach(ensureOneRow);
|
PRE_FILL_TABLES.forEach(ensureOneRow);
|
||||||
Object.keys(TABLES).forEach(renderTable);
|
Object.keys(TABLES).forEach(renderTable);
|
||||||
@@ -1142,14 +1213,322 @@ async function tauriEnviar() {
|
|||||||
const data = buildEstadoForBackend();
|
const data = buildEstadoForBackend();
|
||||||
// 1. Salva local
|
// 1. Salva local
|
||||||
await window.__TAURI__.core.invoke('salvar_fechamento', { estado: data }).catch(e => console.warn('local save err:', e));
|
await window.__TAURI__.core.invoke('salvar_fechamento', { estado: data }).catch(e => console.warn('local save err:', e));
|
||||||
// 2. Envia Supabase
|
// 2. Envia Supabase — debug completo
|
||||||
const result = await window.__TAURI__.core.invoke('sb_salvar_fechamento', { estado: data });
|
console.log('=== Enviando para Supabase ===');
|
||||||
return result;
|
console.log('URL:', 'https://supabase.ofrangao.com.br/rest/v1/fechamentos');
|
||||||
|
console.log('Payload:', JSON.stringify(data, null, 2));
|
||||||
|
try {
|
||||||
|
const result = await window.__TAURI__.core.invoke('sb_salvar_fechamento', { estado: data });
|
||||||
|
console.log('Supabase resposta:', JSON.stringify(result));
|
||||||
|
return result;
|
||||||
|
} catch(e) {
|
||||||
|
console.error('Erro sb_salvar_fechamento:', JSON.stringify(e));
|
||||||
|
// Tenta debug: qual era o payload exato?
|
||||||
|
console.error('Payload que falhou:', JSON.stringify(data));
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function buildConfirmHTML(data) {
|
||||||
|
const fmt = v => 'R$ ' + (v || 0).toFixed(2).replace('.', ',');
|
||||||
|
const sangrias = (data.sangrias_arr || data.sangrias || []).reduce((s,r) => s+(parseNum(r.retirada||r.valor||0)||0), 0);
|
||||||
|
const despesas = (data.despesas_arr || data.despesas || []).reduce((s,r) => s+(parseNum(r.valor||0)||0), 0);
|
||||||
|
const vales = (data.vales_arr || data.vales || []).reduce((s,r) => s+(parseNum(r.valor||0)||0), 0);
|
||||||
|
const receber = (data.receber_arr || data.receber || []).reduce((s,r) => s+(parseNum(r.valor||0)||0), 0);
|
||||||
|
const pixcnpj = (data.pixcnpj_arr || data.pixcnpj || []).reduce((s,r) => s+(parseNum(r.valor||0)||0), 0);
|
||||||
|
const cancelamentos = (data.cancelamentos_arr || data.cancelamentos || []).reduce((s,c) => s+(parseNum(c.valor||0)||0), 0);
|
||||||
|
const cartCred = data.credito || 0;
|
||||||
|
const cartDeb = data.debito || 0;
|
||||||
|
const cartAli = data.alimentacao || 0;
|
||||||
|
const cartPix = data.voucher || data.pix || 0;
|
||||||
|
const fechamento = data.fechamento || 0;
|
||||||
|
const saldoEsp = data.saldo_esperado || 0;
|
||||||
|
const diferenca = fechamento - saldoEsp;
|
||||||
|
const diffClass = diferenca === 0 ? '#2e7d32' : (diferenca > 0 ? '#e65100' : '#c0272d');
|
||||||
|
|
||||||
|
return `<!DOCTYPE html>
|
||||||
|
<html><head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>Relatório de Fechamento</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 #b3d9f7; margin: 6px 0; }
|
||||||
|
.row { display: flex; justify-content: space-between; padding: 2px 0; }
|
||||||
|
.obs { font-size: 11px; color: #555; margin-top: 6px; white-space: pre-wrap; }
|
||||||
|
.footer { text-align: center; font-size: 11px; color: #555; margin-top: 8px; }
|
||||||
|
@media print { body { margin: 0; } }
|
||||||
|
</style>
|
||||||
|
</head><body>
|
||||||
|
<div class="header">
|
||||||
|
<h1>${data.loja}</h1>
|
||||||
|
<h2>Fechamento de Caixa</h2>
|
||||||
|
</div>
|
||||||
|
<div style="margin-bottom:6px"><strong>📅 Data:</strong> ${data.data} <strong>👤 Operador:</strong> ${data.operador} <strong>⏰ Turno:</strong> ${data.turno}</div>
|
||||||
|
<hr>
|
||||||
|
<div>💸 Sangrias: <strong>${fmt(sangrias)}</strong></div>
|
||||||
|
<div>📝 Despesas: <strong>${fmt(despesas)}</strong></div>
|
||||||
|
<div>💰 Vales: <strong>${fmt(vales)}</strong></div>
|
||||||
|
<div>👥 À Receber: <strong>${fmt(receber)}</strong></div>
|
||||||
|
<div>🔵 PIX CNPJ: <strong>${fmt(pixcnpj)}</strong></div>
|
||||||
|
<div>💳 Crédito: <strong>${fmt(cartCred)}</strong></div>
|
||||||
|
<div>💳 Débito: <strong>${fmt(cartDeb)}</strong></div>
|
||||||
|
<div>💳 Alimentação: <strong>${fmt(cartAli)}</strong></div>
|
||||||
|
<div>❌ Cancelamentos: <strong>${fmt(cancelamentos)}</strong></div>
|
||||||
|
<div>📋 Fechamento: <strong>${fmt(fechamento)}</strong></div>
|
||||||
|
<hr>
|
||||||
|
<div>🎯 Saldo Esperado: <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;font-size:10px;color:#888">ID: ${data.loja}_${data.data}_${data.operador}</div>
|
||||||
|
<div class="footer">Gerado em ${new Date().toLocaleString('pt-BR')}</div>
|
||||||
|
</body></html>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Print Preview Window ──────────────────────────────────────────────────
|
||||||
|
function buildPrintHTML(data) {
|
||||||
|
const fmt = v => 'R$ ' + (v || 0).toFixed(2).replace('.', ',');
|
||||||
|
// Usa a diferenca ja calculada pelo tauriRecibo (sys_total - saldo_esperado),
|
||||||
|
// igual ao modal de confirmacao — nao recalcula de fechamento.
|
||||||
|
const diferenca = data.diferenca !== undefined ? data.diferenca
|
||||||
|
: ((data.sys_total || 0) - (data.saldo_esperado || 0));
|
||||||
|
const diffStr = diferenca >= 0 ? `+${fmt(diferenca)}` : fmt(diferenca);
|
||||||
|
const diffColor = diferenca === 0 ? '#2e7d32' : (diferenca > 0 ? '#e65100' : '#c0272d');
|
||||||
|
|
||||||
|
let rows = '';
|
||||||
|
// Cancelamentos (sum the array first — single cancelamentos is a float total, array is from confirm-print)
|
||||||
|
const cancelTotal = Array.isArray(data.cancelamentos)
|
||||||
|
? data.cancelamentos.reduce((s, c) => s + (parseNum(c.valor || 0) || 0), 0)
|
||||||
|
: (data.cancelamentos || 0);
|
||||||
|
// PIX total (flat number from tauriRecibo; pixcnpj is also flat total)
|
||||||
|
const pixTotal = data.pix || data.voucher || 0;
|
||||||
|
const pixCnpjTotal = typeof data.pixcnpj === 'number' ? data.pixcnpj : 0;
|
||||||
|
// Sangrias
|
||||||
|
if (data.sangrias && data.sangrias.length > 0) {
|
||||||
|
data.sangrias.forEach(s => { if (s.hora || s.retirada) rows += `<tr><td>${s.hora||''}</td><td>${fmt(s.retirada)}</td><td>${s.gerente||''}</td></tr>`; });
|
||||||
|
}
|
||||||
|
// Despesas
|
||||||
|
if (data.despesas && data.despesas.length > 0) {
|
||||||
|
data.despesas.forEach(d => { if (d.desc || d.valor) rows += `<tr><td>${d.desc||''}</td><td>${fmt(d.valor)}</td><td>${d.obs||''}</td></tr>`; });
|
||||||
|
}
|
||||||
|
// Vales
|
||||||
|
if (data.vales && data.vales.length > 0) {
|
||||||
|
data.vales.forEach(v => { if (v.nome || v.valor) rows += `<tr><td>${v.nome||''}</td><td>${fmt(v.valor)}</td><td>${v.obs||''}</td></tr>`; });
|
||||||
|
}
|
||||||
|
// Receber
|
||||||
|
if (data.receber && data.receber.length > 0) {
|
||||||
|
data.receber.forEach(r => { if (r.nome || r.valor) rows += `<tr><td>${r.nome||''}</td><td>${fmt(r.valor)}</td><td></td></tr>`; });
|
||||||
|
}
|
||||||
|
// PIX CNPJ (flat total, not array)
|
||||||
|
if (pixCnpjTotal > 0) {
|
||||||
|
rows += `<tr><td>PIX CNPJ</td><td>${fmt(pixCnpjTotal)}</td><td></td></tr>`;
|
||||||
|
}
|
||||||
|
// Cancelamentos
|
||||||
|
if (cancelTotal > 0) {
|
||||||
|
rows += `<tr><td>Cancelamentos</td><td>${fmt(cancelTotal)}</td><td></td></tr>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return `<!DOCTYPE html>
|
||||||
|
<html><head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>Relatório de Fechamento</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: 6px 0; }
|
||||||
|
.section { margin: 6px 0; }
|
||||||
|
.section-title { font-weight: bold; font-size: 14px; border-top: 1px dashed #000; padding-top: 4px; margin-top: 6px; }
|
||||||
|
.row { display: flex; justify-content: space-between; padding: 2px 0; }
|
||||||
|
.row.space-between { justify-content: space-between; }
|
||||||
|
.row.below { margin-top: 4px; }
|
||||||
|
table { width: 100%; border-collapse: collapse; font-size: 12px; }
|
||||||
|
td { padding: 2px 4px; vertical-align: top; }
|
||||||
|
td:nth-child(2) { text-align: right; }
|
||||||
|
td:nth-child(3) { text-align: right; color: #555; font-size: 11px; }
|
||||||
|
.total-row { font-weight: bold; border-top: 1px dashed #000; }
|
||||||
|
.grand-total { font-size: 16px; font-weight: bold; text-align: center; padding: 6px; border: 2px solid #000; margin: 8px 0; }
|
||||||
|
.obs { font-size: 11px; color: #555; margin-top: 6px; white-space: pre-wrap; }
|
||||||
|
.footer { text-align: center; font-size: 11px; color: #555; margin-top: 8px; }
|
||||||
|
@media print { body { margin: 0; } }
|
||||||
|
</style>
|
||||||
|
</head><body>
|
||||||
|
<div class="header">
|
||||||
|
<h1>${data.loja}</h1>
|
||||||
|
<h2>Fechamento de Caixa</h2>
|
||||||
|
</div>
|
||||||
|
<hr>
|
||||||
|
<div class="row"><span>Data:</span><span>${data.data}</span></div>
|
||||||
|
<div class="row"><span>Operador:</span><span>${data.operador}</span></div>
|
||||||
|
<div class="row"><span>Turno:</span><span>${data.turno}</span></div>
|
||||||
|
|
||||||
|
<div class="section-title">ENTRADAS</div>
|
||||||
|
<div class="row"><span>Saldo Troco:</span><span>${fmt(data.saldo_troco)}</span></div>
|
||||||
|
<div class="row"><span>Fechamento:</span><span>${fmt(data.fechamento)}</span></div>
|
||||||
|
<div class="row"><span>Saldo Esperado:</span><span>${fmt(data.saldo_esperado)}</span></div>
|
||||||
|
|
||||||
|
${rows ? `<div class="section-title">DETALHES</div><table><tbody>${rows}</tbody></table>` : ''}
|
||||||
|
|
||||||
|
${data.observacoes ? `<div class="obs">Obs: ${data.observacoes}</div>` : ''}
|
||||||
|
|
||||||
|
<hr>
|
||||||
|
<div class="section-title">CARTÕES</div>
|
||||||
|
<div class="row"><span>Crédito:</span><span>${fmt(data.credito||0)}</span></div>
|
||||||
|
<div class="row"><span>Débito:</span><span>${fmt(data.debito||0)}</span></div>
|
||||||
|
<div class="row"><span>Alimentação:</span><span>${fmt(data.alimentacao||0)}</span></div>
|
||||||
|
<div class="row"><span>PIX Cartão:</span><span>${fmt(pixTotal)}</span></div>
|
||||||
|
<div class="row"><span>Cancelamentos:</span><span>${fmt(cancelTotal)}</span></div>
|
||||||
|
|
||||||
|
<hr>
|
||||||
|
<div class="grand-total">DIFERENÇA: ${diffStr}</div>
|
||||||
|
<div class="footer">Gerado em ${new Date().toLocaleString('pt-BR')}</div>
|
||||||
|
</body></html>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function openPrintPreview(data, mode) {
|
||||||
|
const html = mode === 'confirm' ? buildConfirmHTML(data) : buildPrintHTML(data);
|
||||||
|
// Remove preview anterior se existir
|
||||||
|
const existing = document.getElementById('print-preview-overlay');
|
||||||
|
if (existing) document.body.removeChild(existing);
|
||||||
|
|
||||||
|
const overlay = document.createElement('div');
|
||||||
|
overlay.id = 'print-preview-overlay';
|
||||||
|
overlay.style.cssText = `
|
||||||
|
position: fixed; top: 0; left: 0; right: 0; bottom: 0;
|
||||||
|
background: rgba(0,0,0,0.85); z-index: 99999;
|
||||||
|
display: flex; flex-direction: column; align-items: center;
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const previewStyle = `
|
||||||
|
<style>
|
||||||
|
body { width: 80mm; margin: 0 auto; font-family: 'Courier New', monospace; font-size: 13px; background: #fff; padding: 10px; }
|
||||||
|
.header { text-align: center; margin-bottom: 8px; }
|
||||||
|
.header h1 { font-size: 18px; }
|
||||||
|
.header h2 { font-size: 14px; font-weight: normal; }
|
||||||
|
hr { border: none; border-top: 1px dashed #000; margin: 6px 0; }
|
||||||
|
.section-title { font-weight: bold; font-size: 14px; border-top: 1px dashed #000; padding-top: 4px; margin-top: 6px; }
|
||||||
|
.row { display: flex; justify-content: space-between; padding: 2px 0; }
|
||||||
|
table { width: 100%; border-collapse: collapse; font-size: 12px; }
|
||||||
|
td { padding: 2px 4px; vertical-align: top; }
|
||||||
|
td:nth-child(2) { text-align: right; }
|
||||||
|
td:nth-child(3) { text-align: right; color: #555; font-size: 11px; }
|
||||||
|
.grand-total { font-size: 16px; font-weight: bold; text-align: center; padding: 6px; border: 2px solid #000; margin: 8px 0; }
|
||||||
|
.obs { font-size: 11px; color: #555; margin-top: 6px; white-space: pre-wrap; }
|
||||||
|
.footer { text-align: center; font-size: 11px; color: #555; margin-top: 8px; }
|
||||||
|
@media print { body { background: #fff; } }
|
||||||
|
</style>
|
||||||
|
`;
|
||||||
|
|
||||||
|
const fullHtml = `<!DOCTYPE html><html><head><meta charset="UTF-8"><title>Recibo</title>${previewStyle}</head><body>${html}</body></html>`;
|
||||||
|
|
||||||
|
const iframe = document.createElement('iframe');
|
||||||
|
iframe.style.cssText = 'width:360px; height:600px; border:2px solid #555; margin-top:16px; background:#fff;';
|
||||||
|
iframe.id = 'print-iframe';
|
||||||
|
|
||||||
|
const btnFechar = document.createElement('button');
|
||||||
|
btnFechar.textContent = '✕ Fechar';
|
||||||
|
btnFechar.style.cssText = `
|
||||||
|
margin-top: 12px; padding: 10px 32px;
|
||||||
|
background: #444; color: #fff; border: none; border-radius: 6px;
|
||||||
|
font-size: 15px; cursor: pointer;
|
||||||
|
`;
|
||||||
|
btnFechar.onclick = () => {
|
||||||
|
document.body.removeChild(overlay);
|
||||||
|
};
|
||||||
|
|
||||||
|
const btnImprimir = document.createElement('button');
|
||||||
|
btnImprimir.textContent = '🖨️ Imprimir';
|
||||||
|
btnImprimir.style.cssText = `
|
||||||
|
margin-top: 8px; padding: 10px 32px;
|
||||||
|
background: #e65100; color: #fff; border: none; border-radius: 6px;
|
||||||
|
font-size: 15px; cursor: pointer;
|
||||||
|
`;
|
||||||
|
btnImprimir.onclick = () => {
|
||||||
|
iframe.contentWindow.focus();
|
||||||
|
iframe.contentWindow.print();
|
||||||
|
};
|
||||||
|
|
||||||
|
overlay.appendChild(iframe);
|
||||||
|
overlay.appendChild(btnImprimir);
|
||||||
|
overlay.appendChild(btnFechar);
|
||||||
|
document.body.appendChild(overlay);
|
||||||
|
|
||||||
|
const iframeDoc = iframe.contentWindow.document;
|
||||||
|
iframeDoc.open();
|
||||||
|
iframeDoc.write(fullHtml);
|
||||||
|
iframeDoc.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
function triggerWindowPrint() {
|
||||||
|
// Chama window.print() na janela pai (o próprio app)
|
||||||
|
window.print();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Recibo 80mm via Tauri (ESC/POS USB) ─────────────────────────────────
|
||||||
async function tauriRecibo() {
|
async function tauriRecibo() {
|
||||||
const data = buildEstadoForBackend();
|
// Build flat totals for the Rust ReciboData struct
|
||||||
return await window.__TAURI__.core.invoke('imprimir_recibo', { estado: JSON.stringify(data) });
|
const cartCred = estado.cartoes.credito.reduce((s,r) => s+(r.valor||0), 0);
|
||||||
|
const cartDeb = estado.cartoes.debito.reduce((s,r) => s+(r.valor||0), 0);
|
||||||
|
const cartAli = estado.cartoes.alimentacao.reduce((s,r) => s+(r.valor||0), 0);
|
||||||
|
const cartPix = estado.cartoes.pix.reduce((s,r) => s+(r.valor||0), 0);
|
||||||
|
const pixCnpjTotal = estado.pixcnpj.reduce((s,r) => s+(parseNum(r.valor||0)||0), 0);
|
||||||
|
const sangriasTotal = estado.sangrias.reduce((s,r) => s+(parseNum(r.retirada||0)||0), 0);
|
||||||
|
const despesasTotal = estado.despesas.reduce((s,r) => s+(parseNum(r.valor||0)||0), 0);
|
||||||
|
const valesTotal = estado.vales.reduce((s,r) => s+(parseNum(r.valor||0)||0), 0);
|
||||||
|
const receberTotal = estado.receber.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 saldoEsp = parseNum(val('f-saldo-esperado'));
|
||||||
|
// SYS total = troco + crédito + débito + alimentação + vales + receber + pixCartão + pixCnpj
|
||||||
|
const sysTotal = parseNum(val('sys-troco')) + cartCred + cartDeb + cartAli + valesTotal + receberTotal + cartPix + pixCnpjTotal;
|
||||||
|
const data = {
|
||||||
|
uuid: estado.uuid,
|
||||||
|
id_local: estado.id_local,
|
||||||
|
loja: estado.loja,
|
||||||
|
data: estado.data,
|
||||||
|
operador: estado.operador,
|
||||||
|
turno: estado.turno,
|
||||||
|
saldo_troco: parseNum(val('sys-troco')),
|
||||||
|
saldo_esperado: saldoEsp,
|
||||||
|
fechamento: fechamentoCounted,
|
||||||
|
diferenca: sysTotal - saldoEsp,
|
||||||
|
sys_total: sysTotal,
|
||||||
|
// Flat totals for ReciboData struct (Rust) — must be numbers
|
||||||
|
credito: cartCred,
|
||||||
|
debito: cartDeb,
|
||||||
|
alimentacao: cartAli,
|
||||||
|
voucher: cartPix,
|
||||||
|
pixcnpj: pixCnpjTotal,
|
||||||
|
sangrias: sangriasTotal,
|
||||||
|
despesas: despesasTotal,
|
||||||
|
vales: valesTotal,
|
||||||
|
areceber: receberTotal,
|
||||||
|
cancelamentos: cancelTotal,
|
||||||
|
// Full arrays for backend/storage
|
||||||
|
despesas_arr: estado.despesas.map(r => ({ desc: r.desc||'', obs: r.obs||'', valor: r.valor||0 })),
|
||||||
|
sangrias_arr: estado.sangrias.map(r => ({ hora: r.hora||'', retirada: parseNum(r.retirada)||0, gerente: r.gerente||'' })),
|
||||||
|
pixcnpj_arr: estado.pixcnpj.map(r => ({ nome: r.nome||'', valor: r.valor||0 })),
|
||||||
|
vales_arr: estado.vales.map(r => ({ nome: r.nome||'', obs: r.obs||'', valor: r.valor||0 })),
|
||||||
|
receber_arr: estado.receber.map(r => ({ nome: r.nome||'', valor: r.valor||0 })),
|
||||||
|
cancelamentos_arr: estado.cancelamentos.map(r => ({ numero: r.numero||'', valor: r.valor||0, motivo: r.motivo||'' })),
|
||||||
|
cartoes: { credito: cartCred, debito: cartDeb, alimentacao: cartAli, pix: cartPix },
|
||||||
|
observacoes: estado.observacoes,
|
||||||
|
sync_status: estado.sync_status,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Tenta ESC/POS via Tauri primeiro; se falhar, abre preview
|
||||||
|
try {
|
||||||
|
return await window.__TAURI__.core.invoke('imprimir_recibo', { data: JSON.stringify(data) });
|
||||||
|
} catch(e) {
|
||||||
|
console.warn('ESC/POS falhou, abrindo preview:', e);
|
||||||
|
openPrintPreview(data);
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function tauriCarregarRascunho() {
|
async function tauriCarregarRascunho() {
|
||||||
@@ -1163,25 +1542,33 @@ 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);
|
||||||
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);
|
||||||
const cartPix = estado.cartoes.pix.reduce((s,r) => s+(r.valor||0), 0);
|
const cartPix = estado.cartoes.pix.reduce((s,r) => s+(r.valor||0), 0);
|
||||||
const sangrias = estado.sangrias.reduce((s,r) => s+(parseFloat(r.retirada||0)||0), 0);
|
const sangrias = estado.sangrias.reduce((s,r) => s+(parseNum(r.retirada||0)||0), 0);
|
||||||
const despesas = estado.despesas.reduce((s,r) => s+(r.valor||0), 0);
|
const despesas = estado.despesas.reduce((s,r) => s+(parseNum(r.valor||0)||0), 0);
|
||||||
const vales = estado.vales.reduce((s,r) => s+(r.valor||0), 0);
|
const vales = estado.vales.reduce((s,r) => s+(parseNum(r.valor||0)||0), 0);
|
||||||
const receber = estado.receber.reduce((s,r) => s+(r.valor||0), 0);
|
const receber = estado.receber.reduce((s,r) => s+(parseNum(r.valor||0)||0), 0);
|
||||||
const cancelamentos = estado.cancelamentos.reduce((s,r) => s+(r.valor||0), 0);
|
const cancelamentos = estado.cancelamentos.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 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');
|
||||||
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>
|
||||||
<hr style="border:none;border-top:1px dashed #b3d9f7;margin:6px 0">
|
<hr style="border:none;border-top:1px dashed #b3d9f7;margin:6px 0">
|
||||||
@@ -1189,14 +1576,18 @@ function openConfirmModal() {
|
|||||||
<div>📝 Despesas: <strong>${fmt(despesas)}</strong></div>
|
<div>📝 Despesas: <strong>${fmt(despesas)}</strong></div>
|
||||||
<div>💰 Vales: <strong>${fmt(vales)}</strong></div>
|
<div>💰 Vales: <strong>${fmt(vales)}</strong></div>
|
||||||
<div>👥 À Receber: <strong>${fmt(receber)}</strong></div>
|
<div>👥 À Receber: <strong>${fmt(receber)}</strong></div>
|
||||||
<div>💳 Crédito: <strong style="color:#c0272d">${fmt(cartCred)}</strong></div>
|
<div>🔵 PIX CNPJ: <strong>${fmt(pixcnpj)}</strong></div>
|
||||||
|
<div>💳 Crédito: <strong>${fmt(cartCred)}</strong></div>
|
||||||
<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;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');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1208,19 +1599,32 @@ 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
|
// 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 () => {
|
||||||
closeConfirmModal();
|
closeConfirmModal();
|
||||||
const btn = document.getElementById('btn-recibo');
|
const cartCred = estado.cartoes.credito.reduce((s,r) => s+(r.valor||0), 0);
|
||||||
btn.disabled = true;
|
const cartDeb = estado.cartoes.debito.reduce((s,r) => s+(r.valor||0), 0);
|
||||||
try {
|
const cartAli = estado.cartoes.alimentacao.reduce((s,r) => s+(r.valor||0), 0);
|
||||||
await tauriRecibo();
|
const cartPix = estado.cartoes.pix.reduce((s,r) => s+(r.valor||0), 0);
|
||||||
alert('✅ Impressão enviada!');
|
const fechamentoCounted = parseNum(val('f-fech-dinheiro')) + parseNum(val('f-fech-cartoes')) + parseNum(val('f-fech-receber'));
|
||||||
} catch(e) {
|
const saldoEsp = parseNum(val('f-saldo-esperado'));
|
||||||
alert('Erro ao imprimir: ' + (e.message || e));
|
const data = {
|
||||||
} finally {
|
loja: estado.loja, data: estado.data, operador: estado.operador, turno: estado.turno,
|
||||||
btn.disabled = false;
|
saldo_troco: parseNum(val('sys-troco')) || 0,
|
||||||
}
|
saldo_esperado: saldoEsp,
|
||||||
|
fechamento: fechamentoCounted,
|
||||||
|
diferenca: fechamentoCounted - saldoEsp,
|
||||||
|
despesas_arr: estado.despesas.map(r => ({ desc: r.desc||'', obs: r.obs||'', valor: r.valor||0 })),
|
||||||
|
sangrias_arr: estado.sangrias.map(r => ({ hora: r.hora||'', retirada: parseNum(r.retirada)||0, gerente: r.gerente||'' })),
|
||||||
|
pixcnpj_arr: estado.pixcnpj.map(r => ({ nome: r.nome||'', valor: r.valor||0 })),
|
||||||
|
vales_arr: estado.vales.map(r => ({ nome: r.nome||'', obs: r.obs||'', valor: r.valor||0 })),
|
||||||
|
receber_arr: estado.receber.map(r => ({ nome: r.nome||'', valor: r.valor||0 })),
|
||||||
|
cancelamentos_arr: estado.cancelamentos.map(r => ({ numero: r.numero||'', valor: r.valor||0, motivo: r.motivo||'' })),
|
||||||
|
credito: cartCred, debito: cartDeb, alimentacao: cartAli,
|
||||||
|
voucher: cartPix,
|
||||||
|
observacoes: estado.observacoes,
|
||||||
|
};
|
||||||
|
openPrintPreview(data, 'confirm');
|
||||||
});
|
});
|
||||||
|
|
||||||
// Confirm send
|
// Confirm send
|
||||||
@@ -1233,10 +1637,17 @@ document.getElementById('confirm-send').addEventListener('click', async () => {
|
|||||||
const r = await tauriEnviar();
|
const r = await tauriEnviar();
|
||||||
estado.sync_status = 'synced';
|
estado.sync_status = 'synced';
|
||||||
saveState();
|
saveState();
|
||||||
alert('✅ Enviado com sucesso!');
|
alert('✅ Enviado com sucesso!\n\n📋 Tabela: fechamentos_web\n🌐 Servidor: supabase.ofrangao.com.br\n\nAbra o Supabase Studio para verificar.');
|
||||||
} catch(e) {
|
} catch(e) {
|
||||||
await tauriSaveLocal();
|
await tauriSaveLocal();
|
||||||
alert('Erro ao enviar: ' + (e.message || e) + '\n(Dados salvos localmente)');
|
const msg = e?.message || e?.toString?.() || String(e);
|
||||||
|
alert(
|
||||||
|
'⚠️ Erro ao enviar para o Supabase.\n\n' +
|
||||||
|
'📋 Tabela: fechamentos_web\n' +
|
||||||
|
'🌐 Servidor: supabase.ofrangao.com.br\n\n' +
|
||||||
|
'O servidor pode estar offline. Dados salvos localmente.\n\n' +
|
||||||
|
'Erro: ' + msg
|
||||||
|
);
|
||||||
} finally {
|
} finally {
|
||||||
btn.disabled = false;
|
btn.disabled = false;
|
||||||
btn.textContent = '💾 Enviar Fechamento';
|
btn.textContent = '💾 Enviar Fechamento';
|
||||||
@@ -1245,6 +1656,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();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -1252,10 +1668,13 @@ 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
|
||||||
await tauriRecibo();
|
await tauriRecibo();
|
||||||
alert('✅ Impressão enviada!');
|
// Se chegou aqui sem erro, ESC/POS funcionou
|
||||||
} catch(e) {
|
} catch(e) {
|
||||||
alert('Erro ao imprimir: ' + (e.message || e));
|
// ESC/POS falhou — openPrintPreview já foi chamado dentro de tauriRecibo
|
||||||
|
// Só mostra alerta se nem o preview abriu
|
||||||
|
console.warn('Recibo:', e);
|
||||||
} finally {
|
} finally {
|
||||||
btn.disabled = false;
|
btn.disabled = false;
|
||||||
}
|
}
|
||||||
@@ -1264,36 +1683,170 @@ 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');
|
||||||
const d = new Date(estado.data); d.setDate(d.getDate() - 1);
|
if (btnAnterior) {
|
||||||
const prevDate = d.toISOString().split('T')[0];
|
btnAnterior.addEventListener('click', async () => {
|
||||||
await tauriSaveLocal();
|
console.log('[DEBUG] btn-anterior clicado');
|
||||||
setVal('f-data', prevDate);
|
const loja = val('f-loja') || 'União';
|
||||||
estado.data = prevDate;
|
console.log('[DEBUG] btn-anterior loja:', loja);
|
||||||
estado.uuid = crypto.randomUUID ? crypto.randomUUID() : Date.now().toString();
|
|
||||||
estado.id_local = null;
|
// Salva rascunho atual antes de tudo
|
||||||
syncHeader();
|
await tauriSaveLocal();
|
||||||
const rascunho = await tauriCarregarRascunho();
|
|
||||||
if (rascunho) {
|
// Calcula dia anterior
|
||||||
if (rascunho.operador) setVal('f-operador', rascunho.operador);
|
const prevDate = new Date(estado.data);
|
||||||
if (rascunho.turno) setVal('f-turno', rascunho.turno);
|
prevDate.setDate(prevDate.getDate() - 1);
|
||||||
estado.operador = rascunho.operador || '';
|
const prevDateStr = prevDate.toISOString().split('T')[0];
|
||||||
estado.turno = rascunho.turno || '';
|
|
||||||
if (rascunho.saldo_troco) setVal('sys-troco', fmtNum(rascunho.saldo_troco));
|
// Confirmação explícita antes de trocar de dia
|
||||||
if (rascunho.saldo_esperado) setVal('f-saldo-esperado', fmtNum(rascunho.saldo_esperado));
|
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 (rascunho.despesas && Array.isArray(rascunho.despesas)) {
|
if (!confirm(confirmMsg)) {
|
||||||
estado.despesas = rascunho.despesas.map(d => ({ desc: d.desc || d.descricao || '', obs: '', valor: d.valor || 0 }));
|
console.log('[DEBUG] btn-anterior cancelado pelo usuário');
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
PRE_FILL_TABLES.forEach(ensureOneRow);
|
|
||||||
Object.keys(TABLES).forEach(renderTable);
|
|
||||||
updateTotals();
|
|
||||||
scheduleSave();
|
|
||||||
});
|
|
||||||
|
|
||||||
|
// Carrega lista de fechamentos recentes do Supabase com uuid
|
||||||
|
let recentList = [];
|
||||||
|
try {
|
||||||
|
recentList = await window.__TAURI__.core.invoke('sb_listar_recentes', { loja, limite: 20 });
|
||||||
|
console.log('[DEBUG] sb_listar_recentes retornou:', recentList?.length, 'itens');
|
||||||
|
} catch(e) {
|
||||||
|
console.warn('sb_listar_recentes falhou:', e);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extrai datas únicas ordenadas + mapeamento data→id
|
||||||
|
const dateIdMap = {};
|
||||||
|
const dateList = [];
|
||||||
|
if (recentList && recentList.length > 0) {
|
||||||
|
recentList.forEach(item => {
|
||||||
|
if (item.data && !dateIdMap[item.data]) {
|
||||||
|
dateIdMap[item.data] = item.id;
|
||||||
|
dateList.push(item.data);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
dateList.sort().reverse();
|
||||||
|
}
|
||||||
|
console.log('[DEBUG] dateList:', dateList);
|
||||||
|
|
||||||
|
let selectedDate = null;
|
||||||
|
let selectedId = null;
|
||||||
|
if (dateList.length > 0) {
|
||||||
|
const msg = dateList.map((d, i) => `${i + 1}) ${d}`).join('\n');
|
||||||
|
const escolha = prompt(`Selecione o número do dia anterior:\n${msg}\n\n(Deixe em branco e clique Cancelar para ver só o dia anterior automático)`);
|
||||||
|
if (escolha !== null && escolha.trim() !== '') {
|
||||||
|
const idx = parseInt(escolha) - 1;
|
||||||
|
if (idx >= 0 && idx < dateList.length) {
|
||||||
|
selectedDate = dateList[idx];
|
||||||
|
selectedId = dateIdMap[selectedDate];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Se não escolheu data, usa dia anterior automático (sem id — carrega por data)
|
||||||
|
if (!selectedDate) {
|
||||||
|
selectedDate = prevDateStr;
|
||||||
|
}
|
||||||
|
console.log('[DEBUG] selectedDate:', selectedDate, 'selectedId:', selectedId);
|
||||||
|
|
||||||
|
setVal('f-data', selectedDate);
|
||||||
|
estado.data = selectedDate;
|
||||||
|
estado.uuid = crypto.randomUUID ? crypto.randomUUID() : Date.now().toString();
|
||||||
|
estado.id_local = null;
|
||||||
|
syncHeader();
|
||||||
|
|
||||||
|
// Tenta Supabase primeiro (com id se disponível), senão local
|
||||||
|
let rascunho = null;
|
||||||
|
if (selectedId) {
|
||||||
|
try {
|
||||||
|
rascunho = await window.__TAURI__.core.invoke('sb_buscar_por_id', { id: selectedId });
|
||||||
|
console.log('[DEBUG] sb_buscar_por_id retornou:', !!rascunho);
|
||||||
|
} catch(e) { console.warn('sb_buscar_por_id falhou:', e); }
|
||||||
|
}
|
||||||
|
if (!rascunho) {
|
||||||
|
try {
|
||||||
|
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); }
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!rascunho) {
|
||||||
|
try {
|
||||||
|
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); }
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rascunho) {
|
||||||
|
console.log('[DEBUG] rascunho carregado com sucesso');
|
||||||
|
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.saldo_troco) setVal('sys-troco', fmtNum(rascunho.saldo_troco));
|
||||||
|
if (rascunho.saldo_esperado) setVal('f-saldo-esperado', fmtNum(rascunho.saldo_esperado));
|
||||||
|
if (rascunho.observacoes) { setVal('f-obs', rascunho.observacoes); estado.observacoes = rascunho.observacoes; }
|
||||||
|
if (rascunho.despesas && Array.isArray(rascunho.despesas)) {
|
||||||
|
estado.despesas = rascunho.despesas.map(d => ({ desc: d.desc || d.descricao || '', obs: d.obs || '', valor: parseNum(d.valor) || 0 }));
|
||||||
|
}
|
||||||
|
if (rascunho.sangrias && Array.isArray(rascunho.sangrias)) {
|
||||||
|
estado.sangrias = rascunho.sangrias.map(s => ({ hora: s.hora || '', retirada: parseNum(s.retirada) || 0, gerente: s.gerente || '' }));
|
||||||
|
}
|
||||||
|
if (rascunho.pixcnpj && Array.isArray(rascunho.pixcnpj)) {
|
||||||
|
estado.pixcnpj = rascunho.pixcnpj.map(p => ({ nome: p.nome || '', valor: parseNum(p.valor) || 0 }));
|
||||||
|
}
|
||||||
|
if (rascunho.vales && Array.isArray(rascunho.vales)) {
|
||||||
|
estado.vales = rascunho.vales.map(v => ({ nome: v.nome || '', obs: v.obs || '', valor: parseNum(v.valor) || 0 }));
|
||||||
|
}
|
||||||
|
if (rascunho.receber && Array.isArray(rascunho.receber)) {
|
||||||
|
estado.receber = rascunho.receber.map(r => ({ nome: r.nome || '', valor: parseNum(r.valor) || 0 }));
|
||||||
|
}
|
||||||
|
if (rascunho.cancelamentos && Array.isArray(rascunho.cancelamentos)) {
|
||||||
|
estado.cancelamentos = rascunho.cancelamentos.map(c => ({ numero: c.numero || '', valor: parseNum(c.valor) || 0, motivo: c.motivo || '' }));
|
||||||
|
}
|
||||||
|
if (rascunho.cartoes) {
|
||||||
|
const norm = (arr) => Array.isArray(arr) ? arr.map(v => ({ valor: (typeof v === 'object' && v !== null) ? (parseNum(v.valor) || 0) : (parseNum(v) || 0) })) : [];
|
||||||
|
estado.cartoes.credito = norm(rascunho.cartoes.credito);
|
||||||
|
estado.cartoes.debito = norm(rascunho.cartoes.debito);
|
||||||
|
estado.cartoes.alimentacao = norm(rascunho.cartoes.alimentacao);
|
||||||
|
estado.cartoes.pix = norm(rascunho.cartoes.pix);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.log('[DEBUG] nenhum rascunho encontrado para esta data');
|
||||||
|
estado.despesas = []; estado.sangrias = []; estado.pixcnpj = [];
|
||||||
|
estado.vales = []; estado.receber = []; estado.cancelamentos = [];
|
||||||
|
estado.cartoes = { credito: [], debito: [], alimentacao: [], pix: [] };
|
||||||
|
setVal('sys-troco', ''); setVal('f-saldo-esperado', ''); setVal('f-obs', '');
|
||||||
|
}
|
||||||
|
|
||||||
|
estado.sync_status = rascunho ? 'loaded' : 'local';
|
||||||
|
|
||||||
|
PRE_FILL_TABLES.forEach(ensureOneRow);
|
||||||
|
Object.keys(TABLES).forEach(renderTable);
|
||||||
|
updateTotals();
|
||||||
|
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