Compare commits

..

3 Commits

Author SHA1 Message Date
Coder Agent 7c036b916f feat: configurar Supabase via UI no modal (sem mexer em config.json)
Build Fechamento de Caixa / build (macos-14, app) (push) Has been cancelled
Build Fechamento de Caixa / build (ubuntu-22.04, deb) (push) Has been cancelled
Build Fechamento de Caixa / build (windows-2022, msi) (push) Has been cancelled
- Adiciona ler_config_supabase() e salvar_config_supabase() no app.rs
- App abre mesmo sem config (modo offline-only); Supabase é configurável depois
- Banner amarelo no topo alerta quando Supabase não está configurado
- Nova seção '☁️ Supabase' no modal de Configurações com campos mascarados
- Botões: Salvar chaves, Testar conexão, Revelar/Esconder
2026-07-05 15:52:21 -03:00
Coder Agent c58f01d51b security: remove chaves Supabase hardcoded; use self.diferenca do frontend no recibo
Build Fechamento de Caixa / build (macos-14, app) (push) Has been cancelled
Build Fechamento de Caixa / build (ubuntu-22.04, deb) (push) Has been cancelled
Build Fechamento de Caixa / build (windows-2022, msi) (push) Has been cancelled
2026-07-05 15:40:01 -03:00
root acb90f093d fix: diferenca calculada sempre a partir da array de cancelamentos (nao do escalar)
Build Fechamento de Caixa / build (macos-14, app) (push) Has been cancelled
Build Fechamento de Caixa / build (ubuntu-22.04, deb) (push) Has been cancelled
Build Fechamento de Caixa / build (windows-2022, msi) (push) Has been cancelled
2026-07-04 23:34:56 +00:00
4 changed files with 288 additions and 85 deletions
+26 -16
View File
@@ -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())
@@ -115,6 +122,9 @@ fn main() {
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");
+84
View File
@@ -111,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")
}
+3 -2
View File
@@ -322,9 +322,10 @@ impl ReciboData {
out.extend(escpos::divider()); out.extend(escpos::divider());
// ── DIFERENÇA ── // ── DIFERENÇA ──
// Diferença = TOTAL SISTEMA - saldo_esperado - cancelamentos // Usa o valor calculado pelo frontend (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.sys_total - 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 {
+169 -61
View File
@@ -63,8 +63,8 @@
.meta-select { cursor: pointer; } .meta-select { cursor: pointer; }
/* ── MAIN 2 COLUMNS — single scroll, no internal scrollbars ──────────── */ /* ── MAIN 2 COLUMNS — single scroll, no internal scrollbars ──────────── */
.main { flex: 1; display: grid; grid-template-columns: 1fr 1fr; overflow: hidden; min-height: 0; } .main { flex: 1; display: grid; grid-template-columns: 1fr 1fr; overflow: visible; }
.col { padding: 12px 16px; overflow: hidden; } .col { padding: 12px 16px; overflow: visible; }
.col-left { border-right: 1px solid var(--border); } .col-left { border-right: 1px solid var(--border); }
.col-right { background: var(--surface); display: flex; flex-direction: column; } .col-right { background: var(--surface); display: flex; flex-direction: column; }
.col-right .section { flex-shrink: 0; } .col-right .section { flex-shrink: 0; }
@@ -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>
@@ -323,8 +326,8 @@
<div class="meta-field"> <div class="meta-field">
<span class="meta-label">Loja</span> <span class="meta-label">Loja</span>
<select class="meta-select" id="f-loja"> <select class="meta-select" id="f-loja">
<option value="Uniao">União</option> <option value="União">União</option>
<option value="Alianca">Aliança</option> <option value="Aliança">Aliança</option>
</select> </select>
</div> </div>
</div> </div>
@@ -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,14 +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('.', ',');
// Cancelamentos: SEMPRE recalcular da array — nunca usar o campo escalar // Cancelamentos: SEMPRE recalcular da array (o escalar pode estar desatualizado)
const cancelamentos_arr = Array.isArray(data.cancelamentos_arr) ? data.cancelamentos_arr : []; const cancelamentos_arr = Array.isArray(data.cancelamentos_arr) ? data.cancelamentos_arr : [];
const cancelamentosCalc = cancelamentos_arr.reduce((s, r) => s + (r.valor || 0), 0); const totalCancelCalc = cancelamentos_arr.reduce((s,r) => s+(r.valor||0), 0);
// Diferença = Total Sistema - Saldo Esperado - Cancelamentos const sysTotalCalc = (data.sys_total || data.fechamento || 0);
// ⚠️ buildReciboData retorna snake_case (sys_total, total_cancelamentos)
const sysTotalCalc = (data.sys_total || data.sysTotal || 0);
const saldoEsperadoCalc = (data.saldo_esperado || 0); const saldoEsperadoCalc = (data.saldo_esperado || 0);
const diferenca = sysTotalCalc - saldoEsperadoCalc - cancelamentosCalc; 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;
@@ -1339,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 : [];
@@ -1494,30 +1511,28 @@ ${data.observacoes ? `<div class="obs">${data.observacoes}</div>` : ''}
function buildPrintHTML(data) { function buildPrintHTML(data) {
const fmt = v => 'R$ ' + (v || 0).toFixed(2).replace('.', ','); const fmt = v => 'R$ ' + (v || 0).toFixed(2).replace('.', ',');
const fmtNeg = v => (v < 0 ? '-' : '') + 'R$ ' + Math.abs(v || 0).toFixed(2).replace('.', ','); const fmtNeg = v => (v < 0 ? '-' : '') + 'R$ ' + Math.abs(v || 0).toFixed(2).replace('.', ',');
// Cancelamentos: SEMPRE recalcular da array — nunca usar o campo escalar
const cancelamentos_arr = Array.isArray(data.cancelamentos_arr) ? data.cancelamentos_arr : [];
const cancelamentosCalc = cancelamentos_arr.reduce((s, r) => s + (r.valor || 0), 0);
// Diferença = Total Sistema - Saldo Esperado - Cancelamentos // Diferença = Total Sistema - Saldo Esperado - Cancelamentos
// ⚠️ buildReciboData retorna snake_case (sys_total, total_cancelamentos) const sysTotalCalc = (data.sysTotal || 0);
const sysTotalCalc = (data.sys_total || data.sysTotal || 0);
const saldoEsperadoCalc = (data.saldo_esperado || 0); const saldoEsperadoCalc = (data.saldo_esperado || 0);
const diferenca = sysTotalCalc - saldoEsperadoCalc - cancelamentosCalc; // Cancelamentos: SEMPRE recalcular da array (o scalar 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 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');
const cartCred_arr = Array.isArray(data.cartoes_credito) ? data.cartoes_credito : []; // Arrays — support both array-of-objects and flat totals
const cartDeb_arr = Array.isArray(data.cartoes_debito) ? data.cartoes_debito : [];
const cartAli_arr = Array.isArray(data.cartoes_alimentacao) ? data.cartoes_alimentacao : [];
const cartPix_arr = Array.isArray(data.cartoes_pix) ? data.cartoes_pix : [];
const sangrias_arr = Array.isArray(data.sangrias_arr) ? data.sangrias_arr : []; const sangrias_arr = Array.isArray(data.sangrias_arr) ? data.sangrias_arr : [];
const despesas_arr = Array.isArray(data.despesas_arr) ? data.despesas_arr : []; const despesas_arr = Array.isArray(data.despesas_arr) ? data.despesas_arr : [];
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
// (cartCred_arr etc. already declared above) const cartCred_arr = Array.isArray(data.cartoes_credito) ? data.cartoes_credito : [];
const cartDeb_arr = Array.isArray(data.cartoes_debito) ? data.cartoes_debito : [];
const cartAli_arr = Array.isArray(data.cartoes_alimentacao) ? data.cartoes_alimentacao : [];
const cartPix_arr = Array.isArray(data.cartoes_pix) ? data.cartoes_pix : [];
// Flat totals // Flat totals
const totalSangrias = sangrias_arr.reduce((s,r) => s+(r.retirada||0)+(r.valor||0), 0); const totalSangrias = sangrias_arr.reduce((s,r) => s+(r.retirada||0)+(r.valor||0), 0);
@@ -1731,16 +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;
// Cancelamentos: SEMPRE recalcular da array — nunca usar o campo escalar (pode estar corrompido de bug anterior) // Cancelamentos: SEMPRE recalcular da array (o escalar pode estar desatualizado)
const cancelamentos_arr = Array.isArray(data.cancelamentos_arr) ? data.cancelamentos_arr : []; const cancelamentos_arr = Array.isArray(data.cancelamentos_arr) ? data.cancelamentos_arr : [];
const cancelamentosCalc = cancelamentos_arr.reduce((s, r) => s + (r.valor || 0), 0); 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;
// Diferença = Total Sistema - Saldo Esperado - Cancelamentos const diferenca = sysTotal - saldoEsp - totalCancelCalc;
const sysTotalCalc = (data.sys_total || data.sysTotal || 0);
const saldoEspCalc = (data.saldo_esperado || 0);
const diferenca = sysTotalCalc - saldoEspCalc - cancelamentosCalc;
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';
@@ -1993,11 +2005,9 @@ async function tauriCarregarRascunho() {
// ─── Confirm Modal ──────────────────────────────────────────────────────── // ─── Confirm Modal ────────────────────────────────────────────────────────
function openConfirmModal() { function openConfirmModal() {
console.log('[DEBUG] openConfirmModal chamado'); console.log('[DEBUG] openConfirmModal chamado');
// Usa estado (que o boot fixou para builds fixas) em vez de ler DOM const operador = val('f-operador').trim();
// — o DOM pode estar vazio se o select está disabled/hidden const turno = val('f-turno').trim();
const operador = estado.operador.trim(); const loja = val('f-loja').trim();
const turno = estado.turno.trim();
const loja = estado.loja.trim();
console.log('[DEBUG] openConfirmModal campos:', {operador, turno, loja}); console.log('[DEBUG] openConfirmModal campos:', {operador, turno, loja});
if (!operador || !turno || !loja) { if (!operador || !turno || !loja) {
console.log('[DEBUG] openConfirmModal: campos faltando, alert'); console.log('[DEBUG] openConfirmModal: campos faltando, alert');
@@ -2065,7 +2075,8 @@ document.getElementById('confirm-print').addEventListener('click', async () => {
alert('Erro ao montar dados do recibo.'); alert('Erro ao montar dados do recibo.');
return; return;
} }
// NÃO fecha o modal — o preview de impressão abre POR CIMA (depois de imprimir e fechar, continua no confirm) // Fecha modal APÓS gerar preview (não antes)
closeConfirmModal();
openPrintPreview(data, 'report'); openPrintPreview(data, 'report');
}); });
@@ -3679,41 +3690,138 @@ async function salvarEdicaoSupabase() {
} }
} }
// ─── Boot ───────────────────────────────────────────────────────────────── // ─── Supabase Config (UI) ──────────────────────────────────────────────────
document.addEventListener('DOMContentLoaded', async () => { async function checkSupabaseConfig() {
// Detecta se é build com loja fixa (União ou Aliança) if (!window.__TAURI__) return null;
try { try {
const fixa = await window.__TAURI__.core.invoke('get_loja_fixa'); const status = await window.__TAURI__.core.invoke('ler_config_supabase');
if (fixa) { return status;
// Build fixa: marca global, esconde e bloqueia o seletor de loja } catch (e) {
window.__LOJA_FIXA__ = fixa; console.warn('ler_config_supabase falhou:', e);
estado.loja = fixa; return null;
const sel = document.getElementById('f-loja');
if (sel) {
sel.value = fixa;
sel.disabled = true;
sel.style.display = 'none';
} }
// Esconde o label "Loja" (procura pelo texto, não pela posição) }
document.querySelectorAll('.meta-label').forEach(l => {
if (l.textContent.trim() === 'Loja') l.style.display = 'none'; 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) { } catch (e) {
console.warn('get_loja_fixa não disponível:', 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(); loadConfig();
buildDatalists(); buildDatalists();
if (!loadState()) init(); if (!loadState()) init();
setupSupabaseConfigUI();
// ⚠️ Segurança extra: se build fixa, garante que o valor do select está correto
// e que estado.loja nunca fica vazio (independentemente do que init() fez)
if (window.__LOJA_FIXA__) {
const sel = document.getElementById('f-loja');
if (sel) sel.value = window.__LOJA_FIXA__;
estado.loja = window.__LOJA_FIXA__;
}
}); });
</script> </script>
</body> </body>