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