chore: adiciona README, .gitignore, remove binarios do repo
- Adiciona .gitignore com *.exe, *.tar.gz, *.dll, *.log - Remove binários commitados do tracking (58MB节省) - Adiciona README.md com instruções de setup, estrutura, comandos Tauri - Substitui location.reload() na impressão por iframe (preserva estado) - Substitui location.reload() no btn-limpar por reset de estado in-memory - Nota: security — service_role_key ainda no cliente desktop
This commit is contained in:
+13
@@ -7,3 +7,16 @@ src-tauri/gen/
|
||||
*.msi
|
||||
*.dmg
|
||||
.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.
+40
-26
@@ -1329,37 +1329,44 @@ ${data.observacoes ? `<div class="obs">Obs: ${data.observacoes}</div>` : ''}
|
||||
}
|
||||
|
||||
function openPrintPreview(data) {
|
||||
// Gera HTML e injeta no body de uma div temporária para window.print()
|
||||
// Cria iframe oculto para impressão — NÃO substitui o DOM do app
|
||||
const iframe = document.createElement('iframe');
|
||||
iframe.style.cssText = 'position:fixed;right:0;bottom:0;width:0;height:0;border:none;';
|
||||
iframe.id = 'print-iframe';
|
||||
document.body.appendChild(iframe);
|
||||
|
||||
const html = buildPrintHTML(data);
|
||||
const style = document.createElement('style');
|
||||
style.textContent = `
|
||||
@media print {
|
||||
body { width: 80mm; margin: 0 auto; font-family: 'Courier New', monospace; font-size: 13px; }
|
||||
.no-print { display: none !important; }
|
||||
}
|
||||
.print-actions { position: fixed; top: 10px; right: 10px; z-index: 9999; }
|
||||
.print-actions button { padding: 8px 16px; cursor: pointer; background: #2563eb; color: #fff; border: none; border-radius: 4px; font-size: 14px; }
|
||||
.print-actions button:hover { background: #1d4ed8; }
|
||||
const printStyle = `
|
||||
<style>
|
||||
@media print {
|
||||
body { width: 80mm; margin: 0 auto; font-family: 'Courier New', monospace; font-size: 13px; }
|
||||
.no-print { display: none !important; }
|
||||
}
|
||||
body { padding: 10px; }
|
||||
.print-actions { text-align: center; margin-bottom: 10px; }
|
||||
.print-actions button { padding: 8px 16px; cursor: pointer; background: #e65100; color: #fff; border: none; border-radius: 4px; font-size: 14px; }
|
||||
</style>
|
||||
`;
|
||||
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 originalBody = document.body.innerHTML;
|
||||
const iframeDoc = iframe.contentWindow.document;
|
||||
iframeDoc.open();
|
||||
iframeDoc.write(`<!DOCTYPE html><html><head><title>Recibo</title>${printStyle}</head><body>${html}</body></html>`);
|
||||
iframeDoc.close();
|
||||
|
||||
document.body.innerHTML = html;
|
||||
document.head.appendChild(style);
|
||||
document.body.insertBefore(actions, document.body.firstChild);
|
||||
// Focus e imprime
|
||||
iframe.contentWindow.focus();
|
||||
iframe.contentWindow.print();
|
||||
|
||||
// Quando fechar print dialog, restaura
|
||||
const onAfterPrint = () => {
|
||||
document.body.innerHTML = originalBody;
|
||||
window.removeEventListener('afterprint', onAfterPrint);
|
||||
// Re-carrega scripts essenciais
|
||||
location.reload();
|
||||
// Remove iframe após impressão
|
||||
iframe.contentWindow.onafterprint = () => {
|
||||
document.body.removeChild(iframe);
|
||||
};
|
||||
window.addEventListener('afterprint', onAfterPrint);
|
||||
// Fallback: remove iframe após 5s (onafterprint pode não disparar em alguns browsers)
|
||||
setTimeout(() => {
|
||||
if (document.getElementById('print-iframe')) {
|
||||
document.body.removeChild(iframe);
|
||||
}
|
||||
}, 5000);
|
||||
}
|
||||
|
||||
function triggerWindowPrint() {
|
||||
@@ -1569,7 +1576,14 @@ document.getElementById('btn-recibo').addEventListener('click', async () => {
|
||||
document.getElementById('btn-limpar').addEventListener('click', () => {
|
||||
if (!confirm('Limpar rascunho? Esta acção não pode ser revertida.')) return;
|
||||
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 () => {
|
||||
|
||||
Reference in New Issue
Block a user