Compare commits
4 Commits
v2.0.3-ambos
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 275d92abe6 | |||
| 237ceab042 | |||
| 73c20272e5 | |||
| 56a2ed5d31 |
@@ -0,0 +1,164 @@
|
|||||||
|
# Histórico do Projeto — Fechamento de Caixa O Frangão
|
||||||
|
|
||||||
|
## Contexto
|
||||||
|
|
||||||
|
**Dono:** Filipe.Tavares — O Frangão (lojas União e Aliança)
|
||||||
|
**Problema original:** Planilhas manuais + webhooks n8n frágeis para registrar fechamento de caixa
|
||||||
|
**Solução:** App desktop Tauri 2 + SQLite offline + Supabase cloud + impressão térmica 80mm
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Repositório
|
||||||
|
|
||||||
|
```
|
||||||
|
https://git.ofrangao.com.br/filipe/fechamento-caixa
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Stack Técnica
|
||||||
|
|
||||||
|
| Camada | Tecnologia |
|
||||||
|
|--------|-----------|
|
||||||
|
| Frontend | HTML5 + CSS + JavaScript vanilla (single-file `src/index.html`) |
|
||||||
|
| Runtime | Tauri 2 (Rust + WebView2) |
|
||||||
|
| Dados local | SQLite via `rusqlite` |
|
||||||
|
| Dados cloud | Supabase — PostgreSQL + REST API |
|
||||||
|
| Impressão | HTML → `window.print()` com CSS @media print (80mm) |
|
||||||
|
| Build | Cross-compile Linux → Windows (`x86_64-pc-windows-gnu`) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Releases
|
||||||
|
|
||||||
|
| Tag | Data | Descrição |
|
||||||
|
|-----|------|-----------|
|
||||||
|
| `v2.0.4-alianca` | 2026-07-05 | Fix preview modal re-open + sys_total recalc |
|
||||||
|
| `v2.0.3-uniao` | 2026-07-05 | Fix diferença cálculo from array |
|
||||||
|
| `v2.0.2-uniao` | 2026-07-05 | Fix buildPrintHTML sys_total |
|
||||||
|
| `v2.0.1-uniao` | 2026-07-05 | Builds separadas + docs |
|
||||||
|
| `v2.0.0-uniao/alianca/ambos` | 2026-07-04 | Lançamento — app completo com Tauri 2 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Bugs Corrigidos (desde v2.0.0)
|
||||||
|
|
||||||
|
### Preview + Modal (v2.0.4)
|
||||||
|
- **237ceab** — `confirm-print` não fechava mais o modal antes de abrir o preview; `openPrintPreview` reabria o modal ao fechar se ele ainda estava aberto
|
||||||
|
- **73c2027** — `closeConfirmModal()` era chamada antes de `openPrintPreview()` — preview cobria o modal, mas ao fechar voltava pro form
|
||||||
|
- **56a2ed5** — `buildPrintHTML` usava `data.diferenca` (precomputado, stale) em vez de recalcular `sys_total - saldo_esperado - cancelamentos` inline
|
||||||
|
|
||||||
|
### Cálculo da Diferença (v2.0.3)
|
||||||
|
- **acb90f0** — `diferenca` sempre recalculada da array de cancelamentos (não do escalar `total_cancelamentos`)
|
||||||
|
- **6eb08b6** — mesmo fix em `buildReportHTML`, `buildPrintHTML`, `buildConfirmHTML`
|
||||||
|
- **5a01264** — ignora `data.diferenca` pré-computado, recalcula sempre de `sys_total`
|
||||||
|
|
||||||
|
### Rust ESCPOS (v2.0.3)
|
||||||
|
- **733519e** — Rust `to_escpos`: diferença = `sys_total - saldo_esperado - total_cancelamentos` (não mais `fechamento`)
|
||||||
|
|
||||||
|
### CSS / Layout (v2.0.2)
|
||||||
|
- **6440b26** — `buildReportHTML`: diferença = `sysTotal - saldoEsp - cancelamentos`
|
||||||
|
- **420e104** — `buildConfirmHTML` e `buildPrintHTML` usavam `sysTotal`/`total_cancelamentos` em vez de `sys_total`/`cancelamentos`
|
||||||
|
- **b04e5ac** — `.main min-height:0` (footer invisível em resoluções pequenas)
|
||||||
|
- **65c444a** — seletor loja escondia errado (caracteres asiáticos no DOM)
|
||||||
|
- **2bfbf29** — values das options de loja sem acento (`Uniao`/`Alianca`) — alinhado com `LOJA_FIXA` do Rust
|
||||||
|
|
||||||
|
### Impressão (v2.0.1)
|
||||||
|
- **b1585af / 155782f / 16972f4 / d3d5c4f** — fonte 18px→22px bold nos relatórios e preview
|
||||||
|
- **5f1d9a4 / cc45ed9** — botão "Dia Anterior" agora exige senha antes de abrir
|
||||||
|
|
||||||
|
### Builds (v2.0.0)
|
||||||
|
- **659d977** — `DOBRADO_LOJA` injetado via env no build — 3 binários: `fc-uniao.exe`, `fc-alianca.exe`, `fc-ambos.exe`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Bugs Pendentes (conhecidos)
|
||||||
|
|
||||||
|
| # | Bug | Prioridade | Status |
|
||||||
|
|---|-----|-----------|--------|
|
||||||
|
| #6 | `salvarEdicaoModal` reconstrói `id_fechamento` dos inputs em vez de usar `full.id_fechamento` — PATCH pode errar se operador mudar loja/data/operador | Média | Pendente |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Arquitetura de Dados
|
||||||
|
|
||||||
|
### SQLite local (offline-first)
|
||||||
|
```
|
||||||
|
%LOCALAPPDATA%/FechamentoCaixa/fechamento.db
|
||||||
|
```
|
||||||
|
|
||||||
|
### Supabase cloud
|
||||||
|
```
|
||||||
|
https://supabase.ofrangao.com.br
|
||||||
|
Tabela: fechamentos_web
|
||||||
|
```
|
||||||
|
|
||||||
|
### Fluxo de sync
|
||||||
|
```
|
||||||
|
App abre → carrega rascunho do SQLite → operador trabalha offline
|
||||||
|
↓
|
||||||
|
operador clica "Salvar/Sincronizar"
|
||||||
|
↓
|
||||||
|
1. Salva em SQLite local
|
||||||
|
2. Envia para Supabase (se online)
|
||||||
|
↓
|
||||||
|
operador clica "Imprimir"
|
||||||
|
↓
|
||||||
|
window.print() → receipt 80mm
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Comandos Tauri (frontend chama via `invoke`)
|
||||||
|
|
||||||
|
| Comando | Descrição |
|
||||||
|
|---------|-----------|
|
||||||
|
| `salvar_fechamento` | Salva estado no SQLite local |
|
||||||
|
| `carregar_rascunho` | Carrega rascunho por loja+data |
|
||||||
|
| `sb_salvar_fechamento` | POST para Supabase |
|
||||||
|
| `sb_listar_recentes` | Lista fechamentos recentes |
|
||||||
|
| `sb_buscar_por_id` | Busca por ID numérico |
|
||||||
|
| `sb_buscar_por_uuid` | Busca por UUID |
|
||||||
|
| `detectar_impressora` | Detecta EPSON TM-T20 |
|
||||||
|
| `imprimir_recibo` | Imprime via ESCPOS ou fallback window.print() |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Builds — 3 versões
|
||||||
|
|
||||||
|
| Binário | Loja | Seletor |
|
||||||
|
|---------|------|---------|
|
||||||
|
| `fc-uniao.exe` | Fixa: UNIÃO | Escondido |
|
||||||
|
| `fc-alianca.exe` | Fixa: ALIANÇA | Escondido |
|
||||||
|
| `fc-ambos.exe` | Livre | Dropdown visível |
|
||||||
|
|
||||||
|
### Como buildar
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd src-tauri/
|
||||||
|
|
||||||
|
# UNIÃO
|
||||||
|
DOBRADO_LOJA=Uniao cargo build --release --target x86_64-pc-windows-gnu
|
||||||
|
|
||||||
|
# ALIANÇA
|
||||||
|
DOBRADO_LOJA=Alianca cargo build --release --target x86_64-pc-windows-gnu
|
||||||
|
|
||||||
|
# AMBOS
|
||||||
|
cargo build --release --target x86_64-pc-windows-gnu
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Lições Aprendidas
|
||||||
|
|
||||||
|
1. **Cálculos sempre inline** — nunca usar valores pré-computados (`data.diferenca`, `data.total_cancelamentos`) que podem estar stale. Sempre recalcular da fonte.
|
||||||
|
2. **Fechar modal ANTES de abrir overlay** — se um modal vai ser coberto por outro, fechar primeiro e reabrir depois (salvar estado).
|
||||||
|
3. **Cross-compile Linux→Windows** — funciona perfeitamente com `x86_64-pc-windows-gnu` target.
|
||||||
|
4. **Gitea API** — `Authorization: token {TOKEN}` (minúsculo `token`) funciona; `Token {TOKEN}` não. Upload de assets funciona; DELETE de assets retorna 404 (bug do Gitea?).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Autores
|
||||||
|
|
||||||
|
- **Filipe.Tavares** — dono das lojas / demandante
|
||||||
|
- **Hermes Agent** — desenvolvimento + debugging
|
||||||
@@ -322,13 +322,12 @@ impl ReciboData {
|
|||||||
out.extend(escpos::divider());
|
out.extend(escpos::divider());
|
||||||
|
|
||||||
// ── DIFERENÇA ──
|
// ── DIFERENÇA ──
|
||||||
// Diferença = TOTAL SISTEMA - saldo_esperado - cancelamentos
|
// Usa o valor calculado pelo frontend (já correto)
|
||||||
out.extend(escpos::line_bold("DIFERENCA"));
|
out.extend(escpos::line_bold("DIFERENCA"));
|
||||||
let diff = self.sys_total - self.saldo_esperado - self.cancelamentos;
|
let diff_str = if self.diferenca >= 0.0 {
|
||||||
let diff_str = if diff >= 0.0 {
|
format!("+{}", self.fmt_money(self.diferenca))
|
||||||
format!("+{}", self.fmt_money(diff))
|
|
||||||
} else {
|
} else {
|
||||||
self.fmt_money(diff)
|
self.fmt_money(self.diferenca)
|
||||||
};
|
};
|
||||||
out.extend(escpos::title(&diff_str));
|
out.extend(escpos::title(&diff_str));
|
||||||
|
|
||||||
@@ -528,8 +527,10 @@ pub fn caminho_impressora() -> Option<String> {
|
|||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub fn imprimir_recibo(data: String) -> Result<usize, String> {
|
pub fn imprimir_recibo(data: String) -> Result<usize, String> {
|
||||||
|
eprintln!("[DEBUG RECIBO JSON] {}", &data[..data.len().min(500)]);
|
||||||
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))?;
|
||||||
|
eprintln!("[DEBUG DIFERENCA] self.diferenca={}", r.diferenca);
|
||||||
let bytes = r.to_escpos();
|
let bytes = r.to_escpos();
|
||||||
log::info!("Imprimindo recibo: {} bytes ESC/POS", bytes.len());
|
log::info!("Imprimindo recibo: {} bytes ESC/POS", bytes.len());
|
||||||
PRINTER.imprimir_bytes(&bytes)
|
PRINTER.imprimir_bytes(&bytes)
|
||||||
|
|||||||
+10
-4
@@ -1492,7 +1492,7 @@ function buildPrintHTML(data) {
|
|||||||
const fmt = v => 'R$ ' + (v || 0).toFixed(2).replace('.', ',');
|
const fmt = v => 'R$ ' + (v || 0).toFixed(2).replace('.', ',');
|
||||||
const fmtNeg = v => (v < 0 ? '-' : '') + 'R$ ' + Math.abs(v || 0).toFixed(2).replace('.', ',');
|
const fmtNeg = v => (v < 0 ? '-' : '') + 'R$ ' + Math.abs(v || 0).toFixed(2).replace('.', ',');
|
||||||
// Diferença = Total Sistema - Saldo Esperado - Cancelamentos
|
// Diferença = Total Sistema - Saldo Esperado - Cancelamentos
|
||||||
const sysTotalCalc = (data.sysTotal || 0);
|
const sysTotalCalc = (data.sys_total || 0);
|
||||||
const saldoEsperadoCalc = (data.saldo_esperado || 0);
|
const saldoEsperadoCalc = (data.saldo_esperado || 0);
|
||||||
// Cancelamentos: SEMPRE recalcular da array (o scalar pode estar desatualizado)
|
// Cancelamentos: SEMPRE recalcular da array (o scalar pode estar desatualizado)
|
||||||
const cancelamentos_arr = Array.isArray(data.cancelamentos_arr) ? data.cancelamentos_arr : [];
|
const cancelamentos_arr = Array.isArray(data.cancelamentos_arr) ? data.cancelamentos_arr : [];
|
||||||
@@ -1766,7 +1766,7 @@ function buildReportHTML(data) {
|
|||||||
<div class="row"><span>💳 Crédito:</span><span class="val">${fmt(cartCred)}</span></div>
|
<div class="row"><span>💳 Crédito:</span><span class="val">${fmt(cartCred)}</span></div>
|
||||||
<div class="row"><span>💳 Débito:</span><span class="val">${fmt(cartDeb)}</span></div>
|
<div class="row"><span>💳 Débito:</span><span class="val">${fmt(cartDeb)}</span></div>
|
||||||
<div class="row"><span>💳 Alimentação:</span><span class="val">${fmt(cartAli)}</span></div>
|
<div class="row"><span>💳 Alimentação:</span><span class="val">${fmt(cartAli)}</span></div>
|
||||||
<div class="row"><span>❌ Cancelamentos:</span><span class="val">${fmt(cancelamentos)}</span></div>
|
<div class="row"><span>❌ Cancelamentos:</span><span class="val">${fmt(totalCancelCalc)}</span></div>
|
||||||
<hr>
|
<hr>
|
||||||
<div class="row"><span>📋 Fechamento (contado):</span><span class="val">${fmt(fechamento)}</span></div>
|
<div class="row"><span>📋 Fechamento (contado):</span><span class="val">${fmt(fechamento)}</span></div>
|
||||||
<div class="row"><span>🖥️ Total Sistema:</span><span class="val">${fmt(sysTotal)}</span></div>
|
<div class="row"><span>🖥️ Total Sistema:</span><span class="val">${fmt(sysTotal)}</span></div>
|
||||||
@@ -1835,6 +1835,11 @@ function openPrintPreview(data, mode) {
|
|||||||
`;
|
`;
|
||||||
btnFechar.onclick = () => {
|
btnFechar.onclick = () => {
|
||||||
document.body.removeChild(overlay);
|
document.body.removeChild(overlay);
|
||||||
|
// Garante que o modal de confirmação volte a aparecer (caso ainda tenha a classe 'open')
|
||||||
|
const modalConfirm = document.getElementById('modal-confirm');
|
||||||
|
if (modalConfirm && !modalConfirm.classList.contains('open')) {
|
||||||
|
modalConfirm.classList.add('open');
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const btnImprimir = document.createElement('button');
|
const btnImprimir = document.createElement('button');
|
||||||
@@ -2045,6 +2050,8 @@ document.getElementById('confirm-cancel').addEventListener('click', closeConfirm
|
|||||||
document.getElementById('modal-confirm').addEventListener('click', e => { if (e.target.id === 'modal-confirm') closeConfirmModal(); });
|
document.getElementById('modal-confirm').addEventListener('click', e => { if (e.target.id === 'modal-confirm') closeConfirmModal(); });
|
||||||
|
|
||||||
// Print from confirm modal — abre preview idêntico ao modal de confirmação
|
// Print from confirm modal — abre preview idêntico ao modal de confirmação
|
||||||
|
// Não fecha o modal aqui — o preview é um overlay por cima; quando fechar o preview,
|
||||||
|
// o modal já estará visível por baixo (continua no DOM, só que coberto)
|
||||||
document.getElementById('confirm-print').addEventListener('click', async () => {
|
document.getElementById('confirm-print').addEventListener('click', async () => {
|
||||||
// Usa buildReciboData para não duplicar lógica e ter todos os campos certos
|
// Usa buildReciboData para não duplicar lógica e ter todos os campos certos
|
||||||
let data;
|
let data;
|
||||||
@@ -2055,8 +2062,7 @@ document.getElementById('confirm-print').addEventListener('click', async () => {
|
|||||||
alert('Erro ao montar dados do recibo.');
|
alert('Erro ao montar dados do recibo.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// Fecha modal APÓS gerar preview (não antes)
|
// NÃO fecha o modal aqui — só abre o preview por cima
|
||||||
closeConfirmModal();
|
|
||||||
openPrintPreview(data, 'report');
|
openPrintPreview(data, 'report');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user