Compare commits
124 Commits
v1.2.4
...
v2.0.1-uniao
| Author | SHA1 | Date | |
|---|---|---|---|
| 6eb08b6bd4 | |||
| 5a0126417c | |||
| 733519e1d3 | |||
| 6440b262cc | |||
| 420e104df7 | |||
| b04e5acfd6 | |||
| 65c444ad09 | |||
| 2bfbf29ccb | |||
| cbe2c22440 | |||
| 1dc278dbbd | |||
| c2c9a29247 | |||
| 659d9779b3 | |||
| 5c9cb1905b | |||
| a2d3dcc366 | |||
| 12e3d081a7 | |||
| ad967bf0a1 | |||
| d39cc7518b | |||
| 52bec03988 | |||
| 9639048bf7 | |||
| b1585af3f9 | |||
| 155782f50a | |||
| 16972f4734 | |||
| d3d5c4fbca | |||
| 5f1d9a4e2f | |||
| cc45ed9df7 | |||
| 378a589572 | |||
| 50b0ba4498 | |||
| 7154901ce3 | |||
| 8b24a06496 | |||
| 3058908b37 | |||
| 9b1d45ae50 | |||
| fbed0e57c2 | |||
| 362a76961b | |||
| 7a3a4b221b | |||
| 94bb8840ea | |||
| dba7eb3856 | |||
| 7b3b7a9f1d | |||
| 5641de21f8 | |||
| d77ad55fbd | |||
| 4a5cae8f2f | |||
| 313c740470 | |||
| d21cb0a98d | |||
| 16bee1eacb | |||
| c8016f076d | |||
| b1a9f94231 | |||
| 42aadcca2c | |||
| a8319d96a6 | |||
| bf878545b3 | |||
| 22cda312cf | |||
| d112ad061e | |||
| be917f8fb7 | |||
| cb48afeb9c | |||
| 2b8ff711ed | |||
| 9a4ccabd1b | |||
| 32728eec37 | |||
| 905ca253d7 | |||
| d89b79a3f8 | |||
| c4dda7e2e5 | |||
| 662f3681a5 | |||
| a56d7188f2 | |||
| 7e1ee6bc04 | |||
| 6a8881d41d | |||
| 3b3eb1a4cc | |||
| c7109b1017 | |||
| f45d1e8170 | |||
| 8ea5ba771a | |||
| 84578fe43d | |||
| 3e94789cb0 | |||
| 292d0b85d9 | |||
| a8edab029d | |||
| cbaef8772d | |||
| 80740afbf0 | |||
| 7134c85bb7 | |||
| 4a157d27cb | |||
| a0f2f91131 | |||
| 15f141c191 | |||
| 96a84ec194 | |||
| 7d0c416248 | |||
| 60ec60bf29 | |||
| 6d9e28a6f2 | |||
| ecb9ce3926 | |||
| de5d5aaafb | |||
| a11db54ffc | |||
| 674caf2b7a | |||
| bdfd54e0cf | |||
| 9c64c06ed9 | |||
| 3cf1864af9 | |||
| aa147885a8 | |||
| 00fa17fa52 | |||
| 5e6f8b09a9 | |||
| 9ac4eb7042 | |||
| 2b8fa98ed7 | |||
| 77e927fef0 | |||
| 3f6b2ae6e4 | |||
| 35dcd27dda | |||
| bd867e8419 | |||
| da861ef964 | |||
| 8e642f2e0c | |||
| 7473db6b1f | |||
| 13489d58cf | |||
| 5ad8c1f395 | |||
| 103cd31e50 | |||
| 06783de4e2 | |||
| 11abac8daf | |||
| fea957dfb9 | |||
| 130b2b6da7 | |||
| b47d7766ad | |||
| 78fa778197 | |||
| 5e4340adca | |||
| c01133f39c | |||
| a6b295a6d0 | |||
| f850831e98 | |||
| e70f31e00f | |||
| 79e1dbd645 | |||
| cce8f3df60 | |||
| 318d44e934 | |||
| bfbcd89be9 | |||
| 1848c1aa35 | |||
| 9176b764fd | |||
| 5cab38a27c | |||
| 5b1f3a2691 | |||
| 7e57d30863 | |||
| 7a93a30c22 | |||
| 230304e878 |
+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,139 @@
|
|||||||
|
# Integração Frontend ↔ Tauri (Rust) ↔ Supabase
|
||||||
|
|
||||||
|
## Visão geral da comunicação
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────┐ ┌─────────────────┐ ┌──────────────────┐
|
||||||
|
│ JavaScript │ invoke │ Tauri/Rust │ HTTP │ Supabase │
|
||||||
|
│ (index.html) │────────▶│ commands │────────▶│ (cloud) │
|
||||||
|
│ │◀────────│ │◀────────│ │
|
||||||
|
└─────────────────┘ response└─────────────────┘ response└──────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
O frontend **nunca faz requisições HTTP direto** para o Supabase. Toda comunicação passa pelos comandos Tauri (Rust).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Comandos Tauri disponíveis
|
||||||
|
|
||||||
|
### App
|
||||||
|
| Comando | O que faz |
|
||||||
|
|---------|-----------|
|
||||||
|
| `get_loja` | Retorna a loja configurada |
|
||||||
|
| `get_loja_fixa` | Retorna `"Uniao"`, `"Alianca"` ou `null` (se build fixa) |
|
||||||
|
| `set_loja(loja)` | Define a loja (bloqueado se build fixa) |
|
||||||
|
| `get_config` | Retorna config completo `{ loja, pdv_nome, operador_default, ... }` |
|
||||||
|
| `set_config(cfg)` | Salva config |
|
||||||
|
|
||||||
|
### Storage (SQLite local)
|
||||||
|
| Comando | O que faz |
|
||||||
|
|---------|-----------|
|
||||||
|
| `salvar_fechamento(estado)` | Salva estado completo no SQLite local |
|
||||||
|
| `carregar_rascunho(loja, data)` | Retorna último rascunho para loja+data |
|
||||||
|
| `listar_fechamentos(loja, limite)` | Lista fechamentos de uma loja |
|
||||||
|
| `buscar_fechamento_por_id(id)` | Busca por ID local |
|
||||||
|
| `pendentes_sync` | Retorna fechamentos que ainda não foram syncados |
|
||||||
|
|
||||||
|
### Supabase
|
||||||
|
| Comando | O que faz |
|
||||||
|
|---------|-----------|
|
||||||
|
| `sb_salvar_fechamento(estado)` | Upsert no Supabase via service_role JWT |
|
||||||
|
| `sb_carregar_rascunho(loja, data)` | Busca rascunho mais recente |
|
||||||
|
| `sb_listar_recentes(loja, limite)` | Lista fechamentos recentes |
|
||||||
|
| `sb_buscar_por_id(id)` | Busca por ID |
|
||||||
|
| `sb_buscar_por_uuid(uuid)` | Busca por UUID |
|
||||||
|
| `sb_buscar_por_id_fechamento(id_fechamento)` | Busca por chave natural |
|
||||||
|
| `sb_atualizar_fechamento(id_fechamento, updates)` | PATCH campos específicos |
|
||||||
|
| `sb_salvar_listas(loja, listas)` | Salva operadores/gerentes customizados |
|
||||||
|
| `sb_carregar_listas(loja)` | Carrega listas salvas |
|
||||||
|
| `sb_online` | Retorna `true`/`false` se Supabase está acessível |
|
||||||
|
|
||||||
|
### Printer
|
||||||
|
| Comando | O que faz |
|
||||||
|
|---------|-----------|
|
||||||
|
| `detectar_impressora` | Detecta EPSON TM-T20 USB |
|
||||||
|
| `configurar_impressora(caminho)` | Define caminho da impressora |
|
||||||
|
| `caminho_impressora` | Retorna caminho configurado |
|
||||||
|
| `imprimir_recibo(html)` | Envia HTML puro para impressora |
|
||||||
|
| `teste_impressora` | Imprime página de teste |
|
||||||
|
| `ativar_modo_teste_impressao` | Ativa modo teste (salva PDF em vez de imprimir) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Fluxo de salvar um fechamento
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// No frontend (index.html), quando operador clica "Salvar":
|
||||||
|
async function salvarFechamento() {
|
||||||
|
const estado = buildEstado(); // coleta todos os campos do formulário
|
||||||
|
|
||||||
|
// 1. Salva localmente (sempre funciona offline)
|
||||||
|
await invoke('salvar_fechamento', { estado });
|
||||||
|
|
||||||
|
// 2. Tenta sync com Supabase
|
||||||
|
try {
|
||||||
|
await invoke('sb_salvar_fechamento', { estado });
|
||||||
|
toast('✅ Salvo e sincronizado');
|
||||||
|
} catch(e) {
|
||||||
|
// Supabase offline — fica pendente no SQLite
|
||||||
|
toast('💾 Salvo localmente (sync pendente)');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Como o Rust conversa com o Supabase
|
||||||
|
|
||||||
|
Em `supabase.rs`, o plugin usa `reqwest` (HTTP client) com:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
fn headers(&self, use_service: bool) -> reqwest::header::HeaderMap {
|
||||||
|
let key = if use_service { &self.service_role_key } else { &self.anon_key };
|
||||||
|
let mut headers = reqwest::header::HeaderMap::new();
|
||||||
|
headers.insert(
|
||||||
|
reqwest::header::AUTHORIZATION,
|
||||||
|
format!("Bearer {}", key).parse().unwrap(),
|
||||||
|
);
|
||||||
|
headers
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- **`use_service = true`:** para INSERT/UPDATE (salvar fechamento)
|
||||||
|
- **`use_service = false`:** para GET (listar, buscar)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Upsert — salvar sem duplicar
|
||||||
|
|
||||||
|
O Supabase não tem UPSERT nativo via REST. O plugin usa um truque:
|
||||||
|
|
||||||
|
```http
|
||||||
|
POST /fechamentos_web HTTP/1.1
|
||||||
|
Prefer: resolution=merge-duplicates
|
||||||
|
```
|
||||||
|
|
||||||
|
Se o `id_fechamento` já existe, o PostgREST faz um UPDATE em vez de INSERT. Se não existe, faz INSERT.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## UUID vs id_fechamento
|
||||||
|
|
||||||
|
| Campo | O que é | Quem gera |
|
||||||
|
|-------|---------|-----------|
|
||||||
|
| `id` | ID sequencial do PostgreSQL | Supabase (auto) |
|
||||||
|
| `uuid` | UUID único do registro local | Frontend JS (`crypto.randomUUID()`) |
|
||||||
|
| `id_fechamento` | Chave natural: `{loja}_{data}_{operador}` | Frontend JS (construído na hora) |
|
||||||
|
|
||||||
|
O `id_fechamento` é a chave de negócio. O `uuid` é para rastrear registros entre local e cloud.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Offline first — como funciona
|
||||||
|
|
||||||
|
1. **Operador/edita** → `salvar_fechamento` → SQLite (sempre funciona)
|
||||||
|
2. **Operador/edita** → `sb_salvar_fechamento` → Supabase (só se online)
|
||||||
|
3. **Se offline:** o fechamento fica no SQLite com `sync_status = "local"`
|
||||||
|
4. **Quando conecta:** `pendentes_sync` retorna os pendentes → sync automático
|
||||||
|
|
||||||
|
O sync automático é controlado por `sync_automatico` na config da app.
|
||||||
+209
@@ -0,0 +1,209 @@
|
|||||||
|
# Builds — Como gerar fc-uniao.exe, fc-alianca.exe, fc-ambos.exe
|
||||||
|
|
||||||
|
## Conceito
|
||||||
|
|
||||||
|
O mesmo código gera 3 binários diferentes. A diferença entre eles é apenas o valor da variável de ambiente `DOBRADO_LOJA` passado na hora da compilação Rust.
|
||||||
|
|
||||||
|
- **Build fixa (União ou Aliança):** o operador **não consegue trocar a loja** no formulário
|
||||||
|
- **Build livre (Ambós):** o operador escolhe entre União e Aliança no dropdown
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Mecanismo: `option_env!` em Rust
|
||||||
|
|
||||||
|
Em `src-tauri/src/plugins/app.rs`:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
/// DOBRADO_LOJA é uma variável de ambiente lida em TEMPO DE COMPILAÇÃO.
|
||||||
|
/// Se não definida, retorna None.
|
||||||
|
pub const LOJA_FIXA: Option<&'static str> = option_env!("DOBRADO_LOJA");
|
||||||
|
```
|
||||||
|
|
||||||
|
O `option_env!` é uma macro built-in do Rust que avalia expressões const em tempo de compilação. Se `DOBRADO_LOJA` estiver definida como env var, `LOJA_FIXA` contém o valor. Se não, `None`.
|
||||||
|
|
||||||
|
### Por que compile-time?
|
||||||
|
|
||||||
|
Se fosse runtime (`std::env::var`), o valor estaría hardcoded no binário da mesma forma, mas compile-time é mais limpo porque o Rust elimina código morto (dead code elimination). Se `LOJA_FIXA` é `None`, o Rust pode optimizar o código de "loja fixa" para fora do binário final.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Comportamento por build
|
||||||
|
|
||||||
|
| Build | `DOBRADO_LOJA` | Seletor loja | `set_loja` | `get_loja_fixa` |
|
||||||
|
|-------|----------------|-------------|-------------|-----------------|
|
||||||
|
| União | `Uniao` | **Escondido** | Bloqueado com erro | `"Uniao"` |
|
||||||
|
| Aliança | `Alianca` | **Escondido** | Bloqueado com erro | `"Alianca"` |
|
||||||
|
| Ambos | _(não definido)_ | Visível + funcional | Permitido | `null` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Como buildar (Linux → Windows cross-compile)
|
||||||
|
|
||||||
|
### Pré-requisitos
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Instalar target cross-compile
|
||||||
|
rustup target add x86_64-pc-windows-gnu
|
||||||
|
|
||||||
|
# Ou via apt (Ubuntu/Debian)
|
||||||
|
sudo apt install mingw-w64
|
||||||
|
```
|
||||||
|
|
||||||
|
### Comandos
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd src-tauri/
|
||||||
|
|
||||||
|
# ── UNIÃO ─────────────────────────────────────────────
|
||||||
|
DOBRADO_LOJA=Uniao \
|
||||||
|
cargo build --release --target x86_64-pc-windows-gnu
|
||||||
|
|
||||||
|
# Binário gerado:
|
||||||
|
# src-tauri/target/x86_64-pc-windows-gnu/release/fechamento-caixa.exe
|
||||||
|
|
||||||
|
# ── ALIANÇA ──────────────────────────────────────────
|
||||||
|
DOBRADO_LOJA=Alianca \
|
||||||
|
cargo build --release --target x86_64-pc-windows-gnu
|
||||||
|
|
||||||
|
# ── AMBOS ────────────────────────────────────────────
|
||||||
|
cargo build --release --target x86_64-pc-windows-gnu
|
||||||
|
# (sem DOBRADO_LOJA → loja livre)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Local do binário
|
||||||
|
|
||||||
|
```
|
||||||
|
src-tauri/target/x86_64-pc-windows-gnu/release/fechamento-caixa.exe
|
||||||
|
```
|
||||||
|
|
||||||
|
Renomear para `fc-uniao.exe`, `fc-alianca.exe`, `fc-ambos.exe` antes de distribuir.
|
||||||
|
|
||||||
|
### ⚠️ Distribuição: WebView2Loader.dll
|
||||||
|
|
||||||
|
O binário **precisa da `WebView2Loader.dll` na mesma pasta** para funcionar. Esta DLL é o loader do WebView2 (o navegador embutido do Tauri).
|
||||||
|
|
||||||
|
```
|
||||||
|
Pasta de distribuição (cada build):
|
||||||
|
├── fc-uniao.exe
|
||||||
|
├── WebView2Loader.dll ← copiar junto!
|
||||||
|
```
|
||||||
|
|
||||||
|
A DLL está em:
|
||||||
|
```
|
||||||
|
src-tauri/target/x86_64-pc-windows-gnu/release/WebView2Loader.dll
|
||||||
|
```
|
||||||
|
|
||||||
|
Para copiar junto ao buildar:
|
||||||
|
```bash
|
||||||
|
DLL="src-tauri/target/x86_64-pc-windows-gnu/release/WebView2Loader.dll"
|
||||||
|
cp ${DLL} /tmp/fc-uniao/
|
||||||
|
cp ${DLL} /tmp/fc-alianca/
|
||||||
|
cp ${DLL} /tmp/fc-ambos/
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## O que acontece no frontend quando a loja é fixa
|
||||||
|
|
||||||
|
Em `src/index.html`, `DOMContentLoaded`:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
document.addEventListener('DOMContentLoaded', async () => {
|
||||||
|
try {
|
||||||
|
const fixa = await window.__TAURI__.core.invoke('get_loja_fixa');
|
||||||
|
if (fixa) {
|
||||||
|
// Build fixa: esconde o <select id="f-loja">
|
||||||
|
const sel = document.getElementById('f-loja');
|
||||||
|
sel.style.display = 'none';
|
||||||
|
sel.disabled = true;
|
||||||
|
|
||||||
|
// Esconde o label "Loja"
|
||||||
|
const label = document.querySelector('.meta-label');
|
||||||
|
if (label && label.textContent.trim() === 'Loja') {
|
||||||
|
label.style.display = 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
estado.loja = fixa; // define sem precisar do select
|
||||||
|
}
|
||||||
|
} catch(e) {
|
||||||
|
console.warn('get_loja_fixa não disponível:', e);
|
||||||
|
}
|
||||||
|
// ... resto do init
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## O que acontece no Rust quando a loja é fixa
|
||||||
|
|
||||||
|
```rust
|
||||||
|
// set_loja retorna erro se a build tem loja fixa
|
||||||
|
#[tauri::command]
|
||||||
|
pub fn set_loja(state: State<'_, AppState>, loja: String) -> Result<(), String> {
|
||||||
|
if LOJA_FIXA.is_some() {
|
||||||
|
return Err("Loja fixa em tempo de build — não é possível trocar.".to_string());
|
||||||
|
}
|
||||||
|
state.config.lock().unwrap().loja = loja;
|
||||||
|
state.save()
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Testar localmente (dev mode)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd src-tauri/
|
||||||
|
|
||||||
|
# Com loja fixa (dev)
|
||||||
|
DOBRADO_LOJA=Uniao cargo tauri dev
|
||||||
|
|
||||||
|
# Com loja livre (dev)
|
||||||
|
cargo tauri dev
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Criar release no Gitea
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. Buildar as 3 versões e copiar para /tmp/
|
||||||
|
cp src-tauri/target/x86_64-pc-windows-gnu/release/fechamento-caixa.exe /tmp/fc-uniao.exe
|
||||||
|
cp src-tauri/target/x86_64-pc-windows-gnu/release/fechamento-caixa.exe /tmp/fc-alianca.exe
|
||||||
|
cp src-tauri/target/x86_64-pc-windows-gnu/release/fechamento-caixa.exe /tmp/fc-ambos.exe
|
||||||
|
|
||||||
|
# 2. Criar releases via Gitea API
|
||||||
|
GITEA_TOKEN="seu_token_aqui"
|
||||||
|
REPO="filipe/fechamento-caixa"
|
||||||
|
|
||||||
|
for build in uniao alianca ambos; do
|
||||||
|
TAG="v2.0.0-${build}"
|
||||||
|
FILE="/tmp/fc-${build}.exe"
|
||||||
|
|
||||||
|
# Criar release
|
||||||
|
RESP=$(curl -s -H "Authorization: token ${GITEA_TOKEN}" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d "{\"tag_name\":\"${TAG}\",\"name\":\"${TAG}\",\"body\":\"Build ${build}\"}" \
|
||||||
|
"https://git.ofrangao.com.br/api/v1/repos/${REPO}/releases")
|
||||||
|
ID=$(echo $RESP | python3 -c "import sys,json; print(json.load(sys.stdin)['id'])")
|
||||||
|
|
||||||
|
# Upload asset
|
||||||
|
curl -s -H "Authorization: token ${GITEA_TOKEN}" \
|
||||||
|
-H "Content-Type: application/octet-stream" \
|
||||||
|
--data-binary @${FILE} \
|
||||||
|
"https://git.ofrangao.com.br/api/v1/repos/${REPO}/releases/${ID}/assets?name=fc-${build}.exe"
|
||||||
|
done
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Arquivos que mudam entre builds
|
||||||
|
|
||||||
|
**Nenhum arquivo fonte muda.** O binário é identico exceto pelo valor de `LOJA_FIXA` hardcoded.
|
||||||
|
|
||||||
|
Para verificar o valor hardcoded no binário:
|
||||||
|
```bash
|
||||||
|
strings fc-uniao.exe | grep -E "Uniao|Alianca"
|
||||||
|
```
|
||||||
|
|
||||||
|
O binário `fc-uniao.exe` contém a string `Uniao` no lugar de `LOJA_FIXA`, enquanto `fc-ambos.exe` não contém nada (ou contém `None` ou não tem referência).
|
||||||
@@ -0,0 +1,192 @@
|
|||||||
|
# Fechamento de Caixa — O Frangão
|
||||||
|
|
||||||
|
## O que é
|
||||||
|
|
||||||
|
App desktop nativo para registrar o fechamento de caixa diário das lojas **União** e **Aliança** do O Frangão. Cada fechamento registra: saldo em dinheiro, cartões de crédito/débito/alimentação/PIX, vales, sangrias, despesas, PIX CNPJ, e itens a receber.
|
||||||
|
|
||||||
|
## Ideia central
|
||||||
|
|
||||||
|
O app é um **formulário inteligente** que:
|
||||||
|
|
||||||
|
1. Captura todos os dados de fechamento de caixa (dinheiro, cartões, despesas, etc.)
|
||||||
|
2. Salva localmente em **SQLite** (funciona offline)
|
||||||
|
3. Envia automaticamente para o **Supabase** (cloud) quando há conexão
|
||||||
|
4. Gera um **recibo térmico 80mm** para impressão
|
||||||
|
|
||||||
|
O objetivo é substituir planilhas manuais e webhooks frágeis, dando uma fonte única de verdade para os dados de caixa.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Arquitetura geral
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────┐ ┌──────────────────┐ ┌────────────────────┐
|
||||||
|
│ HTML/JS │────▶│ Tauri (Rust) │────▶│ SQLite (local) │
|
||||||
|
│ (frontend) │◀────│ plugins: │◀────│ fechamentos.db │
|
||||||
|
│ │ │ app, storage, │ │ - funciona offline │
|
||||||
|
│ - formulário │ │ supabase, │ │ - sync automático │
|
||||||
|
│ - impressão │ │ printer │ └────────────────────┘
|
||||||
|
│ - estado JS │ └────────┬─────────┘ │
|
||||||
|
└─────────────────┘ │ ▼
|
||||||
|
│ ┌────────────────────┐
|
||||||
|
│ │ Supabase Cloud │
|
||||||
|
│ │ (supabase. │
|
||||||
|
└─────────────▶│ ofrangao.com.br) │
|
||||||
|
│ - PostgreSQL │
|
||||||
|
│ - RLS + JWT auth │
|
||||||
|
│ - API REST │
|
||||||
|
└────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Stack técnica
|
||||||
|
|
||||||
|
| Camada | Tecnologia |
|
||||||
|
|--------|-----------|
|
||||||
|
| Frontend | HTML5 + CSS + JavaScript vanilla (single-file `src/index.html`) |
|
||||||
|
| Runtime | [Tauri 2](https://tauri.app/) (Rust + WebView2) |
|
||||||
|
| Dados local | SQLite via `rusqlite` |
|
||||||
|
| Dados cloud | [Supabase](https://supabase.com/) — PostgreSQL + REST API |
|
||||||
|
| Impressão | HTML → `window.print()` com CSS @media print (80mm) |
|
||||||
|
| Build | Cross-compile Linux → Windows (`x86_64-pc-windows-gnu`) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Repositório
|
||||||
|
|
||||||
|
```
|
||||||
|
https://git.ofrangao.com.br/filipe/fechamento-caixa
|
||||||
|
```
|
||||||
|
|
||||||
|
### Estrutura de arquivos
|
||||||
|
|
||||||
|
```
|
||||||
|
fechamento-caixa/
|
||||||
|
├── src/
|
||||||
|
│ └── index.html ← Frontend completo (HTML + CSS + JS)
|
||||||
|
├── src-tauri/
|
||||||
|
│ ├── src/
|
||||||
|
│ │ ├── main.rs ← Entry point + plugin initialization
|
||||||
|
│ │ └── plugins/
|
||||||
|
│ │ ├── app.rs ← Config da app (loja, PDV, sync)
|
||||||
|
│ │ ├── storage.rs ← SQLite local
|
||||||
|
│ │ ├── supabase.rs ← Sync com Supabase
|
||||||
|
│ │ ├── printer.rs ← Impressão
|
||||||
|
│ │ └── escpos.rs ← Helpers ESCPOS
|
||||||
|
│ └── tauri.conf.json ← Config Tauri (janela, bundle, etc.)
|
||||||
|
├── SPEC.md ← Este arquivo
|
||||||
|
├── SUPABASE_SCHEMA.md ← Schema do banco de dados
|
||||||
|
├── API_INTEGRATION.md ← Como o frontend se comunica com Supabase
|
||||||
|
└── BUILS_FARM.md ← Como gerar fc-uniao.exe, fc-alianca.exe, fc-ambos.exe
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Builds — 3 versões do app
|
||||||
|
|
||||||
|
O mesmo código gera 3 binários diferentes conforme a variável de ambiente `DOBRADO_LOJA`:
|
||||||
|
|
||||||
|
## Downloads
|
||||||
|
|
||||||
|
| Binário | Loja | Download |
|
||||||
|
|---------|------|----------|
|
||||||
|
| `fc-uniao.exe` | **Travada: UNIÃO** | [Baixar](https://git.ofrangao.com.br/filipe/fechamento-caixa/releases/download/v2.0.0-uniao/fc-uniao.exe) |
|
||||||
|
| `fc-alianca.exe` | **Travada: ALIANÇA** | [Baixar](https://git.ofrangao.com.br/filipe/fechamento-caixa/releases/download/v2.0.0-alianca/fc-alianca.exe) |
|
||||||
|
| `fc-ambos.exe` | **Seletor livre** | [Baixar](https://git.ofrangao.com.br/filipe/fechamento-caixa/releases/download/v2.0.0-ambos/fc-ambos.exe) |
|
||||||
|
|
||||||
|
Quando a loja é fixa, o seletor de loja no formulário é **escondido via JavaScript** e tentativas de mudar via API retornam erro.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Fluxo de um fechamento
|
||||||
|
|
||||||
|
```
|
||||||
|
1. Operador abre o app
|
||||||
|
↓
|
||||||
|
2. Seleciona Data, Operador, Turno, Loja (se não for fixa)
|
||||||
|
↓
|
||||||
|
3. Preenche os campos:
|
||||||
|
- Saldo de Troy
|
||||||
|
- Cartões (crédito, débito, alimentação, PIX) — um valor por vez
|
||||||
|
- Despesas (descrição + valor)
|
||||||
|
- Sangrias (hora + gerente + valor)
|
||||||
|
- PIX CNPJ (nome + valor)
|
||||||
|
- Vales (nome + observação + valor)
|
||||||
|
- A Receber (nome + valor)
|
||||||
|
- Cancelamentos (número + motivo + valor)
|
||||||
|
↓
|
||||||
|
4. App calcula:
|
||||||
|
TOTAL SISTEMA = soma de todos os campos acima
|
||||||
|
DIFERENÇA = TOTAL SISTEMA − Saldo Esperado − Cancelamentos
|
||||||
|
↓
|
||||||
|
5. Operador clica "💾 Salvar / Sincronizar"
|
||||||
|
→ Salva em SQLite local
|
||||||
|
→ Envia para Supabase (se online)
|
||||||
|
↓
|
||||||
|
6. Operador clica "🖨️ Imprimir"
|
||||||
|
→ Abre preview do recibo 80mm
|
||||||
|
→ window.print() para impressora térmica
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Tauri — o runtime desktop
|
||||||
|
|
||||||
|
O app roda como **Tauri 2**, um runtime que embute um navegador WebView2 (Windows) dentro de um processo Rust.
|
||||||
|
|
||||||
|
### O que o Tauri fornece
|
||||||
|
|
||||||
|
| Recurso | Como é usado |
|
||||||
|
|---------|-------------|
|
||||||
|
| **Janela nativa** | A janela do app com barra de título, controls de redimensionar, ícone |
|
||||||
|
| **Sistema de arquivos** | Lê/escreve `config.json` e `fechamento.db` em `%LOCALAPPDATA%/FechamentoCaixa/` |
|
||||||
|
| **Invocação de comandos Rust** | Frontend JS chama `invoke('nome_comando', args)` → Rust executa e retorna |
|
||||||
|
| **HTTP client** | Plugin `supabase.rs` usa `reqwest` para falar com o Supabase |
|
||||||
|
| **Shell** | Abre diálogo de impressoras, executa `cmd /C copy` para impressão |
|
||||||
|
| **Diálogos nativos** | `tauri_plugin_dialog` para escolher arquivos, pastas, impressora |
|
||||||
|
|
||||||
|
### Comandos Tauri (Rust → JS)
|
||||||
|
|
||||||
|
O frontend **nunca acessa** Supabase, SQLite ou impressora diretamente. Tudo passa por `invoke`:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// Salvar fechamento (SQLite + Supabase)
|
||||||
|
await invoke('salvar_fechamento', { estado });
|
||||||
|
|
||||||
|
// Sync com Supabase
|
||||||
|
await invoke('sb_salvar_fechamento', { estado });
|
||||||
|
|
||||||
|
// Detectar impressora
|
||||||
|
await invoke('detectar_impressora');
|
||||||
|
|
||||||
|
// Imprimir
|
||||||
|
await invoke('imprimir_recibo', { html: reciboHTML });
|
||||||
|
```
|
||||||
|
|
||||||
|
### Estrutura dos plugins Rust
|
||||||
|
|
||||||
|
```
|
||||||
|
src-tauri/src/plugins/
|
||||||
|
├── app.rs — Config da app (loja, operador default, sync)
|
||||||
|
├── storage.rs — SQLite local (fonte primária offline)
|
||||||
|
├── supabase.rs — Cliente HTTP para Supabase (service_role JWT)
|
||||||
|
├── printer.rs — Detecção EPSON TM-T20, fallback para window.print()
|
||||||
|
└── escpos.rs — Helpers ESCPOS (alinhamento, negrito, corte)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Onde os dados ficam
|
||||||
|
|
||||||
|
| Dado | Local |
|
||||||
|
|------|-------|
|
||||||
|
| Config (SUPABASE keys, loja default) | `%LOCALAPPDATA%/FechamentoCaixa/config.json` |
|
||||||
|
| Banco SQLite (fechamentos) | `%LOCALAPPDATA%/FechamentoCaixa/fechamento.db` |
|
||||||
|
| Rascunho auto-save | `%LOCALAPPDATA%/FechamentoCaixa/fechamento_rascunho.db` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Autores & Contexto
|
||||||
|
|
||||||
|
- **Desenvolvedor:** Filipe.Tavares
|
||||||
|
- **Dono das lojas:** O Frangão — União e Aliança
|
||||||
|
- **Objetivo:** Substituir planilhas manuais e webhooks n8n por um sistema de fechamento de caixa confiável e offline-first
|
||||||
@@ -0,0 +1,142 @@
|
|||||||
|
# Schema do Supabase — Fechamento de Caixa
|
||||||
|
|
||||||
|
## Servidor
|
||||||
|
|
||||||
|
- **URL:** `https://supabase.ofrangao.com.br`
|
||||||
|
- **Instância:** O Frangão — PostgreSQL hospedado
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Tabela principal: `fechamentos_web`
|
||||||
|
|
||||||
|
```sql
|
||||||
|
CREATE TABLE public.fechamentos_web (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
id_fechamento TEXT UNIQUE NOT NULL, -- chave natural: {loja}_{data}_{operador}
|
||||||
|
loja TEXT NOT NULL, -- "Uniao" | "Alianca"
|
||||||
|
data DATE NOT NULL, -- data do fechamento (YYYY-MM-DD)
|
||||||
|
operador TEXT NOT NULL, -- nome do operador
|
||||||
|
turno TEXT NOT NULL, -- "manha" | "tarde"
|
||||||
|
observacoes TEXT DEFAULT 'rascunho', -- "rascunho" | "finalizado"
|
||||||
|
|
||||||
|
-- Totais de fechamento (campos escalares)
|
||||||
|
fechamento_dinheiro REAL DEFAULT 0, -- dinheiro no caixa
|
||||||
|
fechamento_cartoes REAL DEFAULT 0, -- total cartões
|
||||||
|
fechamento_receber REAL DEFAULT 0, -- a receber
|
||||||
|
fechamento REAL DEFAULT 0, -- TOTAL: dinheiro + cartões + receber
|
||||||
|
|
||||||
|
-- Totais do sistema (valores lidos do sistema PAF-ECF)
|
||||||
|
sys_total REAL DEFAULT 0, -- soma: troco+crédito+débito+alimentação+vales+receber+pixCartao+pixCnpj
|
||||||
|
saldo_esperado REAL DEFAULT 0, -- quanto deveria ter (informado pelo operador)
|
||||||
|
|
||||||
|
-- Controle
|
||||||
|
criado_em TIMESTAMPTZ DEFAULT NOW(),
|
||||||
|
atualizado_em TIMESTAMPTZ DEFAULT NOW(),
|
||||||
|
enviado_por TEXT,
|
||||||
|
ip_origem TEXT,
|
||||||
|
user_agent TEXT,
|
||||||
|
|
||||||
|
-- Dados variáveis em formato livre (JSONB)
|
||||||
|
dados JSONB DEFAULT '{}'::jsonb
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Comentários sobre colunas
|
||||||
|
|
||||||
|
| Coluna | O que guarda |
|
||||||
|
|--------|-------------|
|
||||||
|
| `id_fechamento` | Chave natural única. Ex: `uniao_2026-06-19_filipe`. Formato: `{loja}_{data}_{operador}` em minúsculas, espaços → `_` |
|
||||||
|
| `sys_total` | Total calculado pelo sistema — é a soma de **todos** os campos de cartão + vales + despesas + sangrias + etc |
|
||||||
|
| `saldo_esperado` | Valor informado pelo operador — o quanto ele espera ter em caixa |
|
||||||
|
| `dados` | JSONB com todos os arrays: `cartoes_credito[]`, `cartoes_debito[]`, `cartoes_alimentacao[]`, `cartoes_pix[]`, `despesas[]`, `sangrias[]`, `pixcnpj[]`, `vales[]`, `receber[]`, `cancelamentos[]` |
|
||||||
|
| `observacoes` | `rascunho` enquanto.edita, `finalizado` quando o operador confirma |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Fórmula de diferença
|
||||||
|
|
||||||
|
```
|
||||||
|
DIFERENÇA = sys_total − saldo_esperado − total_cancelamentos
|
||||||
|
```
|
||||||
|
|
||||||
|
Se `DIFERENÇA = 0` → ✅ OK (caixa fecha perfeito)
|
||||||
|
Se `DIFERENÇA > 0` → ⚠️ Sobrou dinheiro
|
||||||
|
Se `DIFERENÇA < 0` → 🔴 Faltou dinheiro
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Colunas dentro do JSONB `dados`
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"cartoes_credito": [152.00, 123.50, ...],
|
||||||
|
"cartoes_debito": [15.45, 20.00, ...],
|
||||||
|
"cartoes_alimentacao": [12.55, ...],
|
||||||
|
"cartoes_pix": [112.50, ...],
|
||||||
|
"despesas": [{"desc": "luz", "valor": 150.00}, ...],
|
||||||
|
"sangrias": [{"hora": "14:30", "gerente": "João", "retirada": 50.00, "valor": 50.00}, ...],
|
||||||
|
"pixcnpj": [{"nome": "João Silva", "valor": 80.00}, ...],
|
||||||
|
"vales": [{"nome": "Maria", "obs": "vale transporte", "valor": 30.00}, ...],
|
||||||
|
"receber": [{"nome": "Pedro", "valor": 45.00}, ...],
|
||||||
|
"cancelamentos": [{"numero": "001", "motivo": "erro digitação", "valor": 25.00}, ...]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Row Level Security (RLS)
|
||||||
|
|
||||||
|
A tabela usa RLS com duas políticas:
|
||||||
|
|
||||||
|
1. **Anon (chave pública):** pode ler registros
|
||||||
|
2. **Service role:** pode ler + escrever registros (usado pelo app com JWT service_role)
|
||||||
|
|
||||||
|
### Chave anon (pública)
|
||||||
|
Usada para consultasreadonly pelo frontend.
|
||||||
|
|
||||||
|
### Chave service_role
|
||||||
|
Usada pelo app Tauri para INSERT/UPDATE via plugin `supabase.rs`. Esta chave **nunca deve ser exposta publicamente** — está embedada no binário compilado.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Endpoints da API REST
|
||||||
|
|
||||||
|
Base: `https://supabase.ofrangao.com.br/rest/v1/`
|
||||||
|
|
||||||
|
| Método | Endpoint | Uso |
|
||||||
|
|--------|----------|-----|
|
||||||
|
| `GET` | `/fechamentos_web?loja=eq.uniao&data=eq.2026-06-19&select=*` | Lista fechamentos |
|
||||||
|
| `GET` | `/fechamentos_web?id_fechamento=eq.uniao_2026-06-19_filipe` | Busca por chave |
|
||||||
|
| `POST` | `/fechamentos_web` | Cria/atualiza (upsert via `Prefer: resolution=merge-duplicates`) |
|
||||||
|
| `PATCH` | `/fechamentos_web?id_fechamento=eq.uniao_2026-06-19_filipe` | Atualiza campos |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Header de autenticação
|
||||||
|
|
||||||
|
```http
|
||||||
|
Authorization: Bearer {SUPABASE_SERVICE_ROLE_JWT}
|
||||||
|
apikey: {SUPABASE_ANON_KEY}
|
||||||
|
Content-Type: application/json
|
||||||
|
Prefer: resolution=merge-duplicates (para upsert no POST)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Supabase — Configuração do projeto
|
||||||
|
|
||||||
|
- **Projeto:** O Frangão
|
||||||
|
- **Host:** `https://supabase.ofrangao.com.br`
|
||||||
|
- **Banco:** PostgreSQL 15+
|
||||||
|
- **API:** REST via PostgREST
|
||||||
|
- **Auth:** JWT (anon key + service role key)
|
||||||
|
|
||||||
|
O app busca a service_role key via `config.json` local (em `%LOCALAPPDATA%/FechamentoCaixa/config.json`). O fallback é a chave embedada no binário.
|
||||||
|
|
||||||
|
```json
|
||||||
|
// config.json (opcional — sobrepõe a embedada)
|
||||||
|
{
|
||||||
|
"SUPABASE_ANON_KEY": "sb_publishable_...",
|
||||||
|
"SUPABASE_SERVICE_KEY": "sb_secret_..."
|
||||||
|
}
|
||||||
|
```
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Generated
-35
@@ -900,14 +900,12 @@ dependencies = [
|
|||||||
"rusqlite",
|
"rusqlite",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"serde_yaml",
|
|
||||||
"tauri",
|
"tauri",
|
||||||
"tauri-build",
|
"tauri-build",
|
||||||
"tauri-plugin-dialog",
|
"tauri-plugin-dialog",
|
||||||
"tauri-plugin-fs",
|
"tauri-plugin-fs",
|
||||||
"tauri-plugin-shell",
|
"tauri-plugin-shell",
|
||||||
"thiserror 2.0.18",
|
"thiserror 2.0.18",
|
||||||
"tokio",
|
|
||||||
"uuid",
|
"uuid",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -3281,19 +3279,6 @@ dependencies = [
|
|||||||
"syn 2.0.118",
|
"syn 2.0.118",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "serde_yaml"
|
|
||||||
version = "0.9.34+deprecated"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47"
|
|
||||||
dependencies = [
|
|
||||||
"indexmap 2.14.0",
|
|
||||||
"itoa",
|
|
||||||
"ryu",
|
|
||||||
"serde",
|
|
||||||
"unsafe-libyaml",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serialize-to-javascript"
|
name = "serialize-to-javascript"
|
||||||
version = "0.1.2"
|
version = "0.1.2"
|
||||||
@@ -4070,25 +4055,11 @@ dependencies = [
|
|||||||
"bytes",
|
"bytes",
|
||||||
"libc",
|
"libc",
|
||||||
"mio",
|
"mio",
|
||||||
"parking_lot",
|
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
"signal-hook-registry",
|
|
||||||
"socket2",
|
"socket2",
|
||||||
"tokio-macros",
|
|
||||||
"windows-sys 0.61.2",
|
"windows-sys 0.61.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "tokio-macros"
|
|
||||||
version = "2.7.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496"
|
|
||||||
dependencies = [
|
|
||||||
"proc-macro2",
|
|
||||||
"quote",
|
|
||||||
"syn 2.0.118",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tokio-native-tls"
|
name = "tokio-native-tls"
|
||||||
version = "0.3.1"
|
version = "0.3.1"
|
||||||
@@ -4399,12 +4370,6 @@ version = "1.13.3"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8"
|
checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "unsafe-libyaml"
|
|
||||||
version = "0.2.11"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "untrusted"
|
name = "untrusted"
|
||||||
version = "0.9.0"
|
version = "0.9.0"
|
||||||
|
|||||||
@@ -15,8 +15,6 @@ serde = { version = "1", features = ["derive"] }
|
|||||||
serde_json = "1"
|
serde_json = "1"
|
||||||
rusqlite = { version = "0.32", features = ["bundled"] }
|
rusqlite = { version = "0.32", features = ["bundled"] }
|
||||||
reqwest = { version = "0.12", features = ["json", "blocking"] }
|
reqwest = { version = "0.12", features = ["json", "blocking"] }
|
||||||
tokio = { version = "1", features = ["full"] }
|
|
||||||
serde_yaml = "0.9"
|
|
||||||
uuid = { version = "1", features = ["v4"] }
|
uuid = { version = "1", features = ["v4"] }
|
||||||
chrono = { version = "0.4", features = ["serde"] }
|
chrono = { version = "0.4", features = ["serde"] }
|
||||||
log = "0.4"
|
log = "0.4"
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
{
|
||||||
|
"identifier": "default",
|
||||||
|
"description": "Default capabilities for Fechamento de Caixa",
|
||||||
|
"windows": ["main"],
|
||||||
|
"permissions": [
|
||||||
|
"core:default",
|
||||||
|
"shell:allow-open",
|
||||||
|
"dialog:allow-message",
|
||||||
|
"dialog:allow-confirm",
|
||||||
|
"dialog:allow-ask",
|
||||||
|
"dialog:allow-save",
|
||||||
|
"dialog:allow-open",
|
||||||
|
"fs:default",
|
||||||
|
"fs:allow-app-read",
|
||||||
|
"fs:allow-app-write",
|
||||||
|
"fs:allow-appdata-read",
|
||||||
|
"fs:allow-appdata-write",
|
||||||
|
"fs:allow-appconfig-read",
|
||||||
|
"fs:allow-appconfig-write",
|
||||||
|
"fs:allow-applocaldata-read",
|
||||||
|
"fs:allow-applocaldata-write"
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -36,10 +36,10 @@ fn load_config() -> (String, String) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Fallback: tenta variáveis de ambiente
|
// Fallback 3: chaves embedadas no binário (funciona em qualquer PC sem config)
|
||||||
(
|
(
|
||||||
std::env::var("SUPABASE_ANON_KEY").unwrap_or_default(),
|
"sb_publishable_NCUGwstslOvVt5JtGxKB9A_p-BERG57".to_string(),
|
||||||
std::env::var("SUPABASE_SERVICE_KEY").unwrap_or_default(),
|
"sb_secret_T6eHIKJDGaH53HMTG6UUKa_6ou6KfEW".to_string(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -96,6 +96,9 @@ fn main() {
|
|||||||
plugins::supabase::sb_carregar_rascunho,
|
plugins::supabase::sb_carregar_rascunho,
|
||||||
plugins::supabase::sb_listar_recentes,
|
plugins::supabase::sb_listar_recentes,
|
||||||
plugins::supabase::sb_buscar_por_id,
|
plugins::supabase::sb_buscar_por_id,
|
||||||
|
plugins::supabase::sb_buscar_por_uuid,
|
||||||
|
plugins::supabase::sb_buscar_por_id_fechamento,
|
||||||
|
plugins::supabase::sb_atualizar_fechamento,
|
||||||
plugins::supabase::sb_salvar_listas,
|
plugins::supabase::sb_salvar_listas,
|
||||||
plugins::supabase::sb_carregar_listas,
|
plugins::supabase::sb_carregar_listas,
|
||||||
plugins::supabase::sb_online,
|
plugins::supabase::sb_online,
|
||||||
@@ -108,6 +111,7 @@ fn main() {
|
|||||||
plugins::printer::ativar_modo_teste_impressao,
|
plugins::printer::ativar_modo_teste_impressao,
|
||||||
// App
|
// App
|
||||||
plugins::app::get_loja,
|
plugins::app::get_loja,
|
||||||
|
plugins::app::get_loja_fixa,
|
||||||
plugins::app::set_loja,
|
plugins::app::set_loja,
|
||||||
plugins::app::get_config,
|
plugins::app::get_config,
|
||||||
plugins::app::set_config,
|
plugins::app::set_config,
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
//! Plugin de configuração da aplicação (loja, preferências)
|
//! Plugin de configuração da aplicação (loja, preferências)
|
||||||
//! Persistido em config.json ao lado do banco SQLite.
|
//! Persistido em config.json ao lado do banco SQLite.
|
||||||
|
//!
|
||||||
|
//! LOJA_FIXA: definido em tempo de build via env DOBRADO_LOJA (Uniao | Alianca).
|
||||||
|
//! Quando definido, a loja é travada e o seletor do formulário fica desabilitado.
|
||||||
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::fs;
|
use std::fs;
|
||||||
@@ -7,6 +10,9 @@ use std::path::PathBuf;
|
|||||||
use std::sync::Mutex;
|
use std::sync::Mutex;
|
||||||
use tauri::State;
|
use tauri::State;
|
||||||
|
|
||||||
|
/// Indica se a loja foi fixada em tempo de build.
|
||||||
|
pub const LOJA_FIXA: Option<&'static str> = option_env!("DOBRADO_LOJA");
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||||
pub struct AppConfig {
|
pub struct AppConfig {
|
||||||
pub loja: String,
|
pub loja: String,
|
||||||
@@ -15,17 +21,28 @@ pub struct AppConfig {
|
|||||||
pub turno_default: String,
|
pub turno_default: String,
|
||||||
pub sync_automatico: bool,
|
pub sync_automatico: bool,
|
||||||
pub auto_open_printer: bool,
|
pub auto_open_printer: bool,
|
||||||
|
/// Verdadeiro quando a build tem loja fixa (não permite trocar no formulário)
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub loja_fixa: Option<bool>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AppConfig {
|
||||||
|
/// Loja padrão: o env DOBRADO_LOJA se existir, senão "Uniao"
|
||||||
|
pub fn default_loja() -> String {
|
||||||
|
LOJA_FIXA.unwrap_or("Uniao").to_string()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for AppConfig {
|
impl Default for AppConfig {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
loja: "Uniao".to_string(),
|
loja: Self::default_loja(),
|
||||||
pdv_nome: "PDV-01".to_string(),
|
pdv_nome: "PDV-01".to_string(),
|
||||||
operador_default: String::new(),
|
operador_default: String::new(),
|
||||||
turno_default: String::new(),
|
turno_default: String::new(),
|
||||||
sync_automatico: true,
|
sync_automatico: true,
|
||||||
auto_open_printer: false,
|
auto_open_printer: false,
|
||||||
|
loja_fixa: LOJA_FIXA.map(|_| true),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -65,8 +82,17 @@ pub fn get_loja(state: State<'_, AppState>) -> String {
|
|||||||
state.config.lock().unwrap().loja.clone()
|
state.config.lock().unwrap().loja.clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub fn get_loja_fixa() -> Option<String> {
|
||||||
|
LOJA_FIXA.map(|s| s.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub fn set_loja(state: State<'_, AppState>, loja: String) -> Result<(), String> {
|
pub fn set_loja(state: State<'_, AppState>, loja: String) -> Result<(), String> {
|
||||||
|
// Se loja_fixa está ativa, não permite alterar
|
||||||
|
if LOJA_FIXA.is_some() {
|
||||||
|
return Err("Loja fixa em tempo de build — não é possível trocar.".to_string());
|
||||||
|
}
|
||||||
state.config.lock().unwrap().loja = loja;
|
state.config.lock().unwrap().loja = loja;
|
||||||
state.save()
|
state.save()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ pub const LF: &[u8] = b"\x0A";
|
|||||||
/// Cortar papel (parcial)
|
/// Cortar papel (parcial)
|
||||||
pub const CUT: &[u8] = b"\x1D\x56\x01";
|
pub const CUT: &[u8] = b"\x1D\x56\x01";
|
||||||
/// Cortar papel (total)
|
/// Cortar papel (total)
|
||||||
|
#[allow(dead_code)]
|
||||||
pub const CUT_FULL: &[u8] = b"\x1D\x56\x00";
|
pub const CUT_FULL: &[u8] = b"\x1D\x56\x00";
|
||||||
/// Alimentar 3 linhas antes de cortar
|
/// Alimentar 3 linhas antes de cortar
|
||||||
pub const FEED_CUT: &[u8] = b"\x1B\x64\x03";
|
pub const FEED_CUT: &[u8] = b"\x1B\x64\x03";
|
||||||
@@ -17,6 +18,7 @@ pub const BOLD_OFF: &[u8] = b"\x1B\x45\x00";
|
|||||||
/// Alinhamento
|
/// Alinhamento
|
||||||
pub const ALIGN_LEFT: &[u8] = b"\x1B\x61\x00";
|
pub const ALIGN_LEFT: &[u8] = b"\x1B\x61\x00";
|
||||||
pub const ALIGN_CENTER: &[u8] = b"\x1B\x61\x01";
|
pub const ALIGN_CENTER: &[u8] = b"\x1B\x61\x01";
|
||||||
|
#[allow(dead_code)]
|
||||||
pub const ALIGN_RIGHT: &[u8] = b"\x1B\x61\x02";
|
pub const ALIGN_RIGHT: &[u8] = b"\x1B\x61\x02";
|
||||||
/// Fonte normal / dupla
|
/// Fonte normal / dupla
|
||||||
pub const FONT_NORMAL: &[u8] = b"\x1B\x21\x00";
|
pub const FONT_NORMAL: &[u8] = b"\x1B\x21\x00";
|
||||||
@@ -38,6 +40,7 @@ pub fn title(text: &str) -> Vec<u8> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Monta linha normal (alinhada à esquerda)
|
/// Monta linha normal (alinhada à esquerda)
|
||||||
|
#[allow(dead_code)]
|
||||||
pub fn line(text: &str) -> Vec<u8> {
|
pub fn line(text: &str) -> Vec<u8> {
|
||||||
let mut out = Vec::new();
|
let mut out = Vec::new();
|
||||||
out.extend_from_slice(ALIGN_LEFT);
|
out.extend_from_slice(ALIGN_LEFT);
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ use std::sync::Mutex;
|
|||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
|
||||||
#[derive(Error, Debug)]
|
#[derive(Error, Debug)]
|
||||||
|
#[allow(dead_code)]
|
||||||
pub enum PrinterError {
|
pub enum PrinterError {
|
||||||
#[error("Impressora não encontrada. Verifique se está conectada via USB e ligada.")]
|
#[error("Impressora não encontrada. Verifique se está conectada via USB e ligada.")]
|
||||||
NotFound,
|
NotFound,
|
||||||
@@ -30,6 +31,50 @@ use crate::plugins::escpos;
|
|||||||
|
|
||||||
// ── Struct de dados do recibo ────────────────────────────────────────────────
|
// ── Struct de dados do recibo ────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/// Linha de uma tabela no recibo (despesas, sangrias, vales, etc.)
|
||||||
|
#[derive(Debug, Serialize, Deserialize, Clone, Default)]
|
||||||
|
pub struct ReciboLinha {
|
||||||
|
#[serde(default)]
|
||||||
|
pub desc: Option<String>,
|
||||||
|
#[serde(rename = "hora", default)]
|
||||||
|
pub hora: Option<String>,
|
||||||
|
#[serde(rename = "nome", default)]
|
||||||
|
pub nome: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub retirada: Option<f64>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub valor: Option<f64>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub obs: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub gerente: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub numero: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub motivo: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ReciboLinha {
|
||||||
|
fn fmt_v(&self) -> String {
|
||||||
|
let v = self.retirada.or(self.valor).unwrap_or(0.0);
|
||||||
|
format!("R$ {:.2}", v).replace('.', ",")
|
||||||
|
}
|
||||||
|
fn label(&self) -> String {
|
||||||
|
self.desc
|
||||||
|
.clone()
|
||||||
|
.or(self.nome.clone())
|
||||||
|
.or(self.hora.clone().map(|h| format!("{}h", h)))
|
||||||
|
.unwrap_or_default()
|
||||||
|
}
|
||||||
|
fn obs(&self) -> String {
|
||||||
|
self.gerente
|
||||||
|
.clone()
|
||||||
|
.or(self.obs.clone())
|
||||||
|
.or(self.motivo.clone())
|
||||||
|
.unwrap_or_default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||||
pub struct ReciboData {
|
pub struct ReciboData {
|
||||||
pub loja: String,
|
pub loja: String,
|
||||||
@@ -37,19 +82,55 @@ pub struct ReciboData {
|
|||||||
pub operador: String,
|
pub operador: String,
|
||||||
pub turno: String,
|
pub turno: String,
|
||||||
pub saldo_troco: f64,
|
pub saldo_troco: f64,
|
||||||
|
/// Valor contado no fechamento (dinheiro + cartões + receber)
|
||||||
pub fechamento: f64,
|
pub fechamento: f64,
|
||||||
|
/// Saldo esperado = total operações − sangrias − despesas
|
||||||
pub saldo_esperado: f64,
|
pub saldo_esperado: f64,
|
||||||
|
/// Diferença = fechamento − saldo_esperado
|
||||||
pub diferenca: f64,
|
pub diferenca: f64,
|
||||||
pub sangrias: f64,
|
/// Total do sistema (troco + crédito + débito + alimentação + voucher + pixcnpj + vales + receber)
|
||||||
pub despesas: f64,
|
#[serde(default)]
|
||||||
pub vales: f64,
|
pub sys_total: f64,
|
||||||
pub areceber: f64,
|
/// Soma de todas operações creditadas (crédito + débito + alimentação + voucher + pixcnpj) — usado no BLACK section como TOTAL
|
||||||
|
#[serde(default)]
|
||||||
|
pub total_operado: f64,
|
||||||
|
// Totais de cartões
|
||||||
pub credito: f64,
|
pub credito: f64,
|
||||||
pub debito: f64,
|
pub debito: f64,
|
||||||
pub alimentacao: f64,
|
pub alimentacao: f64,
|
||||||
pub voucher: f64,
|
pub voucher: f64,
|
||||||
|
// Totais diversos (mantidos para compatibilidade com JSON antigo)
|
||||||
|
#[serde(default)]
|
||||||
|
pub sangrias: f64,
|
||||||
|
#[serde(default)]
|
||||||
|
pub despesas: f64,
|
||||||
|
#[serde(default)]
|
||||||
|
pub vales: f64,
|
||||||
|
#[serde(default)]
|
||||||
|
pub areceber: f64,
|
||||||
|
#[serde(default)]
|
||||||
pub pixcnpj: f64,
|
pub pixcnpj: f64,
|
||||||
|
#[serde(default)]
|
||||||
pub cancelamentos: f64,
|
pub cancelamentos: f64,
|
||||||
|
// Arrays para linhas detalhadas (recebidos do frontend)
|
||||||
|
#[serde(default)]
|
||||||
|
pub sangrias_arr: Vec<ReciboLinha>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub despesas_arr: Vec<ReciboLinha>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub pixcnpj_arr: Vec<ReciboLinha>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub vales_arr: Vec<ReciboLinha>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub receber_arr: Vec<ReciboLinha>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub cancelamentos_arr: Vec<ReciboLinha>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub observacoes: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub clientes: i32,
|
||||||
|
#[serde(default)]
|
||||||
|
pub frango: i32,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ReciboData {
|
impl ReciboData {
|
||||||
@@ -58,6 +139,7 @@ impl ReciboData {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Gera todos os bytes ESC/POS do recibo 80mm
|
/// Gera todos os bytes ESC/POS do recibo 80mm
|
||||||
|
/// Layout: Header → SANGRIAS → DESPESAS → PIX CNPJ → VALES → A RECEBER → CANCELAMENTOS → CARTÕES → TROCO/Saldo Caixa → SALDO ESPERADO → DIFERENÇA
|
||||||
pub fn to_escpos(&self) -> Vec<u8> {
|
pub fn to_escpos(&self) -> Vec<u8> {
|
||||||
let mut out = Vec::new();
|
let mut out = Vec::new();
|
||||||
|
|
||||||
@@ -77,40 +159,193 @@ impl ReciboData {
|
|||||||
out.extend(escpos::kv("Turno:", &self.turno));
|
out.extend(escpos::kv("Turno:", &self.turno));
|
||||||
out.extend(escpos::divider());
|
out.extend(escpos::divider());
|
||||||
|
|
||||||
// Entradas
|
// ── SANGRIAS ──
|
||||||
out.extend(escpos::line_bold("ENTRADAS"));
|
if !self.sangrias_arr.is_empty() {
|
||||||
out.extend(escpos::kv("Saldo Troco:", &self.fmt_money(self.saldo_troco)));
|
out.extend(escpos::line_bold("SANGRIAS"));
|
||||||
out.extend(escpos::kv("Fechamento:", &self.fmt_money(self.fechamento)));
|
for linha in &self.sangrias_arr {
|
||||||
out.extend(escpos::kv("Saldo Esperado:", &self.fmt_money(self.saldo_esperado)));
|
if linha.retirada.unwrap_or(0.0) > 0.0 || linha.valor.unwrap_or(0.0) > 0.0 {
|
||||||
|
let label = linha.label();
|
||||||
|
let obs = linha.obs();
|
||||||
|
out.extend(escpos::kv(&label, &linha.fmt_v()));
|
||||||
|
if !obs.is_empty() {
|
||||||
|
out.extend(escpos::line_center(&format!(" {}", obs)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Total sangrias
|
||||||
|
let total_sangrias: f64 = self.sangrias_arr.iter()
|
||||||
|
.map(|l| l.retirada.unwrap_or(0.0) + l.valor.unwrap_or(0.0))
|
||||||
|
.sum();
|
||||||
|
if total_sangrias > 0.0 {
|
||||||
|
out.extend(escpos::kv("Total:", &self.fmt_money(total_sangrias)));
|
||||||
|
}
|
||||||
out.extend(escpos::divider());
|
out.extend(escpos::divider());
|
||||||
|
}
|
||||||
|
|
||||||
// Débitos
|
// ── DESPESAS ──
|
||||||
out.extend(escpos::line_bold("DEBITOS"));
|
if !self.despesas_arr.is_empty() {
|
||||||
out.extend(escpos::kv("Sangrias:", &self.fmt_money(self.sangrias)));
|
out.extend(escpos::line_bold("DESPESAS"));
|
||||||
out.extend(escpos::kv("Despesas:", &self.fmt_money(self.despesas)));
|
for linha in &self.despesas_arr {
|
||||||
out.extend(escpos::kv("Vales:", &self.fmt_money(self.vales)));
|
if linha.valor.unwrap_or(0.0) > 0.0 {
|
||||||
out.extend(escpos::kv("A Receber:", &self.fmt_money(self.areceber)));
|
let label = linha.label();
|
||||||
out.extend(escpos::kv("PIX CNPJ:", &self.fmt_money(self.pixcnpj)));
|
let obs = linha.obs();
|
||||||
|
out.extend(escpos::kv(&label, &linha.fmt_v()));
|
||||||
|
if !obs.is_empty() {
|
||||||
|
out.extend(escpos::line_center(&format!(" {}", obs)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Total despesas
|
||||||
|
let total_despesas: f64 = self.despesas_arr.iter()
|
||||||
|
.map(|l| l.valor.unwrap_or(0.0))
|
||||||
|
.sum();
|
||||||
|
out.extend(escpos::kv("Total:", &self.fmt_money(total_despesas)));
|
||||||
out.extend(escpos::divider());
|
out.extend(escpos::divider());
|
||||||
|
}
|
||||||
|
|
||||||
// Cartões
|
// ── PIX CNPJ ──
|
||||||
out.extend(escpos::line_bold("CARTÕES"));
|
if !self.pixcnpj_arr.is_empty() {
|
||||||
|
out.extend(escpos::line_bold("PIX CNPJ"));
|
||||||
|
for linha in &self.pixcnpj_arr {
|
||||||
|
if linha.valor.unwrap_or(0.0) > 0.0 {
|
||||||
|
let label = linha.nome.clone().unwrap_or_else(|| "PIX CNPJ".to_string());
|
||||||
|
out.extend(escpos::kv(&label, &linha.fmt_v()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Total PIX CNPJ
|
||||||
|
let total_pixcnpj: f64 = self.pixcnpj_arr.iter()
|
||||||
|
.map(|l| l.valor.unwrap_or(0.0))
|
||||||
|
.sum();
|
||||||
|
if total_pixcnpj > 0.0 {
|
||||||
|
out.extend(escpos::kv("Total:", &self.fmt_money(total_pixcnpj)));
|
||||||
|
}
|
||||||
|
out.extend(escpos::divider());
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── VALES ──
|
||||||
|
if !self.vales_arr.is_empty() {
|
||||||
|
out.extend(escpos::line_bold("VALES"));
|
||||||
|
for linha in &self.vales_arr {
|
||||||
|
if linha.valor.unwrap_or(0.0) > 0.0 {
|
||||||
|
let label = linha.label();
|
||||||
|
let obs = linha.obs();
|
||||||
|
out.extend(escpos::kv(&label, &linha.fmt_v()));
|
||||||
|
if !obs.is_empty() {
|
||||||
|
out.extend(escpos::line_center(&format!(" {}", obs)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Total vales
|
||||||
|
let total_vales: f64 = self.vales_arr.iter()
|
||||||
|
.map(|l| l.valor.unwrap_or(0.0))
|
||||||
|
.sum();
|
||||||
|
if total_vales > 0.0 {
|
||||||
|
out.extend(escpos::kv("Total:", &self.fmt_money(total_vales)));
|
||||||
|
}
|
||||||
|
out.extend(escpos::divider());
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── A RECEBER ──
|
||||||
|
if !self.receber_arr.is_empty() {
|
||||||
|
out.extend(escpos::line_bold("A RECEBER"));
|
||||||
|
for linha in &self.receber_arr {
|
||||||
|
if linha.valor.unwrap_or(0.0) > 0.0 {
|
||||||
|
let label = linha.label();
|
||||||
|
out.extend(escpos::kv(&label, &linha.fmt_v()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Total a receber
|
||||||
|
let total_receber: f64 = self.receber_arr.iter()
|
||||||
|
.map(|l| l.valor.unwrap_or(0.0))
|
||||||
|
.sum();
|
||||||
|
if total_receber > 0.0 {
|
||||||
|
out.extend(escpos::kv("Total:", &self.fmt_money(total_receber)));
|
||||||
|
}
|
||||||
|
out.extend(escpos::divider());
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── CANCELAMENTOS ──
|
||||||
|
if !self.cancelamentos_arr.is_empty() {
|
||||||
|
out.extend(escpos::line_bold("CANCELAMENTOS"));
|
||||||
|
for linha in &self.cancelamentos_arr {
|
||||||
|
if linha.numero.is_some() || linha.valor.unwrap_or(0.0) > 0.0 {
|
||||||
|
let num = linha.numero.clone().unwrap_or_default();
|
||||||
|
out.extend(escpos::kv(&num, &linha.fmt_v()));
|
||||||
|
if let Some(ref m) = linha.motivo {
|
||||||
|
if !m.is_empty() {
|
||||||
|
out.extend(escpos::line_center(&format!(" {}", m)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Total cancelamentos
|
||||||
|
let total_cancel: f64 = self.cancelamentos_arr.iter()
|
||||||
|
.map(|l| l.valor.unwrap_or(0.0))
|
||||||
|
.sum();
|
||||||
|
if total_cancel > 0.0 {
|
||||||
|
out.extend(escpos::kv("Total:", &self.fmt_money(total_cancel)));
|
||||||
|
}
|
||||||
|
out.extend(escpos::divider());
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── CARTÕES ──
|
||||||
|
out.extend(escpos::line_bold("CARTOES"));
|
||||||
out.extend(escpos::kv("Credito:", &self.fmt_money(self.credito)));
|
out.extend(escpos::kv("Credito:", &self.fmt_money(self.credito)));
|
||||||
out.extend(escpos::kv("Debito:", &self.fmt_money(self.debito)));
|
out.extend(escpos::kv("Debito:", &self.fmt_money(self.debito)));
|
||||||
out.extend(escpos::kv("Alimentacao:", &self.fmt_money(self.alimentacao)));
|
out.extend(escpos::kv("Alimentacao:", &self.fmt_money(self.alimentacao)));
|
||||||
out.extend(escpos::kv("Voucher:", &self.fmt_money(self.voucher)));
|
out.extend(escpos::kv("PIX Cartao:", &self.fmt_money(self.voucher)));
|
||||||
out.extend(escpos::kv("Cancelamentos:", &self.fmt_money(self.cancelamentos)));
|
out.extend(escpos::kv("Cancelamentos:", &self.fmt_money(self.cancelamentos)));
|
||||||
out.extend(escpos::divider());
|
out.extend(escpos::divider());
|
||||||
|
|
||||||
// Diferença
|
// ── TROCO / SALDO DE CAIXA ──
|
||||||
let diff_str = if self.diferenca >= 0.0 {
|
out.extend(escpos::line_bold("SALDO DE CAIXA"));
|
||||||
format!("+{}", self.fmt_money(self.diferenca))
|
out.extend(escpos::kv("Troco (+):", &self.fmt_money(self.saldo_troco)));
|
||||||
|
out.extend(escpos::kv("Credito (+):", &self.fmt_money(self.credito)));
|
||||||
|
out.extend(escpos::kv("Debito (+):", &self.fmt_money(self.debito)));
|
||||||
|
out.extend(escpos::kv("Alimentacao (+):", &self.fmt_money(self.alimentacao)));
|
||||||
|
out.extend(escpos::kv("PIX Cartao (+):", &self.fmt_money(self.voucher)));
|
||||||
|
out.extend(escpos::kv("Vales (+):", &self.fmt_money(self.vales)));
|
||||||
|
out.extend(escpos::kv("A Receber (+):", &self.fmt_money(self.areceber)));
|
||||||
|
out.extend(escpos::kv("PIX CNPJ (+):", &self.fmt_money(self.pixcnpj)));
|
||||||
|
out.extend(escpos::divider());
|
||||||
|
|
||||||
|
// ── TOTAL ──
|
||||||
|
if self.sys_total > 0.0 {
|
||||||
|
out.extend(escpos::line_bold("TOTAL"));
|
||||||
|
out.extend(escpos::kv("Total:", &self.fmt_money(self.sys_total)));
|
||||||
|
out.extend(escpos::divider());
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── SALDO ESPERADO ──
|
||||||
|
out.extend(escpos::line_bold("SALDO ESPERADO"));
|
||||||
|
out.extend(escpos::kv("Esperado:", &self.fmt_money(self.saldo_esperado)));
|
||||||
|
out.extend(escpos::divider());
|
||||||
|
|
||||||
|
// ── DIFERENÇA ──
|
||||||
|
// Diferença = TOTAL SISTEMA - saldo_esperado - cancelamentos
|
||||||
|
out.extend(escpos::line_bold("DIFERENCA"));
|
||||||
|
let diff = self.sys_total - self.saldo_esperado - self.cancelamentos;
|
||||||
|
let diff_str = if diff >= 0.0 {
|
||||||
|
format!("+{}", self.fmt_money(diff))
|
||||||
} else {
|
} else {
|
||||||
self.fmt_money(self.diferenca)
|
self.fmt_money(diff)
|
||||||
};
|
};
|
||||||
out.extend(escpos::line_bold("DIFERENCA:"));
|
|
||||||
out.extend(escpos::title(&diff_str));
|
out.extend(escpos::title(&diff_str));
|
||||||
out.extend(escpos::blank_lines(2));
|
|
||||||
|
// ── CONTADORES ──
|
||||||
|
out.extend(escpos::line_bold("CONTADORES"));
|
||||||
|
out.extend(escpos::kv("Clientes:", &self.clientes.to_string()));
|
||||||
|
out.extend(escpos::kv("Frango Assado:", &self.frango.to_string()));
|
||||||
|
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());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Rodapé
|
// Rodapé
|
||||||
out.extend(escpos::divider());
|
out.extend(escpos::divider());
|
||||||
@@ -122,6 +357,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();
|
||||||
|
|||||||
@@ -7,7 +7,31 @@ use std::time::Duration;
|
|||||||
use tauri::State;
|
use tauri::State;
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
|
||||||
|
/// Remove acentos de uma string (ex: "União" → "Uniao")
|
||||||
|
fn sem_acento(s: &str) -> String {
|
||||||
|
s.replace("ã", "a")
|
||||||
|
.replace("á", "a")
|
||||||
|
.replace("à", "a")
|
||||||
|
.replace("â", "a")
|
||||||
|
.replace("é", "e")
|
||||||
|
.replace("è", "e")
|
||||||
|
.replace("ê", "e")
|
||||||
|
.replace("í", "i")
|
||||||
|
.replace("ì", "i")
|
||||||
|
.replace("î", "i")
|
||||||
|
.replace("õ", "o")
|
||||||
|
.replace("ó", "o")
|
||||||
|
.replace("ò", "o")
|
||||||
|
.replace("ô", "o")
|
||||||
|
.replace("ú", "u")
|
||||||
|
.replace("ù", "u")
|
||||||
|
.replace("û", "u")
|
||||||
|
.replace("ç", "c")
|
||||||
|
.replace("ñ", "n")
|
||||||
|
}
|
||||||
|
|
||||||
#[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),
|
||||||
@@ -68,60 +92,78 @@ impl SupabaseClient {
|
|||||||
|
|
||||||
/// Salva/atualiza um fechamento na tabela `fechamentos_web`.
|
/// Salva/atualiza um fechamento na tabela `fechamentos_web`.
|
||||||
pub fn salvar_fechamento(&self, estado: &serde_json::Value) -> Result<Value, SupabaseError> {
|
pub fn salvar_fechamento(&self, estado: &serde_json::Value) -> Result<Value, SupabaseError> {
|
||||||
let uuid = estado
|
// Normaliza loja e turno para minúsculo (CHECK constraints do Postgres)
|
||||||
.get("uuid")
|
let loja = estado
|
||||||
|
.get("loja")
|
||||||
.and_then(|v| v.as_str())
|
.and_then(|v| v.as_str())
|
||||||
.unwrap_or("");
|
.map(|s| sem_acento(&s.to_lowercase()).replace(" ", "_"))
|
||||||
|
.unwrap_or_else(|| "uniao".to_string());
|
||||||
|
let data = estado.get("data").and_then(|v| v.as_str()).unwrap_or("");
|
||||||
|
let operador = estado.get("operador").and_then(|v| v.as_str()).unwrap_or("");
|
||||||
|
let id_fechamento = format!("{}_{}_{}", loja, data, operador)
|
||||||
|
.to_lowercase()
|
||||||
|
.replace(" ", "_");
|
||||||
|
let turno = estado
|
||||||
|
.get("turno")
|
||||||
|
.and_then(|v| v.as_str())
|
||||||
|
.map(|s| sem_acento(&s.to_lowercase()))
|
||||||
|
.unwrap_or_else(|| "manha".to_string());
|
||||||
|
|
||||||
let payload = serde_json::json!({
|
let payload = serde_json::json!({
|
||||||
"uuid": uuid,
|
"id_fechamento": id_fechamento,
|
||||||
"loja": estado["loja"],
|
"loja": loja,
|
||||||
"data": estado["data"],
|
"data": data,
|
||||||
"operador": estado["operador"],
|
"operador": operador,
|
||||||
"turno": estado["turno"],
|
"turno": turno,
|
||||||
"saldo_troco": estado["saldo_troco"],
|
"saldo_troco": estado.get("saldo_troco").and_then(|v| v.as_f64()).unwrap_or(0.0),
|
||||||
"saldo_esperado": estado["saldo_esperado"],
|
"saldo_credito": estado.get("credito").and_then(|v| v.as_f64()).unwrap_or(0.0),
|
||||||
"fechamento": estado["fechamento"],
|
"saldo_debito": estado.get("debito").and_then(|v| v.as_f64()).unwrap_or(0.0),
|
||||||
|
"saldo_alimentacao": estado.get("alimentacao").and_then(|v| v.as_f64()).unwrap_or(0.0),
|
||||||
|
"saldo_vales": estado.get("vales").and_then(|v| v.as_f64()).unwrap_or(0.0),
|
||||||
|
"saldo_areceber": estado.get("areceber").and_then(|v| v.as_f64()).unwrap_or(0.0),
|
||||||
|
"saldo_pixcnpj": estado.get("pixcnpj").and_then(|v| v.as_f64()).unwrap_or(0.0),
|
||||||
|
"fechamento_dinheiro": estado.get("fd").and_then(|v| v.as_f64()).unwrap_or(0.0),
|
||||||
|
"fechamento_cartoes": estado.get("fc").and_then(|v| v.as_f64()).unwrap_or(0.0),
|
||||||
|
"fechamento_areceber": estado.get("fr").and_then(|v| v.as_f64()).unwrap_or(0.0),
|
||||||
|
"total_sangrias": estado.get("total_sangrias").and_then(|v| v.as_f64()).unwrap_or(0.0),
|
||||||
|
"total_despesas": estado.get("total_despesas").and_then(|v| v.as_f64()).unwrap_or(0.0),
|
||||||
|
"total_cancelamentos": estado.get("total_cancelamentos").and_then(|v| v.as_f64()).unwrap_or(0.0),
|
||||||
|
"total_vales": estado.get("total_vales").and_then(|v| v.as_f64()).unwrap_or(0.0),
|
||||||
|
"total_pixcnpj": estado.get("total_pixcnpj").and_then(|v| v.as_f64()).unwrap_or(0.0),
|
||||||
|
"total_areceber": estado.get("total_areceber").and_then(|v| v.as_f64()).unwrap_or(0.0),
|
||||||
|
"total_cartao_credito": estado.get("total_cartao_credito").and_then(|v| v.as_f64()).unwrap_or(0.0),
|
||||||
|
"total_cartao_debito": estado.get("total_cartao_debito").and_then(|v| v.as_f64()).unwrap_or(0.0),
|
||||||
|
"total_cartao_alimentacao": estado.get("total_cartao_alimentacao").and_then(|v| v.as_f64()).unwrap_or(0.0),
|
||||||
|
"total_cartao_pix": estado.get("total_cartao_pix").and_then(|v| v.as_f64()).unwrap_or(0.0),
|
||||||
|
"saldo_esperado": estado.get("saldo_esperado").and_then(|v| v.as_f64()).unwrap_or(0.0),
|
||||||
"dados": estado,
|
"dados": estado,
|
||||||
"observacoes": "rascunho",
|
"observacoes": estado.get("observacoes").and_then(|v| v.as_str()).unwrap_or(""),
|
||||||
|
"clientes": estado.get("clientes").and_then(|v| v.as_i64()).unwrap_or(0),
|
||||||
|
"frango": estado.get("frango").and_then(|v| v.as_i64()).unwrap_or(0),
|
||||||
|
"vendas": estado.get("vendas").and_then(|v| v.as_f64()).unwrap_or(0.0),
|
||||||
});
|
});
|
||||||
|
|
||||||
let _url = format!(
|
// Primeiro tenta INSERT (upsert via Prefer)
|
||||||
"{}/rest/v1/fechamentos_web?uuid=eq.{}&select=id",
|
|
||||||
self.base_url, uuid
|
|
||||||
);
|
|
||||||
|
|
||||||
// Upsert via POST com Prefer: resolution=merge-duplicates
|
|
||||||
let resp = self
|
let resp = self
|
||||||
.http
|
|
||||||
.post(&format!("{}/rest/v1/rpc/upsert_fechamento", self.base_url))
|
|
||||||
.headers(self.headers(true))
|
|
||||||
.json(&payload)
|
|
||||||
.send()?;
|
|
||||||
|
|
||||||
if resp.status().is_success() {
|
|
||||||
Ok(serde_json::json!({ "ok": true, "uuid": uuid }))
|
|
||||||
} else {
|
|
||||||
let body = resp.text().unwrap_or_default();
|
|
||||||
// Fallback: tenta POST direto na tabela
|
|
||||||
let resp2 = self
|
|
||||||
.http
|
.http
|
||||||
.post(&format!("{}/rest/v1/fechamentos_web", self.base_url))
|
.post(&format!("{}/rest/v1/fechamentos_web", self.base_url))
|
||||||
.headers(self.headers(true))
|
.headers(self.headers(true))
|
||||||
.header("Prefer", "resolution=merge-duplicates")
|
.header("Prefer", "resolution=merge-duplicates")
|
||||||
.json(&payload)
|
.json(&payload)
|
||||||
.send()?;
|
.send()?;
|
||||||
if resp2.status().is_success() {
|
|
||||||
Ok(serde_json::json!({ "ok": true, "uuid": uuid }))
|
let status = resp.status();
|
||||||
|
if status.is_success() || status.as_u16() == 409 {
|
||||||
|
Ok(serde_json::json!({ "ok": true, "id_fechamento": id_fechamento }))
|
||||||
} else {
|
} else {
|
||||||
|
let body = resp.text().unwrap_or_default();
|
||||||
Err(SupabaseError::Api(format!(
|
Err(SupabaseError::Api(format!(
|
||||||
"HTTP {}: {}",
|
"HTTP {}: {}",
|
||||||
resp2.status().as_u16(),
|
status.as_u16(),
|
||||||
body
|
body
|
||||||
)))
|
)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
/// Busca o rascunho mais recente para loja+data.
|
/// Busca o rascunho mais recente para loja+data.
|
||||||
pub fn carregar_rascunho(&self, loja: &str, data: &str) -> Result<Option<Value>, SupabaseError> {
|
pub fn carregar_rascunho(&self, loja: &str, data: &str) -> Result<Option<Value>, SupabaseError> {
|
||||||
@@ -152,12 +194,127 @@ impl SupabaseClient {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Lista fechamentos recentes de uma loja.
|
/// Busca um fechamento pelo campo "id" (UUID externo).
|
||||||
pub fn listar_recentes(&self, loja: &str, limite: i64) -> Result<Vec<Value>, SupabaseError> {
|
pub fn buscar_por_uuid(&self, uuid: &str) -> Result<Option<Value>, SupabaseError> {
|
||||||
let url = format!(
|
let url = format!(
|
||||||
"{}/rest/v1/fechamentos_web?loja=eq.{}&order=data.desc,atualizado_em.desc&limit={}&select=id,uuid,data,operador,turno,saldo_troco,saldo_esperado,fechamento,sync_status,observacoes,atualizado_em",
|
"{}/rest/v1/fechamentos_web?id=eq.{}&select=*",
|
||||||
|
self.base_url, uuid
|
||||||
|
);
|
||||||
|
|
||||||
|
let resp = self
|
||||||
|
.http
|
||||||
|
.get(&url)
|
||||||
|
.headers(self.headers(false))
|
||||||
|
.send()?;
|
||||||
|
|
||||||
|
if resp.status().is_success() {
|
||||||
|
let arr: Vec<Value> = resp.json()?;
|
||||||
|
Ok(arr.into_iter().next())
|
||||||
|
} else {
|
||||||
|
let status = resp.status();
|
||||||
|
let body = resp.text().unwrap_or_default();
|
||||||
|
Err(SupabaseError::Api(format!(
|
||||||
|
"HTTP {}: {}",
|
||||||
|
status.as_u16(),
|
||||||
|
body
|
||||||
|
)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Busca um fechamento pelo id_fechamento (ex: uniao_2026-06-24_silanea).
|
||||||
|
pub fn buscar_por_id_fechamento(&self, id_fechamento: &str) -> Result<Option<Value>, SupabaseError> {
|
||||||
|
let url = format!(
|
||||||
|
"{}/rest/v1/fechamentos_web?id_fechamento=eq.{}&select=*",
|
||||||
|
self.base_url, urlencoding::encode(id_fechamento)
|
||||||
|
);
|
||||||
|
|
||||||
|
let resp = self
|
||||||
|
.http
|
||||||
|
.get(&url)
|
||||||
|
.headers(self.headers(false))
|
||||||
|
.send()?;
|
||||||
|
|
||||||
|
if resp.status().is_success() {
|
||||||
|
let arr: Vec<Value> = resp.json()?;
|
||||||
|
Ok(arr.into_iter().next())
|
||||||
|
} else {
|
||||||
|
let status = resp.status();
|
||||||
|
let body = resp.text().unwrap_or_default();
|
||||||
|
Err(SupabaseError::Api(format!(
|
||||||
|
"HTTP {}: {}",
|
||||||
|
status.as_u16(),
|
||||||
|
body
|
||||||
|
)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Atualiza campos específicos de um fechamento pelo id_fechamento.
|
||||||
|
pub fn atualizar_fechamento(&self, id_fechamento: &str, updates: &serde_json::Value) -> Result<Value, SupabaseError> {
|
||||||
|
// Campos que o frontend envia mas NÃO existem como colunas na tabela.
|
||||||
|
// Arrays e objetos são guardados no JSONB `dados` — não em colunas escalar.
|
||||||
|
static BLOCKED: &[&str] = &[
|
||||||
|
"id", "uuid", "criado_em", "atualizado_em", "enviado_por",
|
||||||
|
"ip_origem", "user_agent", "id_fechamento",
|
||||||
|
// arrays e objetos que existem dentro do JSONB `dados`
|
||||||
|
"vales", "despesas", "sangrias", "cancelamentos",
|
||||||
|
"pixcnpj", "receber",
|
||||||
|
"cartoes_credito", "cartoes_debito",
|
||||||
|
"cartoes_alimentacao", "cartoes_pix",
|
||||||
|
];
|
||||||
|
|
||||||
|
let map = updates.as_object().cloned().unwrap_or_default();
|
||||||
|
|
||||||
|
// Remove campos bloqueados
|
||||||
|
let filtered: serde_json::Map<String, serde_json::Value> = map
|
||||||
|
.into_iter()
|
||||||
|
.filter(|(k, _)| !BLOCKED.contains(&k.as_str()))
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
if filtered.is_empty() {
|
||||||
|
return Err(SupabaseError::Api("Nenhum campo para atualizar".into()));
|
||||||
|
}
|
||||||
|
|
||||||
|
let url = format!(
|
||||||
|
"{}/rest/v1/fechamentos_web?id_fechamento=eq.{}",
|
||||||
self.base_url,
|
self.base_url,
|
||||||
urlencoding::encode(loja),
|
urlencoding::encode(id_fechamento)
|
||||||
|
);
|
||||||
|
|
||||||
|
log::info!("[PATCH] url={} body={:#?}", url, filtered);
|
||||||
|
|
||||||
|
let resp = self
|
||||||
|
.http
|
||||||
|
.patch(&url)
|
||||||
|
.headers(self.headers(true))
|
||||||
|
.header("Prefer", "return=representation")
|
||||||
|
.json(&filtered)
|
||||||
|
.send()?;
|
||||||
|
|
||||||
|
let status = resp.status();
|
||||||
|
if status.is_success() {
|
||||||
|
// Ler a resposta real do Supabase (retorna o registro atualizado)
|
||||||
|
if let Ok(ret) = resp.json::<serde_json::Value>() {
|
||||||
|
log::info!("[PATCH] sucesso — resposta: {}", ret);
|
||||||
|
} else {
|
||||||
|
log::warn!("[PATCH] sucesso mas não conseguiu ler resposta");
|
||||||
|
}
|
||||||
|
Ok(serde_json::json!({ "ok": true, "id_fechamento": id_fechamento }))
|
||||||
|
} else {
|
||||||
|
let body = resp.text().unwrap_or_default();
|
||||||
|
log::error!("[PATCH] HTTP {} body: {}", status.as_u16(), body);
|
||||||
|
Err(SupabaseError::Api(format!(
|
||||||
|
"HTTP {}: {}",
|
||||||
|
status.as_u16(),
|
||||||
|
body
|
||||||
|
)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn listar_recentes(&self, loja: &str, limite: i64) -> Result<Vec<Value>, SupabaseError> {
|
||||||
|
let loja_normalized = sem_acento(&loja.to_lowercase());
|
||||||
|
let url = format!(
|
||||||
|
"{}/rest/v1/fechamentos_web?loja=eq.{}&order=data.desc,atualizado_em.desc&limit={}&select=id,id_fechamento,data,operador,turno,saldo_troco,saldo_credito,saldo_debito,saldo_alimentacao,saldo_vales,saldo_areceber,saldo_esperado,total_pixcnpj,total_cartao_credito,total_cartao_debito,total_cartao_alimentacao,total_cartao_pix,total_cancelamentos,fechamento_dinheiro,fechamento_cartoes,fechamento_areceber,diferenca,status_diferenca,observacoes,atualizado_em,dados",
|
||||||
|
self.base_url,
|
||||||
|
urlencoding::encode(&loja_normalized),
|
||||||
limite,
|
limite,
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -273,6 +430,48 @@ impl SupabaseClient {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Tenta forçar PostgREST a recarregar schema com novas colunas (vendas, edited_at, edited_by).
|
||||||
|
/// Estratégia: INSERT com novas colunas → se PGRST204, espera 1.5s → retry.
|
||||||
|
/// Fire-and-forget — falha não bloqueia.
|
||||||
|
pub fn run_migration(&self) {
|
||||||
|
let payload = serde_json::json!({
|
||||||
|
"id_fechamento": "_mig_probe_1900",
|
||||||
|
"loja": "_probe_",
|
||||||
|
"data": "1900-01-01",
|
||||||
|
"operador": "_probe_",
|
||||||
|
"vendas": 0,
|
||||||
|
"edited_at": null,
|
||||||
|
"edited_by": null
|
||||||
|
});
|
||||||
|
let r1 = self.http
|
||||||
|
.post(&format!("{}/rest/v1/fechamentos_web", self.base_url))
|
||||||
|
.headers(self.headers(true))
|
||||||
|
.header("Prefer", "resolution=merge-duplicates")
|
||||||
|
.json(&payload)
|
||||||
|
.send();
|
||||||
|
if let Ok(resp) = r1 {
|
||||||
|
if resp.status().as_u16() == 400 {
|
||||||
|
std::thread::sleep(std::time::Duration::from_millis(1500));
|
||||||
|
let _ = self.http
|
||||||
|
.post(&format!("{}/rest/v1/fechamentos_web", self.base_url))
|
||||||
|
.headers(self.headers(true))
|
||||||
|
.header("Prefer", "resolution=merge-duplicates")
|
||||||
|
.json(&serde_json::json!({
|
||||||
|
"id_fechamento": "_mig_probe_1900",
|
||||||
|
"vendas": 0,
|
||||||
|
"edited_at": null,
|
||||||
|
"edited_by": null
|
||||||
|
}))
|
||||||
|
.send();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Limpa probe
|
||||||
|
let _ = self.http
|
||||||
|
.delete(&format!("{}/rest/v1/fechamentos_web?id_fechamento=eq._mig_probe_1900", self.base_url))
|
||||||
|
.headers(self.headers(true))
|
||||||
|
.send();
|
||||||
|
}
|
||||||
|
|
||||||
/// Verifica se está online.
|
/// Verifica se está online.
|
||||||
pub fn health_check(&self) -> bool {
|
pub fn health_check(&self) -> bool {
|
||||||
let url = format!("{}/rest/v1/?limit=0", self.base_url);
|
let url = format!("{}/rest/v1/?limit=0", self.base_url);
|
||||||
@@ -321,6 +520,32 @@ pub fn sb_buscar_por_id(
|
|||||||
sb.buscar_por_id(id)
|
sb.buscar_por_id(id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub fn sb_buscar_por_uuid(
|
||||||
|
sb: State<'_, SupabaseClient>,
|
||||||
|
uuid: String,
|
||||||
|
) -> Result<Option<serde_json::Value>, SupabaseError> {
|
||||||
|
sb.buscar_por_uuid(&uuid)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Busca um fechamento pelo id_fechamento (ex: uniao_2026-06-24_silanea).
|
||||||
|
#[tauri::command]
|
||||||
|
pub fn sb_buscar_por_id_fechamento(
|
||||||
|
sb: State<'_, SupabaseClient>,
|
||||||
|
id_fechamento: String,
|
||||||
|
) -> Result<Option<serde_json::Value>, SupabaseError> {
|
||||||
|
sb.buscar_por_id_fechamento(&id_fechamento)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command(rename_all = "snake_case")]
|
||||||
|
pub fn sb_atualizar_fechamento(
|
||||||
|
sb: State<'_, SupabaseClient>,
|
||||||
|
id_fechamento: String,
|
||||||
|
updates: serde_json::Value,
|
||||||
|
) -> Result<serde_json::Value, SupabaseError> {
|
||||||
|
sb.atualizar_fechamento(&id_fechamento, &updates)
|
||||||
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub fn sb_salvar_listas(
|
pub fn sb_salvar_listas(
|
||||||
sb: State<'_, SupabaseClient>,
|
sb: State<'_, SupabaseClient>,
|
||||||
|
|||||||
@@ -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/*": "./"
|
||||||
|
|||||||
+2351
-264
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user