Compare commits
2 Commits
main
...
v2.0.4-alianca
| Author | SHA1 | Date | |
|---|---|---|---|
| 7c036b916f | |||
| c58f01d51b |
+26
-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())
|
||||||
@@ -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");
|
||||||
|
|||||||
@@ -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")
|
||||||
|
}
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
+147
@@ -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>
|
||||||
@@ -3670,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