Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9176b764fd | |||
| 5cab38a27c | |||
| 5b1f3a2691 |
+13
@@ -7,3 +7,16 @@ src-tauri/gen/
|
|||||||
*.msi
|
*.msi
|
||||||
*.dmg
|
*.dmg
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
|
||||||
|
# Binarios compilados
|
||||||
|
*.exe
|
||||||
|
*.tar.gz
|
||||||
|
*.zip
|
||||||
|
fechamento-caixa.exe
|
||||||
|
fechamento-caixa-latest.tar.gz
|
||||||
|
WebView2Loader.dll
|
||||||
|
src-tauri/target/x86_64-pc-windows-gnu/release/*.exe
|
||||||
|
src-tauri/target/x86_64-pc-windows-gnu/release/*.dll
|
||||||
|
|
||||||
|
# Logs
|
||||||
|
*.log
|
||||||
|
|||||||
@@ -0,0 +1,140 @@
|
|||||||
|
# Fechamento de Caixa — O Frangão
|
||||||
|
|
||||||
|
App desktop nativo para fechamento de caixa com impressão em impressora térmica 80mm.
|
||||||
|
|
||||||
|
## Stack
|
||||||
|
|
||||||
|
- **Backend:** Tauri 2 + Rust
|
||||||
|
- **Frontend:** HTML + CSS + JavaScript (vanilla, ~1700 linhas)
|
||||||
|
- **Storage local:** SQLite (rusqlite) — funciona offline
|
||||||
|
- **Sync:** Supabase REST API
|
||||||
|
- **Impressão:** ESC/POS via USB (TM-T20 e compatíveis, Windows)
|
||||||
|
|
||||||
|
## Estrutura do Projeto
|
||||||
|
|
||||||
|
```
|
||||||
|
fechamento-caixa/
|
||||||
|
├── src/
|
||||||
|
│ └── index.html # Frontend completo (HTML + CSS + JS inline)
|
||||||
|
├── src-tauri/
|
||||||
|
│ ├── Cargo.toml # Dependências Rust
|
||||||
|
│ ├── tauri.conf.json # Config do app Tauri
|
||||||
|
│ └── src/
|
||||||
|
│ ├── main.rs # Entry point — registra comandos Tauri
|
||||||
|
│ └── plugins/
|
||||||
|
│ ├── supabase.rs # Integração Supabase (REST API)
|
||||||
|
│ ├── printer.rs # Geração ESC/POS + envio USB
|
||||||
|
│ ├── storage.rs # SQLite local
|
||||||
|
│ ├── app.rs # Config da app (loja, operador default)
|
||||||
|
│ └── escpos.rs # Helpers ESC/POS (INIT, CUT, divider, etc.)
|
||||||
|
└── README.md
|
||||||
|
```
|
||||||
|
|
||||||
|
## Pré-requisitos
|
||||||
|
|
||||||
|
- Rust 1.70+ (`rustup install stable`)
|
||||||
|
- Node.js 18+ (para build do frontend Tauri)
|
||||||
|
- Windows 10/11 (impressão USB só funciona no Windows)
|
||||||
|
|
||||||
|
## Setup
|
||||||
|
|
||||||
|
### 1. Clonar o repositório
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone https://git.ofrangao.com.br/filipe/fechamento-caixa.git
|
||||||
|
cd fechamento-caixa
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Configurar chaves do Supabase
|
||||||
|
|
||||||
|
Crie o arquivo `config.json` em `%LOCALAPPDATA%\FechamentoCaixa\config.json` (Windows):
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"SUPABASE_ANON_KEY": "sua_chave_anon_aqui",
|
||||||
|
"SUPABASE_SERVICE_KEY": "sua_chave_service_role_aqui"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
O app procura esse arquivo na inicialização. Sem ele, tenta ler das variáveis de ambiente `SUPABASE_ANON_KEY` e `SUPABASE_SERVICE_KEY`.
|
||||||
|
|
||||||
|
### 3. Build
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd src-tauri
|
||||||
|
cargo build --release --target x86_64-pc-windows-gnu
|
||||||
|
```
|
||||||
|
|
||||||
|
O executável fica em:
|
||||||
|
```
|
||||||
|
src-tauri/target/x86_64-pc-windows-gnu/release/fechamento-caixa.exe
|
||||||
|
```
|
||||||
|
|
||||||
|
Para gerar o pacote de distribuição:
|
||||||
|
```bash
|
||||||
|
cp src-tauri/target/x86_64-pc-windows-gnu/release/fechamento-caixa.exe .
|
||||||
|
cp src-tauri/target/x86_64-pc-windows-gnu/release/WebView2Loader.dll .
|
||||||
|
tar -czf fechamento-caixa-vX.X.X.tar.gz fechamento-caixa.exe WebView2Loader.dll
|
||||||
|
```
|
||||||
|
|
||||||
|
## Códigos de comando Tauri
|
||||||
|
|
||||||
|
O frontend chama esses comandos via `window.__TAURI__.core.invoke()`:
|
||||||
|
|
||||||
|
| Comando | Arquivo | Descrição |
|
||||||
|
|---------|---------|-----------|
|
||||||
|
| `salvar_fechamento` | storage.rs | Salva no SQLite local |
|
||||||
|
| `carregar_rascunho` | storage.rs | Busca rascunho por loja+data |
|
||||||
|
| `listar_fechamentos` | storage.rs | Lista histórico do SQLite |
|
||||||
|
| `buscar_fechamento_por_id` | storage.rs | Busca por ID no SQLite |
|
||||||
|
| `sb_salvar_fechamento` | supabase.rs | Envia para Supabase |
|
||||||
|
| `sb_carregar_rascunho` | supabase.rs | Busca rascunho no Supabase |
|
||||||
|
| `sb_listar_recentes` | supabase.rs | Lista fechamentos recentes |
|
||||||
|
| `sb_buscar_por_id` | supabase.rs | Busca por ID no Supabase |
|
||||||
|
| `sb_salvar_listas` | supabase.rs | Salva operadores/gerentes |
|
||||||
|
| `sb_carregar_listas` | supabase.rs | Carrega listas do servidor |
|
||||||
|
| `sb_online` | supabase.rs | Verifica conectividade |
|
||||||
|
| `imprimir_recibo` | printer.rs | Envia bytes ESC/POS pra USB |
|
||||||
|
| `detectar_impressora` | printer.rs | Detecta porta USB |
|
||||||
|
| `configurar_impressora` | printer.rs | Define porta manual |
|
||||||
|
| `teste_impressora` | printer.rs | Imprime página de teste |
|
||||||
|
| `get_loja` / `set_loja` | app.rs | Loja atual |
|
||||||
|
| `get_config` / `set_config` | app.rs | Config completo |
|
||||||
|
|
||||||
|
## Fluxo de dados
|
||||||
|
|
||||||
|
```
|
||||||
|
[Usuário preenche formulário]
|
||||||
|
│
|
||||||
|
▼ (auto-save a cada 1.5s)
|
||||||
|
SQLite local (fechamento.db)
|
||||||
|
│
|
||||||
|
├── [Botão Enviar] ──► Supabase (fechamentos_web)
|
||||||
|
│
|
||||||
|
└── [Botão Recibo] ──► printer.rs ──► to_escpos() ──► USB
|
||||||
|
│
|
||||||
|
├── Sucesso: imprime
|
||||||
|
└── Falha: openPrintPreview() ──► iframe ──► window.print()
|
||||||
|
```
|
||||||
|
|
||||||
|
## Campos do formulário
|
||||||
|
|
||||||
|
| Campo | Tabela Supabase | Notas |
|
||||||
|
|-------|----------------|-------|
|
||||||
|
| uuid, loja, data, operador, turno | fechamentos_web | PK composto |
|
||||||
|
| saldo_troco, saldo_esperado, fechamento, diferenca | fechamentos_web | diferenca = fechamento - saldo_esperado |
|
||||||
|
| despesas[], sangrias[], vales[], receber[], pixcnpj[], cancelamentos[] | dados (JSON) | Arrays de {desc/nome, valor, ...} |
|
||||||
|
| cartoes{credito[],debito[],alimentacao[],pix[]} | dados (JSON) | Arrays de {valor} |
|
||||||
|
| operadores[], gerentes[], despesas_custom[], clientes[] | listas_personalizadas | Listas por loja |
|
||||||
|
|
||||||
|
## Problemas conhecidos
|
||||||
|
|
||||||
|
- Impressão USB só funciona no Windows
|
||||||
|
- `service_role_key` no cliente desktop é risco de segurança — usar apenas anon_key + RLS
|
||||||
|
- Sem testes automatizados
|
||||||
|
|
||||||
|
## Version History
|
||||||
|
|
||||||
|
- **v1.2.7** — 2026-06-24: 4 bugs críticos concertados (impressão, enviar, dia anterior, localStorage)
|
||||||
|
- **v1.2.6** — 2026-06-24: WebView2Loader.dll incluso; init() restaura operador+turno
|
||||||
|
- **v1.2.5** — 2026-06-24: diferenca = fechamento - esperado (não mais saldo_troco - esperado)
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Generated
-35
@@ -900,14 +900,12 @@ dependencies = [
|
|||||||
"rusqlite",
|
"rusqlite",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"serde_yaml",
|
|
||||||
"tauri",
|
"tauri",
|
||||||
"tauri-build",
|
"tauri-build",
|
||||||
"tauri-plugin-dialog",
|
"tauri-plugin-dialog",
|
||||||
"tauri-plugin-fs",
|
"tauri-plugin-fs",
|
||||||
"tauri-plugin-shell",
|
"tauri-plugin-shell",
|
||||||
"thiserror 2.0.18",
|
"thiserror 2.0.18",
|
||||||
"tokio",
|
|
||||||
"uuid",
|
"uuid",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -3281,19 +3279,6 @@ dependencies = [
|
|||||||
"syn 2.0.118",
|
"syn 2.0.118",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "serde_yaml"
|
|
||||||
version = "0.9.34+deprecated"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47"
|
|
||||||
dependencies = [
|
|
||||||
"indexmap 2.14.0",
|
|
||||||
"itoa",
|
|
||||||
"ryu",
|
|
||||||
"serde",
|
|
||||||
"unsafe-libyaml",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serialize-to-javascript"
|
name = "serialize-to-javascript"
|
||||||
version = "0.1.2"
|
version = "0.1.2"
|
||||||
@@ -4070,25 +4055,11 @@ dependencies = [
|
|||||||
"bytes",
|
"bytes",
|
||||||
"libc",
|
"libc",
|
||||||
"mio",
|
"mio",
|
||||||
"parking_lot",
|
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
"signal-hook-registry",
|
|
||||||
"socket2",
|
"socket2",
|
||||||
"tokio-macros",
|
|
||||||
"windows-sys 0.61.2",
|
"windows-sys 0.61.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "tokio-macros"
|
|
||||||
version = "2.7.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496"
|
|
||||||
dependencies = [
|
|
||||||
"proc-macro2",
|
|
||||||
"quote",
|
|
||||||
"syn 2.0.118",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tokio-native-tls"
|
name = "tokio-native-tls"
|
||||||
version = "0.3.1"
|
version = "0.3.1"
|
||||||
@@ -4399,12 +4370,6 @@ version = "1.13.3"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8"
|
checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "unsafe-libyaml"
|
|
||||||
version = "0.2.11"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "untrusted"
|
name = "untrusted"
|
||||||
version = "0.9.0"
|
version = "0.9.0"
|
||||||
|
|||||||
@@ -15,8 +15,6 @@ serde = { version = "1", features = ["derive"] }
|
|||||||
serde_json = "1"
|
serde_json = "1"
|
||||||
rusqlite = { version = "0.32", features = ["bundled"] }
|
rusqlite = { version = "0.32", features = ["bundled"] }
|
||||||
reqwest = { version = "0.12", features = ["json", "blocking"] }
|
reqwest = { version = "0.12", features = ["json", "blocking"] }
|
||||||
tokio = { version = "1", features = ["full"] }
|
|
||||||
serde_yaml = "0.9"
|
|
||||||
uuid = { version = "1", features = ["v4"] }
|
uuid = { version = "1", features = ["v4"] }
|
||||||
chrono = { version = "0.4", features = ["serde"] }
|
chrono = { version = "0.4", features = ["serde"] }
|
||||||
log = "0.4"
|
log = "0.4"
|
||||||
|
|||||||
@@ -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,
|
||||||
@@ -40,16 +85,39 @@ pub struct ReciboData {
|
|||||||
pub fechamento: f64,
|
pub fechamento: f64,
|
||||||
pub saldo_esperado: f64,
|
pub saldo_esperado: f64,
|
||||||
pub diferenca: f64,
|
pub diferenca: f64,
|
||||||
pub sangrias: f64,
|
// Totais de cartões
|
||||||
pub despesas: f64,
|
|
||||||
pub vales: f64,
|
|
||||||
pub areceber: f64,
|
|
||||||
pub credito: f64,
|
pub credito: f64,
|
||||||
pub debito: f64,
|
pub debito: f64,
|
||||||
pub alimentacao: f64,
|
pub alimentacao: f64,
|
||||||
pub voucher: f64,
|
pub voucher: f64,
|
||||||
|
// Totais diversos (mantidos para compatibilidade com JSON antigo)
|
||||||
|
#[serde(default)]
|
||||||
|
pub sangrias: f64,
|
||||||
|
#[serde(default)]
|
||||||
|
pub despesas: f64,
|
||||||
|
#[serde(default)]
|
||||||
|
pub vales: f64,
|
||||||
|
#[serde(default)]
|
||||||
|
pub areceber: f64,
|
||||||
|
#[serde(default)]
|
||||||
pub pixcnpj: f64,
|
pub pixcnpj: f64,
|
||||||
|
#[serde(default)]
|
||||||
pub cancelamentos: f64,
|
pub cancelamentos: f64,
|
||||||
|
// Arrays para linhas detalhadas (recebidos do frontend)
|
||||||
|
#[serde(default)]
|
||||||
|
pub sangrias_arr: Vec<ReciboLinha>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub despesas_arr: Vec<ReciboLinha>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub pixcnpj_arr: Vec<ReciboLinha>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub vales_arr: Vec<ReciboLinha>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub receber_arr: Vec<ReciboLinha>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub cancelamentos_arr: Vec<ReciboLinha>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub observacoes: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ReciboData {
|
impl ReciboData {
|
||||||
@@ -58,6 +126,7 @@ impl ReciboData {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Gera todos os bytes ESC/POS do recibo 80mm
|
/// Gera todos os bytes ESC/POS do recibo 80mm
|
||||||
|
/// Layout: SANGRIAS → ENTRADAS → DESPESAS → PIX CNPJ → VALES → A RECEBER → CARTÕES → CANCELAMENTOS → TROCO → OBSERVAÇÕES → TOTAL SALDO CAIXA → SALDO ESPERADO → DIFERENÇA
|
||||||
pub fn to_escpos(&self) -> Vec<u8> {
|
pub fn to_escpos(&self) -> Vec<u8> {
|
||||||
let mut out = Vec::new();
|
let mut out = Vec::new();
|
||||||
|
|
||||||
@@ -77,40 +146,141 @@ impl ReciboData {
|
|||||||
out.extend(escpos::kv("Turno:", &self.turno));
|
out.extend(escpos::kv("Turno:", &self.turno));
|
||||||
out.extend(escpos::divider());
|
out.extend(escpos::divider());
|
||||||
|
|
||||||
// Entradas
|
// ── SANGRIAS ──
|
||||||
|
out.extend(escpos::line_bold("SANGRIAS"));
|
||||||
|
for linha in &self.sangrias_arr {
|
||||||
|
if linha.retirada.is_some() || linha.valor.is_some() {
|
||||||
|
let label = linha.label();
|
||||||
|
let obs = linha.obs();
|
||||||
|
out.extend(escpos::kv(&label, &linha.fmt_v()));
|
||||||
|
if !obs.is_empty() {
|
||||||
|
out.extend(escpos::line_center(&format!(" {}", obs)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
out.extend(escpos::divider());
|
||||||
|
|
||||||
|
// ── ENTRADAS ──
|
||||||
out.extend(escpos::line_bold("ENTRADAS"));
|
out.extend(escpos::line_bold("ENTRADAS"));
|
||||||
out.extend(escpos::kv("Saldo Troco:", &self.fmt_money(self.saldo_troco)));
|
out.extend(escpos::kv("Saldo Troco:", &self.fmt_money(self.saldo_troco)));
|
||||||
out.extend(escpos::kv("Fechamento:", &self.fmt_money(self.fechamento)));
|
out.extend(escpos::kv("Fechamento:", &self.fmt_money(self.fechamento)));
|
||||||
out.extend(escpos::kv("Saldo Esperado:", &self.fmt_money(self.saldo_esperado)));
|
out.extend(escpos::kv("Saldo Esperado:", &self.fmt_money(self.saldo_esperado)));
|
||||||
out.extend(escpos::divider());
|
out.extend(escpos::divider());
|
||||||
|
|
||||||
// Débitos
|
// ── DESPESAS ──
|
||||||
out.extend(escpos::line_bold("DEBITOS"));
|
if !self.despesas_arr.is_empty() {
|
||||||
out.extend(escpos::kv("Sangrias:", &self.fmt_money(self.sangrias)));
|
out.extend(escpos::line_bold("DESPESAS"));
|
||||||
out.extend(escpos::kv("Despesas:", &self.fmt_money(self.despesas)));
|
for linha in &self.despesas_arr {
|
||||||
out.extend(escpos::kv("Vales:", &self.fmt_money(self.vales)));
|
if linha.valor.is_some() {
|
||||||
out.extend(escpos::kv("A Receber:", &self.fmt_money(self.areceber)));
|
let label = linha.label();
|
||||||
out.extend(escpos::kv("PIX CNPJ:", &self.fmt_money(self.pixcnpj)));
|
let obs = linha.obs();
|
||||||
out.extend(escpos::divider());
|
out.extend(escpos::kv(&label, &linha.fmt_v()));
|
||||||
|
if !obs.is_empty() {
|
||||||
|
out.extend(escpos::line_center(&format!(" {}", obs)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
out.extend(escpos::divider());
|
||||||
|
}
|
||||||
|
|
||||||
// Cartões
|
// ── PIX CNPJ ──
|
||||||
out.extend(escpos::line_bold("CARTÕES"));
|
if !self.pixcnpj_arr.is_empty() || self.pixcnpj > 0.0 {
|
||||||
|
out.extend(escpos::line_bold("PIX CNPJ"));
|
||||||
|
for linha in &self.pixcnpj_arr {
|
||||||
|
if linha.valor.is_some() {
|
||||||
|
let label = linha.nome.clone().unwrap_or_else(|| "PIX CNPJ".to_string());
|
||||||
|
out.extend(escpos::kv(&label, &linha.fmt_v()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
out.extend(escpos::divider());
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── VALES ──
|
||||||
|
if !self.vales_arr.is_empty() {
|
||||||
|
out.extend(escpos::line_bold("VALES"));
|
||||||
|
for linha in &self.vales_arr {
|
||||||
|
if linha.valor.is_some() {
|
||||||
|
let label = linha.label();
|
||||||
|
let obs = linha.obs();
|
||||||
|
out.extend(escpos::kv(&label, &linha.fmt_v()));
|
||||||
|
if !obs.is_empty() {
|
||||||
|
out.extend(escpos::line_center(&format!(" {}", obs)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
out.extend(escpos::divider());
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── A RECEBER ──
|
||||||
|
if !self.receber_arr.is_empty() {
|
||||||
|
out.extend(escpos::line_bold("A RECEBER"));
|
||||||
|
for linha in &self.receber_arr {
|
||||||
|
if linha.valor.is_some() {
|
||||||
|
let label = linha.label();
|
||||||
|
out.extend(escpos::kv(&label, &linha.fmt_v()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
out.extend(escpos::divider());
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── CARTÕES ──
|
||||||
|
out.extend(escpos::line_bold("CARTOES"));
|
||||||
out.extend(escpos::kv("Credito:", &self.fmt_money(self.credito)));
|
out.extend(escpos::kv("Credito:", &self.fmt_money(self.credito)));
|
||||||
out.extend(escpos::kv("Debito:", &self.fmt_money(self.debito)));
|
out.extend(escpos::kv("Debito:", &self.fmt_money(self.debito)));
|
||||||
out.extend(escpos::kv("Alimentacao:", &self.fmt_money(self.alimentacao)));
|
out.extend(escpos::kv("Alimentacao:", &self.fmt_money(self.alimentacao)));
|
||||||
out.extend(escpos::kv("Voucher:", &self.fmt_money(self.voucher)));
|
out.extend(escpos::kv("PIX Cartao:", &self.fmt_money(self.voucher)));
|
||||||
out.extend(escpos::kv("Cancelamentos:", &self.fmt_money(self.cancelamentos)));
|
out.extend(escpos::kv("Cancelamentos:", &self.fmt_money(self.cancelamentos)));
|
||||||
out.extend(escpos::divider());
|
out.extend(escpos::divider());
|
||||||
|
|
||||||
// Diferença
|
// ── CANCELAMENTOS (detalhe) ──
|
||||||
|
if !self.cancelamentos_arr.is_empty() {
|
||||||
|
out.extend(escpos::line_bold("CANCELAMENTOS"));
|
||||||
|
for linha in &self.cancelamentos_arr {
|
||||||
|
if linha.valor.is_some() {
|
||||||
|
let num = linha.numero.clone().unwrap_or_default();
|
||||||
|
out.extend(escpos::kv(&num, &linha.fmt_v()));
|
||||||
|
if let Some(ref m) = linha.motivo {
|
||||||
|
if !m.is_empty() {
|
||||||
|
out.extend(escpos::line_center(&format!(" {}", m)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
out.extend(escpos::divider());
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── TROCO ──
|
||||||
|
out.extend(escpos::line_bold("TROCO"));
|
||||||
|
out.extend(escpos::kv("Troco:", &self.fmt_money(self.saldo_troco)));
|
||||||
|
out.extend(escpos::divider());
|
||||||
|
|
||||||
|
// ── OBSERVAÇÕES ──
|
||||||
|
if let Some(ref obs) = self.observacoes {
|
||||||
|
if !obs.trim().is_empty() {
|
||||||
|
out.extend(escpos::line_bold("OBSERVACOES"));
|
||||||
|
out.extend(escpos::line_center(obs.trim()));
|
||||||
|
out.extend(escpos::divider());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── TOTAL SALDO CAIXA ──
|
||||||
|
out.extend(escpos::line_bold("TOTAL SALDO DE CAIXA"));
|
||||||
|
out.extend(escpos::kv("Total:", &self.fmt_money(self.fechamento)));
|
||||||
|
out.extend(escpos::divider());
|
||||||
|
|
||||||
|
// ── SALDO ESPERADO ──
|
||||||
|
out.extend(escpos::line_bold("SALDO ESPERADO"));
|
||||||
|
out.extend(escpos::kv("Esperado:", &self.fmt_money(self.saldo_esperado)));
|
||||||
|
out.extend(escpos::divider());
|
||||||
|
|
||||||
|
// ── DIFERENÇA ──
|
||||||
|
out.extend(escpos::line_bold("DIFERENCA"));
|
||||||
let diff_str = if self.diferenca >= 0.0 {
|
let diff_str = if self.diferenca >= 0.0 {
|
||||||
format!("+{}", self.fmt_money(self.diferenca))
|
format!("+{}", self.fmt_money(self.diferenca))
|
||||||
} else {
|
} else {
|
||||||
self.fmt_money(self.diferenca)
|
self.fmt_money(self.diferenca)
|
||||||
};
|
};
|
||||||
out.extend(escpos::line_bold("DIFERENCA:"));
|
|
||||||
out.extend(escpos::title(&diff_str));
|
out.extend(escpos::title(&diff_str));
|
||||||
out.extend(escpos::blank_lines(2));
|
|
||||||
|
|
||||||
// Rodapé
|
// Rodapé
|
||||||
out.extend(escpos::divider());
|
out.extend(escpos::divider());
|
||||||
@@ -122,6 +292,7 @@ impl ReciboData {
|
|||||||
|
|
||||||
// Corta papel
|
// Corta papel
|
||||||
out.extend(escpos::FEED_CUT);
|
out.extend(escpos::FEED_CUT);
|
||||||
|
out.extend(escpos::CUT);
|
||||||
|
|
||||||
out
|
out
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -335,6 +335,7 @@ impl DbManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Atualiza sync_status de um registro.
|
/// Atualiza sync_status de um registro.
|
||||||
|
#[allow(dead_code)]
|
||||||
pub fn atualizar_sync(&self, uuid: &str, status: SyncStatus) -> Result<(), StorageError> {
|
pub fn atualizar_sync(&self, uuid: &str, status: SyncStatus) -> Result<(), StorageError> {
|
||||||
let conn = self.conn.lock().map_err(|_| StorageError::Lock)?;
|
let conn = self.conn.lock().map_err(|_| StorageError::Lock)?;
|
||||||
let now = Utc::now().format("%Y-%m-%dT%H:%M:%S%.3fZ").to_string();
|
let now = Utc::now().format("%Y-%m-%dT%H:%M:%S%.3fZ").to_string();
|
||||||
@@ -346,6 +347,7 @@ impl DbManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Remove um registro (para debugging).
|
/// Remove um registro (para debugging).
|
||||||
|
#[allow(dead_code)]
|
||||||
pub fn remover(&self, uuid: &str) -> Result<bool, StorageError> {
|
pub fn remover(&self, uuid: &str) -> Result<bool, StorageError> {
|
||||||
let conn = self.conn.lock().map_err(|_| StorageError::Lock)?;
|
let conn = self.conn.lock().map_err(|_| StorageError::Lock)?;
|
||||||
let n = conn.execute("DELETE FROM fechamentos WHERE uuid = ?1", params![uuid])?;
|
let n = conn.execute("DELETE FROM fechamentos WHERE uuid = ?1", params![uuid])?;
|
||||||
@@ -508,7 +510,7 @@ pub fn carregar_rascunho(
|
|||||||
// Simplifica para formato que o frontend espera
|
// Simplifica para formato que o frontend espera
|
||||||
let total_sangrias: f64 = e.tabelas.sangrias.iter().filter_map(|r| r.valor).sum();
|
let total_sangrias: f64 = e.tabelas.sangrias.iter().filter_map(|r| r.valor).sum();
|
||||||
let total_despesas: f64 = e.tabelas.despesas.iter().filter_map(|r| r.valor).sum();
|
let total_despesas: f64 = e.tabelas.despesas.iter().filter_map(|r| r.valor).sum();
|
||||||
let total_receber: f64 = e.tabelas.areceber.iter().filter_map(|r| r.valor).sum();
|
let _total_receber: f64 = e.tabelas.areceber.iter().filter_map(|r| r.valor).sum();
|
||||||
let despesa_rows: Vec<_> = e.tabelas.despesas.iter().filter_map(|r| {
|
let despesa_rows: Vec<_> = e.tabelas.despesas.iter().filter_map(|r| {
|
||||||
r.descricao.as_ref().map(|d| serde_json::json!({"desc": d, "valor": r.valor}))
|
r.descricao.as_ref().map(|d| serde_json::json!({"desc": d, "valor": r.valor}))
|
||||||
}).collect();
|
}).collect();
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ use tauri::State;
|
|||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
|
||||||
#[derive(Error, Debug)]
|
#[derive(Error, Debug)]
|
||||||
|
#[allow(dead_code)]
|
||||||
pub enum SupabaseError {
|
pub enum SupabaseError {
|
||||||
#[error("HTTP error: {0}")]
|
#[error("HTTP error: {0}")]
|
||||||
Http(#[from] reqwest::Error),
|
Http(#[from] reqwest::Error),
|
||||||
|
|||||||
+75
-27
@@ -1329,37 +1329,78 @@ ${data.observacoes ? `<div class="obs">Obs: ${data.observacoes}</div>` : ''}
|
|||||||
}
|
}
|
||||||
|
|
||||||
function openPrintPreview(data) {
|
function openPrintPreview(data) {
|
||||||
// Gera HTML e injeta no body de uma div temporária para window.print()
|
// Remove preview anterior se existir
|
||||||
const html = buildPrintHTML(data);
|
const existing = document.getElementById('print-preview-overlay');
|
||||||
const style = document.createElement('style');
|
if (existing) document.body.removeChild(existing);
|
||||||
style.textContent = `
|
|
||||||
@media print {
|
const overlay = document.createElement('div');
|
||||||
body { width: 80mm; margin: 0 auto; font-family: 'Courier New', monospace; font-size: 13px; }
|
overlay.id = 'print-preview-overlay';
|
||||||
.no-print { display: none !important; }
|
overlay.style.cssText = `
|
||||||
}
|
position: fixed; top: 0; left: 0; right: 0; bottom: 0;
|
||||||
.print-actions { position: fixed; top: 10px; right: 10px; z-index: 9999; }
|
background: rgba(0,0,0,0.85); z-index: 99999;
|
||||||
.print-actions button { padding: 8px 16px; cursor: pointer; background: #2563eb; color: #fff; border: none; border-radius: 4px; font-size: 14px; }
|
display: flex; flex-direction: column; align-items: center;
|
||||||
.print-actions button:hover { background: #1d4ed8; }
|
font-family: 'Courier New', monospace;
|
||||||
`;
|
`;
|
||||||
const actions = document.createElement('div');
|
|
||||||
actions.className = 'print-actions no-print';
|
|
||||||
actions.innerHTML = '<button onclick="window.print()">🖨️ Imprimir (Ctrl+P)</button>';
|
|
||||||
|
|
||||||
// Guarda conteúdo original
|
const previewStyle = `
|
||||||
const originalBody = document.body.innerHTML;
|
<style>
|
||||||
|
body { width: 80mm; margin: 0 auto; font-family: 'Courier New', monospace; font-size: 13px; background: #fff; padding: 10px; }
|
||||||
|
.header { text-align: center; margin-bottom: 8px; }
|
||||||
|
.header h1 { font-size: 18px; }
|
||||||
|
.header h2 { font-size: 14px; font-weight: normal; }
|
||||||
|
hr { border: none; border-top: 1px dashed #000; margin: 6px 0; }
|
||||||
|
.section-title { font-weight: bold; font-size: 14px; border-top: 1px dashed #000; padding-top: 4px; margin-top: 6px; }
|
||||||
|
.row { display: flex; justify-content: space-between; padding: 2px 0; }
|
||||||
|
table { width: 100%; border-collapse: collapse; font-size: 12px; }
|
||||||
|
td { padding: 2px 4px; vertical-align: top; }
|
||||||
|
td:nth-child(2) { text-align: right; }
|
||||||
|
td:nth-child(3) { text-align: right; color: #555; font-size: 11px; }
|
||||||
|
.grand-total { font-size: 16px; font-weight: bold; text-align: center; padding: 6px; border: 2px solid #000; margin: 8px 0; }
|
||||||
|
.obs { font-size: 11px; color: #555; margin-top: 6px; white-space: pre-wrap; }
|
||||||
|
.footer { text-align: center; font-size: 11px; color: #555; margin-top: 8px; }
|
||||||
|
@media print { body { background: #fff; } }
|
||||||
|
</style>
|
||||||
|
`;
|
||||||
|
|
||||||
document.body.innerHTML = html;
|
const html = buildPrintHTML(data);
|
||||||
document.head.appendChild(style);
|
const fullHtml = `<!DOCTYPE html><html><head><meta charset="UTF-8"><title>Recibo</title>${previewStyle}</head><body>${html}</body></html>`;
|
||||||
document.body.insertBefore(actions, document.body.firstChild);
|
|
||||||
|
|
||||||
// Quando fechar print dialog, restaura
|
const iframe = document.createElement('iframe');
|
||||||
const onAfterPrint = () => {
|
iframe.style.cssText = 'width:360px; height:600px; border:2px solid #555; margin-top:16px; background:#fff;';
|
||||||
document.body.innerHTML = originalBody;
|
iframe.id = 'print-iframe';
|
||||||
window.removeEventListener('afterprint', onAfterPrint);
|
|
||||||
// Re-carrega scripts essenciais
|
const btnFechar = document.createElement('button');
|
||||||
location.reload();
|
btnFechar.textContent = '✕ Fechar';
|
||||||
|
btnFechar.style.cssText = `
|
||||||
|
margin-top: 12px; padding: 10px 32px;
|
||||||
|
background: #444; color: #fff; border: none; border-radius: 6px;
|
||||||
|
font-size: 15px; cursor: pointer;
|
||||||
|
`;
|
||||||
|
btnFechar.onclick = () => {
|
||||||
|
document.body.removeChild(overlay);
|
||||||
};
|
};
|
||||||
window.addEventListener('afterprint', onAfterPrint);
|
|
||||||
|
const btnImprimir = document.createElement('button');
|
||||||
|
btnImprimir.textContent = '🖨️ Imprimir';
|
||||||
|
btnImprimir.style.cssText = `
|
||||||
|
margin-top: 8px; padding: 10px 32px;
|
||||||
|
background: #e65100; color: #fff; border: none; border-radius: 6px;
|
||||||
|
font-size: 15px; cursor: pointer;
|
||||||
|
`;
|
||||||
|
btnImprimir.onclick = () => {
|
||||||
|
iframe.contentWindow.focus();
|
||||||
|
iframe.contentWindow.print();
|
||||||
|
};
|
||||||
|
|
||||||
|
overlay.appendChild(iframe);
|
||||||
|
overlay.appendChild(btnImprimir);
|
||||||
|
overlay.appendChild(btnFechar);
|
||||||
|
document.body.appendChild(overlay);
|
||||||
|
|
||||||
|
const iframeDoc = iframe.contentWindow.document;
|
||||||
|
iframeDoc.open();
|
||||||
|
iframeDoc.write(fullHtml);
|
||||||
|
iframeDoc.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
function triggerWindowPrint() {
|
function triggerWindowPrint() {
|
||||||
@@ -1569,7 +1610,14 @@ 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 = newEstado();
|
||||||
|
estado.loja = val('f-loja') || 'União';
|
||||||
|
estado.data = val('f-data') || new Date().toISOString().split('T')[0];
|
||||||
|
calcular();
|
||||||
|
renderAllTables();
|
||||||
|
syncUI();
|
||||||
|
showToast('Rascunho limpo.', 'info');
|
||||||
});
|
});
|
||||||
|
|
||||||
document.getElementById('btn-anterior').addEventListener('click', async () => {
|
document.getElementById('btn-anterior').addEventListener('click', async () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user