Files
SteelBase/public/js/database/admin-panel.js

1280 lines
56 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* PAINEL DE ADMINISTRAÇÃO DE DADOS
*
* Interface para gerenciar o cache de dados, atualizar CSVs
* e monitorar o status do sistema de dados.
*/
// Variáveis globais para gerenciamento de configurações
let adminConfigManager = null;
let backupManager = null;
/**
* Inicializa o sistema de configurações administrativas
*/
async function initAdminConfig() {
try {
// Evitar duplicação: todos os managers são carregados via index.html
// Aguarde até que as classes e instâncias globais estejam disponíveis
const ready = await waitForManagers();
if (!ready) {
console.error('❌ Managers não carregados após tempo de espera.');
return false;
}
// Vincular instâncias globais, sem recriar
adminConfigManager = window.adminConfigManager || (typeof AdminConfigManager !== 'undefined' ? new AdminConfigManager() : null);
backupManager = window.backupManager || (typeof BackupManager !== 'undefined' ? new BackupManager() : null);
console.log('✅ Sistema de configurações administrativas inicializado');
return true;
} catch (error) {
console.error('❌ Erro ao inicializar sistema de configurações:', error);
return false;
}
}
/**
* Aguarda até que os managers essenciais estejam disponíveis
* Retorna true quando AdminConfigManager, BackupManager e ToastManager existem
*/
async function waitForManagers(timeoutMs = 3000, intervalMs = 100) {
const start = Date.now();
return new Promise((resolve) => {
const check = () => {
const classesReady = (typeof AdminConfigManager !== 'undefined') && (typeof BackupManager !== 'undefined') && (typeof ToastManager !== 'undefined');
const instancesReady = !!(window.adminConfigManager || window.backupManager || window.toastManager);
if (classesReady || instancesReady) {
resolve(true);
return;
}
if (Date.now() - start >= timeoutMs) {
resolve(false);
return;
}
setTimeout(check, intervalMs);
};
check();
});
}
/**
* Carrega um script dinamicamente
* @param {string} src - Caminho do script
* @returns {Promise} Promise que resolve quando o script for carregado
*/
function loadScript(src) {
return new Promise((resolve, reject) => {
const script = document.createElement('script');
script.src = src;
script.onload = resolve;
script.onerror = reject;
document.head.appendChild(script);
});
}
/**
* Abre o painel de administração de dados
*/
async function abrirPainelDados() {
console.log('🔧 Abrindo painel de administração de dados');
// Inicializar sistema de configurações se necessário
if (!adminConfigManager) {
const configLoaded = await initAdminConfig();
if (!configLoaded) {
alert('❌ Erro ao carregar sistema de configurações');
return;
}
}
const stats = window.dataManager.getCacheStats();
const metadata = window.dataManager.getMetadata();
const adminConfig = adminConfigManager ? adminConfigManager.getConfig() : null;
const modalHTML = `
<div class="modal active" id="modal-admin-dados" onclick="fecharPainelDados(event)">
<div class="modal-content" onclick="event.stopPropagation()" style="max-width: 800px; background:#0a0a0f;">
<div class="modal-header">
<div class="modal-title">🗄️ Administração de Dados</div>
<button class="close-btn" onclick="fecharPainelDados()">×</button>
</div>
<div class="modal-body">
<!-- Status Geral -->
<div class="card" style="background: var(--color-bg-1); margin-bottom: 20px;">
<h3 style="color: var(--color-primary); margin: 0 0 16px 0;">📊 Status do Sistema</h3>
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 16px;">
<div>
<div style="font-size: 12px; color: var(--color-text-secondary);">Versão</div>
<div style="font-size: 18px; font-weight: bold;">${stats.version}</div>
</div>
<div>
<div style="font-size: 12px; color: var(--color-text-secondary);">Cache</div>
<div style="font-size: 18px; font-weight: bold; color: ${stats.hasCache ? 'var(--color-success)' : 'var(--color-error)'};">
${stats.hasCache ? '✅ Ativo' : '❌ Vazio'}
</div>
</div>
<div>
<div style="font-size: 12px; color: var(--color-text-secondary);">Última Atualização</div>
<div style="font-size: 14px; font-weight: bold;">
${stats.lastUpdate ? new Date(stats.lastUpdate).toLocaleString('pt-BR') : 'Nunca'}
</div>
</div>
</div>
</div>
<!-- Ações Rápidas -->
<div class="card" style="background: var(--color-bg-1); margin-bottom: 20px;">
<h3 style="color: var(--color-primary); margin: 0 0 16px 0;">⚡ Ações Rápidas</h3>
<div style="display: flex; gap: 12px; flex-wrap: wrap;">
<button class="btn btn-primary" onclick="atualizarTodosDados()">
🔄 Atualizar Todos os Dados
</button>
<button class="btn btn-success" onclick="abrirImportadorCSV()">
📥 Importar CSV
</button>
<button class="btn btn-warning" onclick="limparCacheCompleto()">
🗑️ Limpar Cache
</button>
<button class="btn btn-info" onclick="exportarDados()">
📤 Exportar Dados
</button>
<button class="btn btn-success" onclick="verificarIntegridade()">
🔍 Verificar Integridade
</button>
</div>
</div>
<!-- Status por Tipo -->
<div class="card" style="background: var(--color-bg-1);">
<h3 style="color: var(--color-primary); margin: 0 0 16px 0;">📋 Status por Tipo de Perfil</h3>
<div class="table-container">
<table style="width: 100%; border-collapse: collapse;">
<thead>
<tr style="background: var(--color-bg-2);">
<th style="padding: 12px; text-align: left; border-bottom: 2px solid var(--color-border);">Tipo</th>
<th style="padding: 12px; text-align: center; border-bottom: 2px solid var(--color-border);">Status</th>
<th style="padding: 12px; text-align: center; border-bottom: 2px solid var(--color-border);">Itens</th>
<th style="padding: 12px; text-align: center; border-bottom: 2px solid var(--color-border);">Ações</th>
</tr>
</thead>
<tbody>
${Object.entries(stats.types).map(([key, info]) => `
<tr style="border-bottom: 1px solid var(--color-border);">
<td style="padding: 12px;">
<div style="font-weight: bold;">${info.name}</div>
<div style="font-size: 12px; color: var(--color-text-secondary);">
Fonte: ${info.meta?.source || '—'}
</div>
<div style="font-size: 12px; color: var(--color-text-secondary);">
Atualizado: ${info.meta?.lastUpdate ? new Date(info.meta.lastUpdate).toLocaleString('pt-BR') : '—'}
</div>
<div style="font-size: 12px; color: var(--color-text-secondary);">
Documento (.md): ${info.meta?.docSource || '—'}
</div>
<div style="font-size: 12px; color: var(--color-text-secondary);">
Status do documento: ${info.meta?.docStatus || '—'}
</div>
<div style="margin-top: 6px; display: flex; gap: 8px; align-items: center;">
<input type="text" id="docfile-${key}" class="form-control" placeholder="conhecimento/.../arquivo.md ou BD/.../arquivo.md" value="${info.meta?.docSource || ''}" style="max-width: 420px;">
<button class="btn btn-sm btn-secondary" onclick="salvarDocFonte('${key}')">📄 Definir .md</button>
</div>
</td>
<td style="padding: 12px; text-align: center;">
<span class="badge badge-${info.cached ? 'success' : 'error'}">
${info.cached ? '✅ Cached' : '❌ Vazio'}
</span>
</td>
<td style="padding: 12px; text-align: center; font-weight: bold;">
${info.count.toLocaleString('pt-BR')}
</td>
<td style="padding: 12px; text-align: center;">
<button class="btn btn-sm btn-primary" onclick="atualizarTipoEspecifico('${key}')">
🔄 Atualizar
</button>
<button class="btn btn-sm btn-warning" onclick="limparTipoEspecifico('${key}')">
🗑️ Limpar
</button>
</td>
</tr>
`).join('')}
</tbody>
</table>
</div>
</div>
<!-- Configurações Administrativas -->
<div class="card" style="background: var(--color-bg-1); margin-top: 20px;">
<h3 style="color: var(--color-primary); margin: 0 0 16px 0;">⚙️ Configurações Administrativas</h3>
<!-- Configurações Gerais -->
<div style="margin-bottom: 20px;">
<h4 style="color: var(--color-text-secondary); margin: 0 0 12px 0;">Aplicação</h4>
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 16px;">
<div>
<label style="display: block; margin-bottom: 4px; font-size: 12px; color: var(--color-text-secondary);">Nome da Aplicação</label>
<input type="text" id="app-name" class="form-control" value="${adminConfig?.appName || 'SteelBase'}"
onchange="salvarConfiguracao('appName', this.value)" placeholder="Nome da aplicação">
</div>
<div>
<label style="display: block; margin-bottom: 4px; font-size: 12px; color: var(--color-text-secondary);">Versão</label>
<input type="text" id="app-version" class="form-control" value="${adminConfig?.version || '1.0.0'}"
onchange="salvarConfiguracao('version', this.value)" placeholder="Versão">
</div>
<div>
<label style="display: block; margin-bottom: 4px; font-size: 12px; color: var(--color-text-secondary);">Tema</label>
<select id="app-theme" class="form-control" onchange="salvarConfiguracao('themeDefault', this.value)">
<option value="escuro" ${adminConfig?.themeDefault === 'escuro' ? 'selected' : ''}>Escuro</option>
<option value="claro" ${adminConfig?.themeDefault === 'claro' ? 'selected' : ''}>Claro</option>
</select>
</div>
<div>
<label style="display: block; margin-bottom: 4px; font-size: 12px; color: var(--color-text-secondary);">Idioma</label>
<select id="app-language" class="form-control" onchange="salvarConfiguracao('appLanguage', this.value)">
<option value="pt-BR" ${adminConfig?.appLanguage === 'pt-BR' ? 'selected' : ''}>Português (BR)</option>
<option value="en-US" ${adminConfig?.appLanguage === 'en-US' ? 'selected' : ''}>English (US)</option>
<option value="es-ES" ${adminConfig?.appLanguage === 'es-ES' ? 'selected' : ''}>Español (ES)</option>
</select>
</div>
<div>
<label style="display: block; margin-bottom: 4px; font-size: 12px; color: var(--color-text-secondary);">Subtítulo</label>
<input type="text" id="app-subtitle" class="form-control" value="${adminConfig?.appSubtitle || ''}"
onchange="salvarConfiguracao('appSubtitle', this.value)" placeholder="Subtítulo">
</div>
<div>
<label style="display: block; margin-bottom: 4px; font-size: 12px; color: var(--color-text-secondary);">Rodapé</label>
<input type="text" id="app-footer" class="form-control" value="${adminConfig?.footerText || ''}"
onchange="salvarConfiguracao('footerText', this.value)" placeholder="Texto do rodapé">
</div>
</div>
</div>
<!-- Branding / Logotipo -->
<div style="margin-bottom: 20px;">
<h4 style="color: var(--color-text-secondary); margin: 0 0 12px 0;">Identidade Visual (Logotipo)</h4>
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 16px; align-items: start;">
<div>
<label style="display: block; margin-bottom: 6px; font-size: 12px; color: var(--color-text-secondary);">Upload do Logotipo (PNG/SVG)</label>
<input type="file" id="branding-logo-input" accept="image/png,image/svg+xml" class="form-control">
<small style="color: var(--color-text-secondary); display: block; margin-top: 6px;">Tamanho recomendado: 192x192 ou maior. SVG também é suportado.</small>
<div style="display:flex; gap:8px; margin-top:10px;">
<button class="btn btn-primary" onclick="salvarLogoBranding()">Salvar Logotipo</button>
<button class="btn btn-warning" onclick="removerLogoBranding()">Restaurar Padrão</button>
</div>
</div>
<div>
<label style="display: block; margin-bottom: 6px; font-size: 12px; color: var(--color-text-secondary);">Preview</label>
<div id="branding-logo-preview" class="branding-logo-preview">${adminConfig?.branding?.logo ? `<img src="${adminConfig.branding.logo}" alt="Preview"/>` : '<div class="branding-logo-fallback">🏗️</div>'}</div>
<div style="margin-top:8px; display:flex; gap:8px; align-items:center;">
<label style="font-size: 12px; color: var(--color-text-secondary);">Usar em PWA</label>
<input type="checkbox" ${adminConfig?.branding?.useInPWA ? 'checked' : ''} onchange="salvarConfiguracao('branding.useInPWA', this.checked)">
</div>
</div>
</div>
</div>
<!-- Configurações de Backup -->
<div style="margin-bottom: 20px;">
<h4 style="color: var(--color-text-secondary); margin: 0 0 12px 0;">Backup Automático</h4>
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 16px;">
<div>
<label style="display: flex; align-items: center; gap: 8px;">
<input type="checkbox" id="auto-backup"
${adminConfig?.backup?.autoBackup ? 'checked' : ''}
onchange="salvarConfiguracao('backup.autoBackup', this.checked)">
<span style="font-size: 12px;">Ativar backup automático</span>
</label>
</div>
<div>
<label style="display: block; margin-bottom: 4px; font-size: 12px; color: var(--color-text-secondary);">Intervalo (minutos)</label>
<input type="number" id="backup-interval" class="form-control"
value="${adminConfig?.backup?.intervalMinutes || 60}" min="5" max="1440"
onchange="salvarConfiguracao('backup.intervalMinutes', parseInt(this.value))">
</div>
</div>
</div>
<!-- Ações de Configuração -->
<div style="display: flex; gap: 12px; flex-wrap: wrap;">
<button class="btn btn-primary" onclick="criarBackupManual()">
💾 Criar Backup Manual
</button>
<button class="btn btn-info" onclick="abrirGerenciadorBackups()">
📂 Gerenciar Backups
</button>
<button class="btn btn-warning" onclick="exportarConfiguracoes()">
📤 Exportar Configurações
</button>
<button class="btn btn-success" onclick="importarConfiguracoes()">
📥 Importar Configurações
</button>
<button class="btn btn-danger" onclick="resetarConfiguracoes()">
🔄 Resetar Configurações
</button>
</div>
</div>
<!-- Preferências e Modo Padrão -->
<div class="card" style="background: var(--color-bg-1); margin-top: 20px;">
<h3 style="color: var(--color-primary); margin: 0 0 16px 0;">🧩 Preferências</h3>
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 16px;">
<div>
<label style="display: block; margin-bottom: 4px; font-size: 12px; color: var(--color-text-secondary);">Modo Padrão</label>
<select id="default-mode" class="form-control" onchange="salvarConfiguracao('modeDefault', this.value)">
<option value="simples" ${adminConfig?.modeDefault === 'simples' ? 'selected' : ''}>Simples</option>
<option value="expert" ${adminConfig?.modeDefault === 'expert' || adminConfig?.modeDefault === 'experto' ? 'selected' : ''}>Expert</option>
</select>
</div>
<div>
<label style="display: block; margin-bottom: 4px; font-size: 12px; color: var(--color-text-secondary);">Alternar Expert</label>
<button class="btn btn-secondary" id="adminExpertToggle" onclick="toggleExpertMode()">
${document.documentElement.classList.contains('expert-mode') ? '🔬 Expert Ativo' : '🎯 Alternar Expert'}
</button>
</div>
</div>
</div>
<!-- Ferramentas Visíveis (Modo Simples) -->
<div class="card" style="background: var(--color-bg-1); margin-top: 20px;">
<h3 style="color: var(--color-primary); margin: 0 0 16px 0;">🔧 Ferramentas Visíveis (Modo Simples)</h3>
<p style="color: var(--color-text-secondary); margin-bottom: 12px;">No Modo Expert, todas as ferramentas ficam visíveis.</p>
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 8px;">
${Object.keys(adminConfig?.toolsVisibility || {}).map(tool => `
<label style="display:flex; align-items:center; gap:8px; font-size:14px;">
<input type="checkbox" ${adminConfig.toolsVisibility[tool] ? 'checked' : ''} onchange="salvarConfiguracao('toolsVisibility.${tool}', this.checked)">
<span>${tool}</span>
</label>
`).join('')}
</div>
</div>
<!-- Log de Atividades -->
<div class="card" style="background: var(--color-bg-1); margin-top: 20px;">
<h3 style="color: var(--color-primary); margin: 0 0 16px 0;">📝 Log de Atividades</h3>
<div id="admin-log" style="background: #000; color: #0f0; padding: 16px; border-radius: 8px; font-family: monospace; font-size: 12px; max-height: 200px; overflow-y: auto;">
${metadata ? `
<div>✅ Última atualização: ${new Date(metadata.lastUpdate).toLocaleString('pt-BR')}</div>
<div>📊 Tipos carregados: ${metadata.successCount}/${metadata.totalTypes}</div>
${metadata.errorCount > 0 ? `<div style="color: #f00;">❌ Erros: ${metadata.errorCount}</div>` : ''}
` : '<div>⚠️ Nenhum log disponível</div>'}
${adminConfig ? `<div>⚙️ Configurações: ${adminConfig.version}</div>` : ''}
</div>
</div>
</div>
<div class="modal-footer">
<button class="btn btn-secondary" onclick="fecharPainelDados()">Fechar</button>
</div>
</div>
</div>
`;
// Remover modal existente se houver
const existingModal = document.getElementById('modal-admin-dados');
if (existingModal) {
existingModal.remove();
}
// Adicionar estilos CSS para os campos de formulário
addAdminPanelStyles();
// Adicionar novo modal
document.body.insertAdjacentHTML('beforeend', modalHTML);
}
/**
* Fecha o painel de administração
*/
function fecharPainelDados(event) {
if (event && event.target !== event.currentTarget) return;
const modal = document.getElementById('modal-admin-dados');
if (modal) {
modal.remove();
}
}
/**
* Atualiza todos os dados
*/
async function atualizarTodosDados() {
if (!confirm('🔄 Deseja atualizar todos os dados? Isso pode levar alguns segundos.')) {
return;
}
const logEl = document.getElementById('admin-log');
if (logEl) {
logEl.innerHTML = '<div>🔄 Iniciando atualização completa...</div>';
}
try {
const results = await window.dataManager.updateAllData();
if (logEl) {
logEl.innerHTML = `
<div>✅ Atualização completa finalizada!</div>
<div>📊 Sucessos: ${results.success.length}</div>
${results.errors.length > 0 ? `<div style="color: #f00;">❌ Erros: ${results.errors.length}</div>` : ''}
<div style="margin-top: 8px;">Detalhes:</div>
${results.success.map(s => `<div>✅ ${s.name}: ${s.count} itens</div>`).join('')}
${results.errors.map(e => `<div style="color: #f00;">❌ ${e.name}: ${e.error}</div>`).join('')}
`;
}
// Recarregar painel
setTimeout(() => {
fecharPainelDados();
abrirPainelDados();
}, 2000);
} catch (error) {
console.error('❌ Erro ao atualizar dados:', error);
if (logEl) {
logEl.innerHTML = `<div style="color: #f00;">❌ Erro: ${error.message}</div>`;
}
}
}
/**
* Limpa todo o cache
*/
function limparCacheCompleto() {
if (!confirm('🗑️ Deseja limpar todo o cache? Os dados serão recarregados na próxima vez.')) {
return;
}
try {
window.dataManager.clearCache();
alert('✅ Cache limpo com sucesso!');
// Recarregar painel
fecharPainelDados();
abrirPainelDados();
} catch (error) {
console.error('❌ Erro ao limpar cache:', error);
alert('❌ Erro ao limpar cache: ' + error.message);
}
}
/**
* Atualiza um tipo específico de dados
*/
async function atualizarTipoEspecifico(tipo) {
if (!confirm(`🔄 Deseja atualizar os dados de ${tipo}?`)) {
return;
}
try {
// Ler docSource do campo (se preenchido)
const input = document.getElementById(`docfile-${tipo}`);
let docSource = input ? input.value.trim() : null;
if (docSource) {
docSource = window.dataManager.normalizeDocSource(docSource);
}
// Atualizar via DataManager (com registro de metadata por tipo)
const result = await window.dataManager.updateTypeData(tipo, { docSource });
const config = window.dataManager.csvConfigs[tipo];
const docMsg = result.docSource ? `\n📄 Documento: ${result.docSource} (${result.docStatus || '—'})` : '';
alert(`${config.displayName} atualizado com sucesso! ${result.count} itens carregados.${docMsg}`);
// Recarregar painel
fecharPainelDados();
abrirPainelDados();
} catch (error) {
console.error(`❌ Erro ao atualizar ${tipo}:`, error);
alert(`❌ Erro ao atualizar ${tipo}: ` + error.message);
}
}
/**
* Salva a fonte de documento (.md) para um tipo
*/
function salvarDocFonte(tipo) {
try {
const input = document.getElementById(`docfile-${tipo}`);
if (!input) return;
const value = input.value.trim();
const normalized = window.dataManager.normalizeDocSource(value);
// Atualizar metadados mantendo os demais campos
const prev = window.dataManager.getTypeMetadata(tipo) || {};
const updated = { ...prev, docSource: normalized || null };
// Tentar validar imediatamente para refletir status
if (normalized) {
fetch(normalized, { cache: 'no-cache' })
.then(resp => {
updated.docStatus = resp.ok ? 'ok' : `não encontrado (HTTP ${resp.status})`;
window.dataManager.setTypeMetadata(tipo, updated);
alert('📄 Documento fonte definido para ' + tipo + `\nArquivo: ${normalized}\nStatus: ${updated.docStatus}`);
// Atualizar UI do painel
fecharPainelDados();
abrirPainelDados();
})
.catch(err => {
updated.docStatus = 'erro ao carregar';
window.dataManager.setTypeMetadata(tipo, updated);
alert('⚠️ Não foi possível validar o documento. Caminho salvo como: ' + normalized);
fecharPainelDados();
abrirPainelDados();
});
} else {
window.dataManager.setTypeMetadata(tipo, updated);
alert('📄 Documento fonte definido para ' + tipo);
fecharPainelDados();
abrirPainelDados();
}
} catch (e) {
console.error('❌ Erro ao salvar doc fonte:', e);
alert('❌ Erro ao salvar doc fonte: ' + e.message);
}
}
/**
* Limpa cache de um tipo específico
*/
function limparTipoEspecifico(tipo) {
if (!confirm(`🗑️ Deseja limpar o cache de ${tipo}?`)) {
return;
}
try {
localStorage.removeItem(`acoCalcPro_cache_${tipo}`);
alert(`✅ Cache de ${tipo} limpo com sucesso!`);
// Recarregar painel
fecharPainelDados();
abrirPainelDados();
} catch (error) {
console.error(`❌ Erro ao limpar cache de ${tipo}:`, error);
alert(`❌ Erro ao limpar cache: ` + error.message);
}
}
/**
* Exporta dados para JSON
*/
function exportarDados() {
try {
const stats = window.dataManager.getCacheStats();
const exportData = {
metadata: window.dataManager.getMetadata(),
stats: stats,
exportedAt: new Date().toISOString()
};
const blob = new Blob([JSON.stringify(exportData, null, 2)], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `aco-calc-pro-data-export-${Date.now()}.json`;
a.click();
URL.revokeObjectURL(url);
alert('✅ Dados exportados com sucesso!');
} catch (error) {
console.error('❌ Erro ao exportar dados:', error);
alert('❌ Erro ao exportar dados: ' + error.message);
}
}
/**
* Verifica integridade dos dados
*/
async function verificarIntegridade() {
const logEl = document.getElementById('admin-log');
if (logEl) {
logEl.innerHTML = '<div>🔍 Verificando integridade dos dados...</div>';
}
try {
const stats = window.dataManager.getCacheStats();
const issues = [];
// Verificar cada tipo
for (const [key, info] of Object.entries(stats.types)) {
if (!info.cached) {
issues.push(`${info.name}: Sem cache`);
} else if (info.count === 0) {
issues.push(`⚠️ ${info.name}: Cache vazio`);
}
}
if (logEl) {
if (issues.length === 0) {
logEl.innerHTML = '<div>✅ Todos os dados estão íntegros!</div>';
} else {
logEl.innerHTML = `
<div>⚠️ Problemas encontrados:</div>
${issues.map(i => `<div>${i}</div>`).join('')}
`;
}
}
if (issues.length === 0) {
alert('✅ Todos os dados estão íntegros!');
} else {
alert(`⚠️ ${issues.length} problema(s) encontrado(s). Verifique o log.`);
}
} catch (error) {
console.error('❌ Erro ao verificar integridade:', error);
if (logEl) {
logEl.innerHTML = `<div style="color: #f00;">❌ Erro: ${error.message}</div>`;
}
}
}
console.log('✅ Admin Panel carregado');
/**
* Adiciona estilos CSS para o painel de administração
*/
function addAdminPanelStyles() {
// Verificar se os estilos já existem
if (document.querySelector('#admin-panel-styles')) {
return;
}
const styles = document.createElement('style');
styles.id = 'admin-panel-styles';
styles.textContent = `
/* Estilos para campos de formulário do painel admin */
.form-control {
width: 100%;
padding: 8px 12px;
border: 1px solid var(--color-border);
border-radius: 6px;
background: var(--color-bg-2);
color: var(--color-text);
font-size: 14px;
transition: all 0.2s ease;
}
.form-control:focus {
outline: none;
border-color: var(--color-primary);
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
}
.form-control:hover {
border-color: var(--color-primary-light);
}
/* Estilos para checkboxes */
input[type="checkbox"] {
width: 16px;
height: 16px;
accent-color: var(--color-primary);
cursor: pointer;
}
/* Estilos para badges */
.badge {
display: inline-flex;
align-items: center;
padding: 4px 8px;
border-radius: 12px;
font-size: 12px;
font-weight: 500;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.badge-success {
background: rgba(16, 185, 129, 0.1);
color: #10b981;
border: 1px solid rgba(16, 185, 129, 0.2);
}
.badge-error {
background: rgba(239, 68, 68, 0.1);
color: #ef4444;
border: 1px solid rgba(239, 68, 68, 0.2);
}
.badge-primary {
background: rgba(59, 130, 246, 0.1);
color: #3b82f6;
border: 1px solid rgba(59, 130, 246, 0.2);
}
.badge-secondary {
background: rgba(107, 114, 128, 0.1);
color: #6b7280;
border: 1px solid rgba(107, 114, 128, 0.2);
}
/* Estilos para botões pequenos */
.btn-sm {
padding: 6px 12px;
font-size: 12px;
border-radius: 4px;
}
/* Estilos para containers de tabelas */
.table-container {
background: var(--color-bg-2);
border-radius: 8px;
overflow: hidden;
border: 1px solid var(--color-border);
}
/* Cabeçalhos de tabela no painel Admin: alto contraste */
.modal-admin .table-container th {
background: var(--color-bg-2);
color: var(--color-gray-200);
font-weight: 700;
border-bottom: 2px solid var(--color-border);
text-align: left;
padding: 10px;
}
.modal-admin .table-container th sup {
color: inherit;
font-weight: 700;
opacity: 1;
}
/* Animações para inputs */
@keyframes input-focus {
from {
transform: scale(0.98);
opacity: 0.8;
}
to {
transform: scale(1);
opacity: 1;
}
}
.form-control:focus {
animation: input-focus 0.2s ease-out;
}
/* Responsividade para o painel */
@media (max-width: 768px) {
.modal-content {
margin: 10px;
max-height: 90vh;
overflow-y: auto;
}
.form-control {
font-size: 16px; /* Previne zoom no iOS */
}
}
/* Branding preview */
.branding-logo-preview {
width: 180px;
height: 180px;
border: 1px dashed var(--color-border);
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
background: var(--color-bg-2);
}
.branding-logo-preview img {
max-width: 100%;
max-height: 100%;
object-fit: contain;
border-radius: 8px;
}
.branding-logo-fallback {
font-size: 64px;
}
`;
document.head.appendChild(styles);
}
// ========================================
// ATALHO DE TECLADO
// ========================================
/**
* Atalho de teclado: Ctrl + Shift + D
*/
document.addEventListener('keydown', function(e) {
if (e.ctrlKey && e.shiftKey && e.key === 'D') {
e.preventDefault();
abrirPainelDados();
console.log('🗄️ Painel de dados aberto via atalho de teclado');
}
});
console.log('💡 Dica: Pressione Ctrl + Shift + D para abrir o painel de administração de dados');
/**
* Salva uma configuração específica
* @param {string} key - Chave da configuração (ex: 'app.name')
* @param {*} value - Valor a ser salvo
*/
async function salvarConfiguracao(key, value) {
try {
if (!adminConfigManager) {
console.error('❌ AdminConfigManager não inicializado');
return;
}
// Atualizar configuração
adminConfigManager.updateConfig(key, value);
// Mostrar feedback visual se o ToastManager estiver disponível
if (window.toastManager) {
window.toastManager.success(`Configuração "${key}" salva com sucesso!`);
} else {
console.log(`✅ Configuração "${key}" salva com sucesso`);
}
} catch (error) {
console.error('❌ Erro ao salvar configuração:', error);
if (window.toastManager) {
window.toastManager.error(`Erro ao salvar configuração: ${error.message}`);
}
}
}
/**
* Salva o logotipo enviado no campo de branding
*/
async function salvarLogoBranding() {
try {
const input = document.getElementById('branding-logo-input');
if (!input || !input.files || !input.files[0]) {
alert('Selecione um arquivo de imagem (PNG ou SVG).');
return;
}
const file = input.files[0];
const reader = new FileReader();
reader.onload = async (e) => {
const dataUrl = e.target.result;
// Persistir em admin config
await salvarConfiguracao('branding.logo', dataUrl);
// Atualizar preview
const preview = document.getElementById('branding-logo-preview');
if (preview) {
preview.innerHTML = `<img src="${dataUrl}" alt="Preview">`;
}
// Aplicar imediatamente
if (window.applyAdminConfig) window.applyAdminConfig();
if (window.toastManager) window.toastManager.success('Logotipo salvo e aplicado!');
};
reader.readAsDataURL(file);
} catch (error) {
console.error('❌ Erro ao salvar logotipo:', error);
if (window.toastManager) {
window.toastManager.error('Erro ao salvar logotipo: ' + error.message);
}
}
}
/**
* Remove o logotipo personalizado e restaura o padrão
*/
function removerLogoBranding() {
try {
salvarConfiguracao('branding.logo', null);
const preview = document.getElementById('branding-logo-preview');
if (preview) {
preview.innerHTML = '<div class="branding-logo-fallback">🏗️</div>';
}
if (window.applyAdminConfig) window.applyAdminConfig();
if (window.toastManager) window.toastManager.info('Logotipo restaurado para o padrão');
} catch (error) {
console.error('❌ Erro ao remover logotipo:', error);
}
}
/**
* Aplica um tema específico
* @param {string} themeName - Nome do tema (dark, light, auto)
*/
function aplicarTema(themeName) {
try {
if (!adminConfigManager) {
console.error('❌ AdminConfigManager não inicializado');
return;
}
// Aplicar tema usando o AdminConfigManager
adminConfigManager.applyTheme(themeName);
console.log(`✅ Tema "${themeName}" aplicado com sucesso`);
} catch (error) {
console.error('❌ Erro ao aplicar tema:', error);
if (window.toastManager) {
window.toastManager.error(`Erro ao aplicar tema: ${error.message}`);
}
}
}
/**
* Cria um backup manual
*/
async function criarBackupManual() {
try {
if (!backupManager) {
console.error('❌ BackupManager não inicializado');
if (window.toastManager) {
window.toastManager.error('Sistema de backup não inicializado');
}
return;
}
// Mostrar loading
const loadingToast = window.toastManager ? window.toastManager.loading('Criando backup...') : null;
// Criar backup
const backup = await backupManager.createBackup();
// Remover loading
if (loadingToast && window.toastManager) {
window.toastManager.removeToast(loadingToast);
}
// Mostrar sucesso
if (window.toastManager) {
window.toastManager.success(`Backup criado com sucesso! ID: ${backup.id}`);
}
console.log('✅ Backup manual criado:', backup);
} catch (error) {
console.error('❌ Erro ao criar backup:', error);
if (window.toastManager) {
window.toastManager.error(`Erro ao criar backup: ${error.message}`);
}
}
}
/**
* Abre o gerenciador de backups
*/
function abrirGerenciadorBackups() {
try {
if (!backupManager) {
console.error('❌ BackupManager não inicializado');
if (window.toastManager) {
window.toastManager.error('Sistema de backup não inicializado');
}
return;
}
// Obter lista de backups
const backups = backupManager.getBackups();
if (backups.length === 0) {
if (window.toastManager) {
window.toastManager.info('Nenhum backup disponível');
}
return;
}
// Criar modal de gerenciamento
const modalHTML = `
<div class="modal active" id="modal-backup-manager" onclick="fecharGerenciadorBackups(event)">
<div class="modal-content" onclick="event.stopPropagation()" style="max-width: 600px;">
<div class="modal-header">
<div class="modal-title">📂 Gerenciador de Backups</div>
<button class="close-btn" onclick="fecharGerenciadorBackups()">×</button>
</div>
<div class="modal-body">
<div class="table-container">
<table style="width: 100%; border-collapse: collapse;">
<thead>
<tr style="background: var(--color-bg-2);">
<th style="padding: 12px; text-align: left; border-bottom: 2px solid var(--color-border);">Data</th>
<th style="padding: 12px; text-align: center; border-bottom: 2px solid var(--color-border);">Tipo</th>
<th style="padding: 12px; text-align: center; border-bottom: 2px solid var(--color-border);">Tamanho</th>
<th style="padding: 12px; text-align: center; border-bottom: 2px solid var(--color-border);">Ações</th>
</tr>
</thead>
<tbody>
${backups.map(backup => `
<tr style="border-bottom: 1px solid var(--color-border);">
<td style="padding: 12px;">${(() => { const d = Number(backup.createdAt ?? backup.timestamp); return isNaN(d) ? '—' : new Date(d).toLocaleString('pt-BR'); })()}</td>
<td style="padding: 12px; text-align: center;">
<span class="badge badge-${backup.type === 'manual' ? 'primary' : 'secondary'}">
${backup.type === 'manual' ? 'MANUAL' : 'AUTOMÁTICO'}
</span>
</td>
<td style="padding: 12px; text-align: center;">${(() => { const sb = Number(backup.sizeBytes); if (backup.size) return backup.size; return isNaN(sb) ? '—' : (window.backupManager ? window.backupManager.formatBytes(sb) : `${sb} B`); })()}</td>
<td style="padding: 12px; text-align: center;">
<button class="btn btn-sm btn-success" onclick="restaurarBackup('${backup.id || backup.timestamp}')">
↻ Restaurar
</button>
<button class="btn btn-sm btn-danger" onclick="removerBackup('${backup.id || backup.timestamp}')">
🗑️ Remover
</button>
</td>
</tr>
`).join('')}
</tbody>
</table>
</div>
</div>
<div class="modal-footer">
<button class="btn btn-secondary" onclick="fecharGerenciadorBackups()">Fechar</button>
</div>
</div>
</div>
`;
// Adicionar modal ao body
document.body.insertAdjacentHTML('beforeend', modalHTML);
} catch (error) {
console.error('❌ Erro ao abrir gerenciador de backups:', error);
if (window.toastManager) {
window.toastManager.error(`Erro ao abrir gerenciador: ${error.message}`);
}
}
}
/**
* Fecha o gerenciador de backups
*/
function fecharGerenciadorBackups(event) {
if (event && event.target !== event.currentTarget) return;
const modal = document.getElementById('modal-backup-manager');
if (modal) {
modal.remove();
}
}
/**
* Restaura um backup específico
* @param {string} backupId - ID do backup
*/
async function restaurarBackup(backupId) {
try {
if (!backupManager) {
console.error('❌ BackupManager não inicializado');
return;
}
const resumo = backupManager.getBackup(backupId);
const dataStr = resumo ? new Date(resumo.createdAt || resumo.timestamp).toLocaleString('pt-BR') : 'desconhecida';
if (!confirm(`⚠️ Tem certeza que deseja restaurar o backup de ${dataStr}?\n\nIsso substituirá todas as configurações atuais.`)) {
return;
}
// Mostrar loading
const loadingToast = window.toastManager ? window.toastManager.loading('Restaurando backup...') : null;
// Restaurar backup
await backupManager.restoreBackup(backupId);
// Remover loading
if (loadingToast && window.toastManager) {
window.toastManager.removeToast(loadingToast);
}
// Mostrar sucesso
if (window.toastManager) {
window.toastManager.success('Backup restaurado com sucesso!');
}
// Recarregar painel
fecharGerenciadorBackups();
fecharPainelDados();
setTimeout(() => abrirPainelDados(), 500);
console.log('✅ Backup restaurado:', backupId);
} catch (error) {
console.error('❌ Erro ao restaurar backup:', error);
if (window.toastManager) {
window.toastManager.error(`Erro ao restaurar backup: ${error.message}`);
}
}
}
/**
* Remove um backup específico
* @param {string} backupId - ID do backup
*/
function removerBackup(backupId) {
try {
if (!backupManager) {
console.error('❌ BackupManager não inicializado');
return;
}
if (!confirm('⚠️ Tem certeza que deseja remover este backup?')) {
return;
}
// Remover backup
backupManager.removeBackup(backupId);
// Mostrar sucesso
if (window.toastManager) {
window.toastManager.success('Backup removido com sucesso!');
}
// Recarregar gerenciador
fecharGerenciadorBackups();
setTimeout(() => abrirGerenciadorBackups(), 100);
console.log('✅ Backup removido:', backupId);
} catch (error) {
console.error('❌ Erro ao remover backup:', error);
if (window.toastManager) {
window.toastManager.error(`Erro ao remover backup: ${error.message}`);
}
}
}
/**
* Exporta as configurações atuais
*/
function exportarConfiguracoes() {
try {
if (!adminConfigManager) {
console.error('❌ AdminConfigManager não inicializado');
if (window.toastManager) {
window.toastManager.error('Sistema de configurações não inicializado');
}
return;
}
// Obter configurações atuais
const config = adminConfigManager.getConfig();
// Criar blob e download
const blob = new Blob([JSON.stringify(config, null, 2)], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `aco-calc-pro-config-${Date.now()}.json`;
a.click();
URL.revokeObjectURL(url);
// Mostrar sucesso
if (window.toastManager) {
window.toastManager.success('Configurações exportadas com sucesso!');
}
console.log('✅ Configurações exportadas');
} catch (error) {
console.error('❌ Erro ao exportar configurações:', error);
if (window.toastManager) {
window.toastManager.error(`Erro ao exportar configurações: ${error.message}`);
}
}
}
/**
* Importa configurações de um arquivo
*/
function importarConfiguracoes() {
try {
if (!adminConfigManager) {
console.error('❌ AdminConfigManager não inicializado');
if (window.toastManager) {
window.toastManager.error('Sistema de configurações não inicializado');
}
return;
}
// Criar input file
const input = document.createElement('input');
input.type = 'file';
input.accept = '.json';
input.onchange = async (e) => {
const file = e.target.files[0];
if (!file) return;
try {
// Ler arquivo
const text = await file.text();
const config = JSON.parse(text);
// Confirmar importação
if (!confirm('⚠️ Tem certeza que deseja importar estas configurações?\n\nIsso substituirá as configurações atuais.')) {
return;
}
// Importar configurações
await adminConfigManager.setConfig(config);
// Mostrar sucesso
if (window.toastManager) {
window.toastManager.success('Configurações importadas com sucesso!');
}
// Recarregar painel
fecharPainelDados();
setTimeout(() => abrirPainelDados(), 500);
console.log('✅ Configurações importadas');
} catch (error) {
console.error('❌ Erro ao importar configurações:', error);
if (window.toastManager) {
window.toastManager.error(`Erro ao importar configurações: ${error.message}`);
}
}
};
input.click();
} catch (error) {
console.error('❌ Erro ao importar configurações:', error);
if (window.toastManager) {
window.toastManager.error(`Erro ao importar configurações: ${error.message}`);
}
}
}
/**
* Reseta as configurações para os valores padrão
*/
function resetarConfiguracoes() {
try {
if (!adminConfigManager) {
console.error('❌ AdminConfigManager não inicializado');
if (window.toastManager) {
window.toastManager.error('Sistema de configurações não inicializado');
}
return;
}
if (!confirm('⚠️ Tem certeza que deseja resetar todas as configurações?\n\nIsso irá restaurar os valores padrão.')) {
return;
}
// Resetar configurações
adminConfigManager.resetConfig();
// Mostrar sucesso
if (window.toastManager) {
window.toastManager.success('Configurações resetadas com sucesso!');
}
// Recarregar painel
fecharPainelDados();
setTimeout(() => abrirPainelDados(), 500);
console.log('✅ Configurações resetadas');
} catch (error) {
console.error('❌ Erro ao resetar configurações:', error);
if (window.toastManager) {
window.toastManager.error(`Erro ao resetar configurações: ${error.message}`);
}
}
}