Compare commits
15 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7c036b916f | |||
| c58f01d51b | |||
| acb90f093d | |||
| 6eb08b6bd4 | |||
| 5a0126417c | |||
| 733519e1d3 | |||
| 6440b262cc | |||
| 420e104df7 | |||
| b04e5acfd6 | |||
| 65c444ad09 | |||
| 2bfbf29ccb | |||
| cbe2c22440 | |||
| 1dc278dbbd | |||
| c2c9a29247 | |||
| 659d9779b3 |
@@ -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).
|
||||||
@@ -1,140 +1,192 @@
|
|||||||
# Fechamento de Caixa — O Frangão
|
# Fechamento de Caixa — O Frangão
|
||||||
|
|
||||||
App desktop nativo para fechamento de caixa com impressão em impressora térmica 80mm.
|
## O que é
|
||||||
|
|
||||||
## Stack
|
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.
|
||||||
|
|
||||||
- **Backend:** Tauri 2 + Rust
|
## Ideia central
|
||||||
- **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
|
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/
|
fechamento-caixa/
|
||||||
├── src/
|
├── src/
|
||||||
│ └── index.html # Frontend completo (HTML + CSS + JS inline)
|
│ └── index.html ← Frontend completo (HTML + CSS + JS)
|
||||||
├── src-tauri/
|
├── src-tauri/
|
||||||
│ ├── Cargo.toml # Dependências Rust
|
│ ├── src/
|
||||||
│ ├── tauri.conf.json # Config do app Tauri
|
│ │ ├── main.rs ← Entry point + plugin initialization
|
||||||
│ └── src/
|
│ │ └── plugins/
|
||||||
│ ├── main.rs # Entry point — registra comandos Tauri
|
│ │ ├── app.rs ← Config da app (loja, PDV, sync)
|
||||||
│ └── plugins/
|
│ │ ├── storage.rs ← SQLite local
|
||||||
│ ├── supabase.rs # Integração Supabase (REST API)
|
│ │ ├── supabase.rs ← Sync com Supabase
|
||||||
│ ├── printer.rs # Geração ESC/POS + envio USB
|
│ │ ├── printer.rs ← Impressão
|
||||||
│ ├── storage.rs # SQLite local
|
│ │ └── escpos.rs ← Helpers ESCPOS
|
||||||
│ ├── app.rs # Config da app (loja, operador default)
|
│ └── tauri.conf.json ← Config Tauri (janela, bundle, etc.)
|
||||||
│ └── escpos.rs # Helpers ESC/POS (INIT, CUT, divider, etc.)
|
├── SPEC.md ← Este arquivo
|
||||||
└── README.md
|
├── 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
|
||||||
```
|
```
|
||||||
|
|
||||||
## Pré-requisitos
|
---
|
||||||
|
|
||||||
- Rust 1.70+ (`rustup install stable`)
|
## Builds — 3 versões do app
|
||||||
- Node.js 18+ (para build do frontend Tauri)
|
|
||||||
- Windows 10/11 (impressão USB só funciona no Windows)
|
|
||||||
|
|
||||||
## Setup
|
O mesmo código gera 3 binários diferentes conforme a variável de ambiente `DOBRADO_LOJA`:
|
||||||
|
|
||||||
### 1. Clonar o repositório
|
## Downloads
|
||||||
|
|
||||||
```bash
|
| Binário | Loja | Download |
|
||||||
git clone https://git.ofrangao.com.br/filipe/fechamento-caixa.git
|
|---------|------|----------|
|
||||||
cd fechamento-caixa
|
| `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) |
|
||||||
|
|
||||||
### 2. Configurar chaves do Supabase
|
Quando a loja é fixa, o seletor de loja no formulário é **escondido via JavaScript** e tentativas de mudar via API retornam erro.
|
||||||
|
|
||||||
Crie o arquivo `config.json` em `%LOCALAPPDATA%\FechamentoCaixa\config.json` (Windows):
|
---
|
||||||
|
|
||||||
```json
|
## Fluxo de um fechamento
|
||||||
{
|
|
||||||
"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]
|
1. Operador abre o app
|
||||||
│
|
↓
|
||||||
▼ (auto-save a cada 1.5s)
|
2. Seleciona Data, Operador, Turno, Loja (se não for fixa)
|
||||||
SQLite local (fechamento.db)
|
↓
|
||||||
│
|
3. Preenche os campos:
|
||||||
├── [Botão Enviar] ──► Supabase (fechamentos_web)
|
- Saldo de Troy
|
||||||
│
|
- Cartões (crédito, débito, alimentação, PIX) — um valor por vez
|
||||||
└── [Botão Recibo] ──► printer.rs ──► to_escpos() ──► USB
|
- Despesas (descrição + valor)
|
||||||
│
|
- Sangrias (hora + gerente + valor)
|
||||||
├── Sucesso: imprime
|
- PIX CNPJ (nome + valor)
|
||||||
└── Falha: openPrintPreview() ──► iframe ──► window.print()
|
- 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
|
||||||
```
|
```
|
||||||
|
|
||||||
## Campos do formulário
|
---
|
||||||
|
|
||||||
| Campo | Tabela Supabase | Notas |
|
## Tauri — o runtime desktop
|
||||||
|-------|----------------|-------|
|
|
||||||
| 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
|
O app roda como **Tauri 2**, um runtime que embute um navegador WebView2 (Windows) dentro de um processo Rust.
|
||||||
|
|
||||||
- Impressão USB só funciona no Windows
|
### O que o Tauri fornece
|
||||||
- `service_role_key` no cliente desktop é risco de segurança — usar apenas anon_key + RLS
|
|
||||||
- Sem testes automatizados
|
|
||||||
|
|
||||||
## Version History
|
| 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 |
|
||||||
|
|
||||||
- **v1.2.7** — 2026-06-24: 4 bugs críticos concertados (impressão, enviar, dia anterior, localStorage)
|
### Comandos Tauri (Rust → JS)
|
||||||
- **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)
|
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_..."
|
||||||
|
}
|
||||||
|
```
|
||||||
+27
-16
@@ -19,7 +19,7 @@ fn data_dir_path() -> PathBuf {
|
|||||||
.join("FechamentoCaixa")
|
.join("FechamentoCaixa")
|
||||||
}
|
}
|
||||||
|
|
||||||
fn load_config() -> (String, String) {
|
fn load_config() -> Result<(String, String), String> {
|
||||||
let cfg_path = data_dir_path().join("config.json");
|
let cfg_path = data_dir_path().join("config.json");
|
||||||
if let Ok(content) = fs::read_to_string(&cfg_path) {
|
if let Ok(content) = fs::read_to_string(&cfg_path) {
|
||||||
if let Ok(cfg) = serde_json::from_str::<serde_json::Value>(&content) {
|
if let Ok(cfg) = serde_json::from_str::<serde_json::Value>(&content) {
|
||||||
@@ -32,15 +32,17 @@ fn load_config() -> (String, String) {
|
|||||||
.unwrap_or("")
|
.unwrap_or("")
|
||||||
.to_string();
|
.to_string();
|
||||||
if !anon.is_empty() && !service.is_empty() {
|
if !anon.is_empty() && !service.is_empty() {
|
||||||
return (anon, service);
|
return Ok((anon, service));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Fallback 3: chaves embedadas no binário (funciona em qualquer PC sem config)
|
Err(format!(
|
||||||
(
|
"SUPABASE_ANON_KEY e SUPABASE_SERVICE_KEY nao encontradas em config.json.\n\
|
||||||
"sb_publishable_NCUGwstslOvVt5JtGxKB9A_p-BERG57".to_string(),
|
Crie o arquivo {:?} com o seguinte formato:\n\
|
||||||
"sb_secret_T6eHIKJDGaH53HMTG6UUKa_6ou6KfEW".to_string(),
|
{{\"SUPABASE_ANON_KEY\": \"sua_chave_anon\", \"SUPABASE_SERVICE_KEY\": \"sua_chave_service\"}}\n\
|
||||||
)
|
Gere as chaves em: https://supabase.ofrangao.com.br/project/default/settings/api",
|
||||||
|
cfg_path
|
||||||
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
@@ -55,17 +57,22 @@ fn main() {
|
|||||||
fs::create_dir_all(&data_dir).expect("não foi possível criar diretório de dados");
|
fs::create_dir_all(&data_dir).expect("não foi possível criar diretório de dados");
|
||||||
log::info!("Data dir: {:?}", data_dir);
|
log::info!("Data dir: {:?}", data_dir);
|
||||||
|
|
||||||
// Carrega chaves do Supabase
|
// Carrega chaves do Supabase — opcional. App abre mesmo sem config (modo offline-only).
|
||||||
let (anon_key, service_key) = load_config();
|
// O usuário pode configurar depois via modal ⚙️.
|
||||||
if anon_key.is_empty() || service_key.is_empty() {
|
let (anon_key, service_key) = match load_config() {
|
||||||
log::warn!("SUPABASE_ANON_KEY ou SUPABASE_SERVICE_KEY não encontradas!");
|
Ok(keys) => {
|
||||||
log::warn!("Cole-as em: {:?}", data_dir.join("config.json"));
|
|
||||||
log::warn!("Formato: {{\"SUPABASE_ANON_KEY\": \"...\", \"SUPABASE_SERVICE_KEY\": \"...\"}}");
|
|
||||||
} else {
|
|
||||||
log::info!("Supabase: keys carregadas (anon={}..., service={}...)",
|
log::info!("Supabase: keys carregadas (anon={}..., service={}...)",
|
||||||
&anon_key[..8.min(anon_key.len())],
|
&keys.0[..8.min(keys.0.len())],
|
||||||
&service_key[..8.min(service_key.len())]);
|
&keys.1[..8.min(keys.1.len())]);
|
||||||
|
keys
|
||||||
}
|
}
|
||||||
|
Err(e) => {
|
||||||
|
log::warn!("Supabase: configuracao ausente — app abrindo em modo offline-only.");
|
||||||
|
log::warn!("Motivo: {}", e);
|
||||||
|
log::warn!("Use o botao Configurar no app pra definir as chaves.");
|
||||||
|
(String::new(), String::new())
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// Inicializa plugins
|
// Inicializa plugins
|
||||||
let storage = storage::init(data_dir.clone())
|
let storage = storage::init(data_dir.clone())
|
||||||
@@ -111,9 +118,13 @@ 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,
|
||||||
|
// Supabase config (gerenciado pela UI)
|
||||||
|
plugins::app::ler_config_supabase,
|
||||||
|
plugins::app::salvar_config_supabase,
|
||||||
])
|
])
|
||||||
.run(tauri::generate_context!())
|
.run(tauri::generate_context!())
|
||||||
.expect("erro ao inicializar Tauri");
|
.expect("erro ao inicializar Tauri");
|
||||||
|
|||||||
@@ -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()
|
||||||
}
|
}
|
||||||
@@ -85,3 +111,87 @@ pub fn set_config(state: State<'_, AppState>, cfg: AppConfig) -> Result<(), Stri
|
|||||||
pub fn init(data_dir: PathBuf) -> AppState {
|
pub fn init(data_dir: PathBuf) -> AppState {
|
||||||
AppState::new(data_dir)
|
AppState::new(data_dir)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Lê o config.json cru e devolve as chaves do Supabase.
|
||||||
|
/// Retorna masked:true se as chaves existirem (sem expor os valores completos).
|
||||||
|
/// Retorna masked:false se o arquivo não existir ou estiver vazio.
|
||||||
|
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||||
|
pub struct SupabaseConfigStatus {
|
||||||
|
pub tem_config: bool,
|
||||||
|
pub anon_masked: String,
|
||||||
|
pub service_masked: String,
|
||||||
|
pub config_path: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub fn ler_config_supabase() -> SupabaseConfigStatus {
|
||||||
|
let config_path = crate::plugins::app::data_dir_for_status()
|
||||||
|
.join("config.json");
|
||||||
|
|
||||||
|
let (anon, service) = match fs::read_to_string(&config_path) {
|
||||||
|
Ok(content) => {
|
||||||
|
let cfg: serde_json::Value = serde_json::from_str(&content).unwrap_or_default();
|
||||||
|
(
|
||||||
|
cfg.get("SUPABASE_ANON_KEY").and_then(|v| v.as_str()).unwrap_or("").to_string(),
|
||||||
|
cfg.get("SUPABASE_SERVICE_KEY").and_then(|v| v.as_str()).unwrap_or("").to_string(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Err(_) => (String::new(), String::new()),
|
||||||
|
};
|
||||||
|
|
||||||
|
SupabaseConfigStatus {
|
||||||
|
tem_config: !anon.is_empty() && !service.is_empty(),
|
||||||
|
anon_masked: mask_key(&anon),
|
||||||
|
service_masked: mask_key(&service),
|
||||||
|
config_path: config_path.to_string_lossy().to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn mask_key(key: &str) -> String {
|
||||||
|
if key.is_empty() {
|
||||||
|
return String::new();
|
||||||
|
}
|
||||||
|
if key.len() <= 12 {
|
||||||
|
return format!("{}...", &key[..4.min(key.len())]);
|
||||||
|
}
|
||||||
|
format!("{}...{}", &key[..6], &key[key.len()-4..])
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Salva (ou atualiza) as chaves do Supabase no config.json.
|
||||||
|
/// Cria o diretório se necessário. Mantém os outros campos do config.json intactos.
|
||||||
|
#[tauri::command]
|
||||||
|
pub fn salvar_config_supabase(anon_key: String, service_key: String) -> Result<SupabaseConfigStatus, String> {
|
||||||
|
if anon_key.trim().is_empty() || service_key.trim().is_empty() {
|
||||||
|
return Err("As duas chaves são obrigatórias.".to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
let config_path = crate::plugins::app::data_dir_for_status().join("config.json");
|
||||||
|
if let Some(parent) = config_path.parent() {
|
||||||
|
fs::create_dir_all(parent).map_err(|e| format!("Falha ao criar diretório: {}", e))?;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Lê config existente pra preservar outros campos (loja, pdv_nome, etc.)
|
||||||
|
let mut cfg: serde_json::Value = fs::read_to_string(&config_path)
|
||||||
|
.ok()
|
||||||
|
.and_then(|c| serde_json::from_str(&c).ok())
|
||||||
|
.unwrap_or_else(|| serde_json::json!({}));
|
||||||
|
|
||||||
|
cfg["SUPABASE_ANON_KEY"] = serde_json::Value::String(anon_key.trim().to_string());
|
||||||
|
cfg["SUPABASE_SERVICE_KEY"] = serde_json::Value::String(service_key.trim().to_string());
|
||||||
|
|
||||||
|
let json = serde_json::to_string_pretty(&cfg)
|
||||||
|
.map_err(|e| format!("Falha ao serializar config: {}", e))?;
|
||||||
|
fs::write(&config_path, json).map_err(|e| format!("Falha ao salvar config: {}", e))?;
|
||||||
|
|
||||||
|
log::info!("Supabase config salva em {:?}", config_path);
|
||||||
|
|
||||||
|
Ok(ler_config_supabase())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Helper pra obter o data_dir sem precisar importar de main.rs.
|
||||||
|
/// Mantém o mesmo cálculo de `main::data_dir_path()`.
|
||||||
|
pub fn data_dir_for_status() -> PathBuf {
|
||||||
|
dirs::data_local_dir()
|
||||||
|
.unwrap_or_else(|| PathBuf::from("."))
|
||||||
|
.join("FechamentoCaixa")
|
||||||
|
}
|
||||||
|
|||||||
@@ -322,9 +322,10 @@ impl ReciboData {
|
|||||||
out.extend(escpos::divider());
|
out.extend(escpos::divider());
|
||||||
|
|
||||||
// ── DIFERENÇA ──
|
// ── DIFERENÇA ──
|
||||||
// Diferença = total_fechamento - saldo_esperado - cancelamentos
|
// Usa o valor calculado pelo frontend (self.diferenca).
|
||||||
|
// O frontend já subtrai saldo_esperado e cancelamentos de sys_total.
|
||||||
out.extend(escpos::line_bold("DIFERENCA"));
|
out.extend(escpos::line_bold("DIFERENCA"));
|
||||||
let diff = self.fechamento - self.saldo_esperado - self.cancelamentos;
|
let diff = self.diferenca;
|
||||||
let diff_str = if diff >= 0.0 {
|
let diff_str = if diff >= 0.0 {
|
||||||
format!("+{}", self.fmt_money(diff))
|
format!("+{}", self.fmt_money(diff))
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
+161
-10
@@ -292,6 +292,9 @@
|
|||||||
<body>
|
<body>
|
||||||
|
|
||||||
<!-- ═══ HEADER ════════════════════════════════════════════════════════════ -->
|
<!-- ═══ HEADER ════════════════════════════════════════════════════════════ -->
|
||||||
|
<div id="supabase-warning" style="display:none;background:#fff3e0;color:#bf360c;padding:8px 18px;font-size:12px;font-weight:600;border-bottom:2px solid var(--orange)">
|
||||||
|
⚠️ Supabase não configurado — fechamento será salvo <strong>somente localmente</strong>. <a href="#" id="supabase-warning-link" style="color:#bf360c;text-decoration:underline">Configurar agora</a>
|
||||||
|
</div>
|
||||||
<div class="header">
|
<div class="header">
|
||||||
<div class="header-left">
|
<div class="header-left">
|
||||||
<span class="header-title">Fechamento de Caixa</span>
|
<span class="header-title">Fechamento de Caixa</span>
|
||||||
@@ -547,6 +550,23 @@
|
|||||||
<input type="password" id="cfg-dynamic-password" style="flex:1;padding:6px;border:1px solid #ddd;border-radius:4px;font-size:13px" placeholder="Dinâmica: 10+dia+mes (ex: 1217)">
|
<input type="password" id="cfg-dynamic-password" style="flex:1;padding:6px;border:1px solid #ddd;border-radius:4px;font-size:13px" placeholder="Dinâmica: 10+dia+mes (ex: 1217)">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="cfg-section">
|
||||||
|
<div class="cfg-section-title">☁️ Supabase (sincronização na nuvem)</div>
|
||||||
|
<div id="supabase-status" style="font-size:12px;padding:6px 10px;border-radius:4px;margin-bottom:8px;background:#f5f5f5;color:#666">
|
||||||
|
Verificando...
|
||||||
|
</div>
|
||||||
|
<div style="display:flex;flex-direction:column;gap:6px">
|
||||||
|
<input type="password" id="cfg-supabase-anon" placeholder="SUPABASE_ANON_KEY" style="padding:6px;border:1px solid #ddd;border-radius:4px;font-size:12px;font-family:monospace">
|
||||||
|
<input type="password" id="cfg-supabase-service" placeholder="SUPABASE_SERVICE_KEY" style="padding:6px;border:1px solid #ddd;border-radius:4px;font-size:12px;font-family:monospace">
|
||||||
|
</div>
|
||||||
|
<div style="display:flex;gap:6px;margin-top:6px">
|
||||||
|
<button class="btn-add-config" id="cfg-supabase-salvar">💾 Salvar chaves</button>
|
||||||
|
<button class="btn-add-config" id="cfg-supabase-testar" style="background:#1976d2">🔌 Testar conexão</button>
|
||||||
|
<button class="btn-add-config" id="cfg-supabase-revelar" style="background:#888;font-size:10px;padding:4px 8px">👁 Revelar</button>
|
||||||
|
</div>
|
||||||
|
<div id="supabase-result" style="font-size:12px;margin-top:6px;display:none"></div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-footer">
|
<div class="modal-footer">
|
||||||
<button class="btn-primary" id="cfg-salvar">Salvar Configuração</button>
|
<button class="btn-primary" id="cfg-salvar">Salvar Configuração</button>
|
||||||
@@ -1323,9 +1343,12 @@ function buildConfirmHTML(data) {
|
|||||||
// Relatório completo — mesmo layout do PDF
|
// Relatório completo — mesmo layout do PDF
|
||||||
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 = fechamento - saldo_esperado - cancelamentos
|
// Cancelamentos: SEMPRE recalcular da array (o escalar pode estar desatualizado)
|
||||||
const diferenca = data.diferenca !== undefined ? data.diferenca
|
const cancelamentos_arr = Array.isArray(data.cancelamentos_arr) ? data.cancelamentos_arr : [];
|
||||||
: ((data.fechamento || 0) - (data.saldo_esperado || 0) - (data.cancelamentos || 0));
|
const totalCancelCalc = cancelamentos_arr.reduce((s,r) => s+(r.valor||0), 0);
|
||||||
|
const sysTotalCalc = (data.sys_total || data.fechamento || 0);
|
||||||
|
const saldoEsperadoCalc = (data.saldo_esperado || 0);
|
||||||
|
const diferenca = sysTotalCalc - saldoEsperadoCalc - totalCancelCalc;
|
||||||
const diffStr = diferenca >= 0 ? `+${fmt(diferenca)}` : fmtNeg(diferenca);
|
const diffStr = diferenca >= 0 ? `+${fmt(diferenca)}` : fmtNeg(diferenca);
|
||||||
const diffOk = diferenca === 0;
|
const diffOk = diferenca === 0;
|
||||||
|
|
||||||
@@ -1334,7 +1357,6 @@ function buildConfirmHTML(data) {
|
|||||||
const pixcnpj_arr = Array.isArray(data.pixcnpj_arr) ? data.pixcnpj_arr : [];
|
const pixcnpj_arr = Array.isArray(data.pixcnpj_arr) ? data.pixcnpj_arr : [];
|
||||||
const vales_arr = Array.isArray(data.vales_arr) ? data.vales_arr : [];
|
const vales_arr = Array.isArray(data.vales_arr) ? data.vales_arr : [];
|
||||||
const receber_arr = Array.isArray(data.receber_arr) ? data.receber_arr : [];
|
const receber_arr = Array.isArray(data.receber_arr) ? data.receber_arr : [];
|
||||||
const cancelamentos_arr = Array.isArray(data.cancelamentos_arr) ? data.cancelamentos_arr : [];
|
|
||||||
|
|
||||||
const cartCred_arr = Array.isArray(data.cartoes_credito) ? data.cartoes_credito : [];
|
const cartCred_arr = Array.isArray(data.cartoes_credito) ? data.cartoes_credito : [];
|
||||||
const cartDeb_arr = Array.isArray(data.cartoes_debito) ? data.cartoes_debito : [];
|
const cartDeb_arr = Array.isArray(data.cartoes_debito) ? data.cartoes_debito : [];
|
||||||
@@ -1492,8 +1514,10 @@ function buildPrintHTML(data) {
|
|||||||
// Diferença = Total Sistema - Saldo Esperado - Cancelamentos
|
// Diferença = Total Sistema - Saldo Esperado - Cancelamentos
|
||||||
const sysTotalCalc = (data.sysTotal || 0);
|
const sysTotalCalc = (data.sysTotal || 0);
|
||||||
const saldoEsperadoCalc = (data.saldo_esperado || 0);
|
const saldoEsperadoCalc = (data.saldo_esperado || 0);
|
||||||
const cancelamentosCalc = (data.cancelamentos || 0);
|
// Cancelamentos: SEMPRE recalcular da array (o scalar pode estar desatualizado)
|
||||||
const diferenca = sysTotalCalc - saldoEsperadoCalc - cancelamentosCalc;
|
const cancelamentos_arr = Array.isArray(data.cancelamentos_arr) ? data.cancelamentos_arr : [];
|
||||||
|
const totalCancelCalc = cancelamentos_arr.reduce((s,r) => s+(r.valor||0), 0);
|
||||||
|
const diferenca = sysTotalCalc - saldoEsperadoCalc - totalCancelCalc;
|
||||||
const diffStr = diferenca >= 0 ? `+${fmt(diferenca)}` : fmtNeg(diferenca);
|
const diffStr = diferenca >= 0 ? `+${fmt(diferenca)}` : fmtNeg(diferenca);
|
||||||
const diffColor = diferenca === 0 ? '#2e7d32' : (diferenca > 0 ? '#e65100' : '#c0272d');
|
const diffColor = diferenca === 0 ? '#2e7d32' : (diferenca > 0 ? '#e65100' : '#c0272d');
|
||||||
|
|
||||||
@@ -1503,7 +1527,6 @@ function buildPrintHTML(data) {
|
|||||||
const pixcnpj_arr = Array.isArray(data.pixcnpj_arr) ? data.pixcnpj_arr : [];
|
const pixcnpj_arr = Array.isArray(data.pixcnpj_arr) ? data.pixcnpj_arr : [];
|
||||||
const vales_arr = Array.isArray(data.vales_arr) ? data.vales_arr : [];
|
const vales_arr = Array.isArray(data.vales_arr) ? data.vales_arr : [];
|
||||||
const receber_arr = Array.isArray(data.receber_arr) ? data.receber_arr : [];
|
const receber_arr = Array.isArray(data.receber_arr) ? data.receber_arr : [];
|
||||||
const cancelamentos_arr = Array.isArray(data.cancelamentos_arr) ? data.cancelamentos_arr : [];
|
|
||||||
|
|
||||||
// Cartoes arrays from estado
|
// Cartoes arrays from estado
|
||||||
const cartCred_arr = Array.isArray(data.cartoes_credito) ? data.cartoes_credito : [];
|
const cartCred_arr = Array.isArray(data.cartoes_credito) ? data.cartoes_credito : [];
|
||||||
@@ -1723,12 +1746,13 @@ function buildReportHTML(data) {
|
|||||||
const vales = data.vales || 0;
|
const vales = data.vales || 0;
|
||||||
const receber = data.areceber || 0;
|
const receber = data.areceber || 0;
|
||||||
const pixcnpj = data.pixcnpj || 0;
|
const pixcnpj = data.pixcnpj || 0;
|
||||||
const cancelamentos = data.cancelamentos || 0;
|
// Cancelamentos: SEMPRE recalcular da array (o escalar pode estar desatualizado)
|
||||||
|
const cancelamentos_arr = Array.isArray(data.cancelamentos_arr) ? data.cancelamentos_arr : [];
|
||||||
|
const totalCancelCalc = cancelamentos_arr.reduce((s,r) => s+(r.valor||0), 0);
|
||||||
const fechamento = data.fechamento || 0;
|
const fechamento = data.fechamento || 0;
|
||||||
const sysTotal = data.sys_total || 0;
|
const sysTotal = data.sys_total || 0;
|
||||||
const saldoEsp = data.saldo_esperado || 0;
|
const saldoEsp = data.saldo_esperado || 0;
|
||||||
const diferenca = data.diferenca !== undefined ? data.diferenca
|
const diferenca = sysTotal - saldoEsp - totalCancelCalc;
|
||||||
: (fechamento - saldoEsp - cancelamentos);
|
|
||||||
const diffStr = diferenca >= 0 ? `+${fmt(diferenca)}` : fmtNeg(diferenca);
|
const diffStr = diferenca >= 0 ? `+${fmt(diferenca)}` : fmtNeg(diferenca);
|
||||||
const diffColor = diferenca === 0 ? '#2e7d32' : '#c0272d';
|
const diffColor = diferenca === 0 ? '#2e7d32' : '#c0272d';
|
||||||
|
|
||||||
@@ -3666,11 +3690,138 @@ async function salvarEdicaoSupabase() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ─── Supabase Config (UI) ──────────────────────────────────────────────────
|
||||||
|
async function checkSupabaseConfig() {
|
||||||
|
if (!window.__TAURI__) return null;
|
||||||
|
try {
|
||||||
|
const status = await window.__TAURI__.core.invoke('ler_config_supabase');
|
||||||
|
return status;
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('ler_config_supabase falhou:', e);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderSupabaseStatus(status) {
|
||||||
|
const banner = document.getElementById('supabase-warning');
|
||||||
|
const statusEl = document.getElementById('supabase-status');
|
||||||
|
const anonInput = document.getElementById('cfg-supabase-anon');
|
||||||
|
const serviceInput = document.getElementById('cfg-supabase-service');
|
||||||
|
|
||||||
|
if (!status) {
|
||||||
|
banner.style.display = 'none';
|
||||||
|
statusEl.textContent = 'Tauri não disponível (rodando fora do app).';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (status.tem_config) {
|
||||||
|
banner.style.display = 'none';
|
||||||
|
statusEl.style.background = '#e8f5e9';
|
||||||
|
statusEl.style.color = '#2e7d32';
|
||||||
|
statusEl.innerHTML = `✅ Configurado: anon=${status.anon_masked} service=${status.service_masked}`;
|
||||||
|
anonInput.placeholder = status.anon_masked + ' (já configurado — cole nova chave pra substituir)';
|
||||||
|
serviceInput.placeholder = status.service_masked + ' (já configurado — cole nova chave pra substituir)';
|
||||||
|
} else {
|
||||||
|
banner.style.display = 'block';
|
||||||
|
statusEl.style.background = '#fff3e0';
|
||||||
|
statusEl.style.color = '#bf360c';
|
||||||
|
statusEl.innerHTML = `⚠️ Não configurado. Arquivo: <code style="font-size:11px">${status.config_path}</code>`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function setupSupabaseConfigUI() {
|
||||||
|
// Banner → abre modal
|
||||||
|
const link = document.getElementById('supabase-warning-link');
|
||||||
|
if (link) link.addEventListener('click', e => {
|
||||||
|
e.preventDefault();
|
||||||
|
document.getElementById('btn-config').click();
|
||||||
|
setTimeout(() => {
|
||||||
|
document.getElementById('cfg-supabase-anon').focus();
|
||||||
|
}, 200);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Botão Salvar
|
||||||
|
document.getElementById('cfg-supabase-salvar').addEventListener('click', async () => {
|
||||||
|
const anon = document.getElementById('cfg-supabase-anon').value.trim();
|
||||||
|
const service = document.getElementById('cfg-supabase-service').value.trim();
|
||||||
|
const result = document.getElementById('supabase-result');
|
||||||
|
|
||||||
|
if (!anon || !service) {
|
||||||
|
result.style.display = 'block';
|
||||||
|
result.style.background = '#ffebee';
|
||||||
|
result.style.color = '#c0272d';
|
||||||
|
result.innerHTML = '⚠️ Cole as duas chaves (anon e service).';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const status = await window.__TAURI__.core.invoke('salvar_config_supabase', {
|
||||||
|
anonKey: anon,
|
||||||
|
serviceKey: service,
|
||||||
|
});
|
||||||
|
result.style.display = 'block';
|
||||||
|
result.style.background = '#e8f5e9';
|
||||||
|
result.style.color = '#2e7d32';
|
||||||
|
result.innerHTML = '✅ Chaves salvas! Reinicie o app para que a sincronização comece a funcionar.';
|
||||||
|
// Limpa os campos e atualiza status
|
||||||
|
document.getElementById('cfg-supabase-anon').value = '';
|
||||||
|
document.getElementById('cfg-supabase-service').value = '';
|
||||||
|
renderSupabaseStatus(status);
|
||||||
|
} catch (e) {
|
||||||
|
result.style.display = 'block';
|
||||||
|
result.style.background = '#ffebee';
|
||||||
|
result.style.color = '#c0272d';
|
||||||
|
result.innerHTML = '❌ Erro ao salvar: ' + e;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Botão Testar conexão (usa sb_online que já existia)
|
||||||
|
document.getElementById('cfg-supabase-testar').addEventListener('click', async () => {
|
||||||
|
const result = document.getElementById('supabase-result');
|
||||||
|
result.style.display = 'block';
|
||||||
|
result.style.background = '#f5f5f5';
|
||||||
|
result.style.color = '#666';
|
||||||
|
result.innerHTML = '🔌 Testando...';
|
||||||
|
|
||||||
|
try {
|
||||||
|
const online = await window.__TAURI__.core.invoke('sb_online');
|
||||||
|
if (online) {
|
||||||
|
result.style.background = '#e8f5e9';
|
||||||
|
result.style.color = '#2e7d32';
|
||||||
|
result.innerHTML = '✅ Conectado ao Supabase com sucesso!';
|
||||||
|
} else {
|
||||||
|
result.style.background = '#ffebee';
|
||||||
|
result.style.color = '#c0272d';
|
||||||
|
result.innerHTML = '❌ Não foi possível conectar. Verifique as chaves e a internet.';
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
result.style.background = '#ffebee';
|
||||||
|
result.style.color = '#c0272d';
|
||||||
|
result.innerHTML = '❌ Erro: ' + e;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Botão Revelar/Esconder
|
||||||
|
document.getElementById('cfg-supabase-revelar').addEventListener('click', () => {
|
||||||
|
const a = document.getElementById('cfg-supabase-anon');
|
||||||
|
const s = document.getElementById('cfg-supabase-service');
|
||||||
|
const isPwd = a.type === 'password';
|
||||||
|
a.type = isPwd ? 'text' : 'password';
|
||||||
|
s.type = isPwd ? 'text' : 'password';
|
||||||
|
event.target.textContent = isPwd ? '🙈 Esconder' : '👁 Revelar';
|
||||||
|
});
|
||||||
|
|
||||||
|
// Verifica status inicial
|
||||||
|
const status = await checkSupabaseConfig();
|
||||||
|
renderSupabaseStatus(status);
|
||||||
|
}
|
||||||
|
|
||||||
// ─── Boot ─────────────────────────────────────────────────────────────────
|
// ─── Boot ─────────────────────────────────────────────────────────────────
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
loadConfig();
|
loadConfig();
|
||||||
buildDatalists();
|
buildDatalists();
|
||||||
if (!loadState()) init();
|
if (!loadState()) init();
|
||||||
|
setupSupabaseConfigUI();
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
Reference in New Issue
Block a user