/** * AdminConfigManager - Gerenciador de Configurações Administrativas * Responsável por persistir e gerenciar todas as configurações do painel administrativo */ class AdminConfigManager { constructor() { this.configKey = 'acoCalcPro_admin_config'; this.version = '1.0.0'; this.defaultConfig = { // Informações básicas da aplicação appName: 'SteelBase', appSubtitle: 'Plataforma Técnica com Base de Dados de Materiais Brasileiros', footerText: '© 2025 SteelBase v7.5 PROFESSIONAL EDITION', // Branding (logotipo e identidade) branding: { logo: null, // DataURL (PNG/SVG) do logotipo logoAlt: 'Logo', themeColor: '#21808d', backgroundColor: '#fcfcf9', useInPWA: true, useInReports: true, useInFooter: true }, // Configurações de tema e modo themeDefault: 'escuro', modeDefault: 'simples', // Visibilidade de ferramentas (baseado nas ferramentas existentes) toolsVisibility: { 'cev': true, 'seletor': true, 'equivalencias': true, 'comparativo': true, 'parafusos': true, 'layout': true, 'cantoneiras': true, 'perfis-i': true, 'perfis-u': true, 'perfis-w': true, 'perfis-hp': true, 'barras-chatas': true, 'barras-redondas': true, 'barras-quadradas': true, 'barras-hexagonais': true, 'tubos-circulares': true, 'tubos-quadrados': true, 'tubos-retangulares': true, 'chapas': true, 'solda': true, 'tintas': true, 'fixadores': true, 'calculadoras': true }, // Configurações de atualização e backup dataRefreshInterval: 24, // horas autoBackup: true, backupInterval: 7, // dias lastBackup: null, // Estado da UI uiState: { activeSidebarTab: 0, collapsedSections: [], appliedFilters: {}, tableSort: {}, columnWidths: {}, scrollPositions: {} }, // Versionamento version: this.version }; // Inicializar com migração se necessário this.initialize(); } /** * Inicializa o gerenciador, executando migrações se necessário */ initialize() { try { const savedVersion = this.getVersion(); if (savedVersion !== this.version) { console.log(`🔄 Migrando config de ${savedVersion} para ${this.version}`); this.migrateConfig(savedVersion); } } catch (error) { console.warn('⚠️ Erro ao inicializar AdminConfigManager:', error); } } /** * Migra configurações de versões antigas * @param {string} fromVersion - Versão de origem */ migrateConfig(fromVersion) { try { console.log(`📦 Iniciando migração de ${fromVersion} para ${this.version}`); const currentConfig = this.getConfig(); let migratedConfig = { ...currentConfig }; // Migração de versões muito antigas (0.x.x) if (fromVersion.startsWith('0.')) { console.log('📋 Migrando de versão 0.x.x - aplicando estrutura padrão'); // Garantir que todas as chaves padrão existam migratedConfig = { ...this.defaultConfig, ...migratedConfig, version: this.version, lastModified: Date.now() }; // Migrar configurações antigas se existirem if (migratedConfig.appTitle && !migratedConfig.appName) { migratedConfig.appName = migratedConfig.appTitle; delete migratedConfig.appTitle; } if (migratedConfig.defaultTheme && !migratedConfig.themeDefault) { migratedConfig.themeDefault = migratedConfig.defaultTheme; delete migratedConfig.defaultTheme; } } // Migração de versão 1.x.x para versões mais recentes if (fromVersion.startsWith('1.') && this.version.startsWith('2.')) { console.log('🆙 Atualizando estrutura para versão 2.x.x'); // Adicionar novos campos se não existirem if (!migratedConfig.toolsVisibility) { migratedConfig.toolsVisibility = { ...this.defaultConfig.toolsVisibility }; } if (!migratedConfig.backupSettings) { migratedConfig.backupSettings = { ...this.defaultConfig.backupSettings }; } if (!migratedConfig.uiState) { migratedConfig.uiState = { ...this.defaultConfig.uiState }; } } // Sempre atualizar versão e timestamp migratedConfig.version = this.version; migratedConfig.lastModified = Date.now(); // Salvar configurações migradas this.saveConfig(migratedConfig); console.log('✅ Migração concluída com sucesso'); } catch (error) { console.error('❌ Erro durante migração:', error); // Em caso de erro, resetar para config padrão this.resetConfig(); } } /** * Obtém a versão atual das configurações salvas */ getVersion() { try { const saved = localStorage.getItem(this.configKey); if (saved) { const parsed = JSON.parse(saved); return parsed.version || '0.0.0'; } } catch (error) { console.warn('⚠️ Erro ao obter versão:', error); } return '0.0.0'; } /** * Salva as configurações no localStorage * @param {Object} config - Configurações a serem salvas * @returns {Object} Configurações salvas */ saveConfig(config) { try { const currentConfig = this.getConfig(); const configToSave = { ...currentConfig, ...config, version: this.version, lastModified: Date.now() }; localStorage.setItem(this.configKey, JSON.stringify(configToSave)); console.log('✅ Configurações admin salvas com sucesso'); // Disparar evento de mudança de config this.notifyConfigChange(configToSave); return configToSave; } catch (error) { console.error('❌ Erro ao salvar configurações:', error); throw new Error('Falha ao salvar configurações: ' + error.message); } } /** * Obtém as configurações salvas ou retorna as padrões * @returns {Object} Configurações atuais */ getConfig() { try { const saved = localStorage.getItem(this.configKey); if (saved) { const parsed = JSON.parse(saved); // Validar estrutura básica if (this.validateConfig(parsed)) { return { ...this.defaultConfig, ...parsed }; } } } catch (error) { console.warn('⚠️ Erro ao carregar configurações, usando padrões:', error); } return { ...this.defaultConfig }; } /** * Reseta as configurações para os valores padrão * @returns {Object} Configurações padrão */ resetConfig() { try { localStorage.setItem(this.configKey, JSON.stringify(this.defaultConfig)); console.log('🔄 Configurações resetadas para padrão'); // Disparar evento de mudança de config this.notifyConfigChange(this.defaultConfig); return { ...this.defaultConfig }; } catch (error) { console.error('❌ Erro ao resetar configurações:', error); throw new Error('Falha ao resetar configurações: ' + error.message); } } /** * Atualiza uma configuração específica * @param {string} key - Chave da configuração * @param {*} value - Novo valor * @returns {Object} Configurações atualizadas */ updateConfig(key, value) { const config = this.getConfig(); // Validar chaves específicas if (!this.validateKeyValue(key, value)) { throw new Error(`Valor inválido para ${key}: ${value}`); } // Atualizar valor aninhado se necessário if (key.includes('.')) { this.setNestedValue(config, key, value); } else { config[key] = value; } return this.saveConfig(config); } /** * Obtém uma configuração específica * @param {string} key - Chave da configuração * @param {*} defaultValue - Valor padrão se não existir * @returns {*} Valor da configuração */ getConfigValue(key, defaultValue = null) { const config = this.getConfig(); if (key.includes('.')) { return this.getNestedValue(config, key, defaultValue); } return config[key] !== undefined ? config[key] : defaultValue; } /** * Define visibilidade de uma ferramenta específica * @param {string} toolName - Nome da ferramenta * @param {boolean} visible - Visibilidade * @returns {Object} Configurações atualizadas */ setToolVisibility(toolName, visible) { return this.updateConfig(`toolsVisibility.${toolName}`, visible); } /** * Obtém visibilidade de uma ferramenta * @param {string} toolName - Nome da ferramenta * @returns {boolean} Visibilidade da ferramenta */ getToolVisibility(toolName) { return this.getConfigValue(`toolsVisibility.${toolName}`, true); } /** * Aplica as configurações ao aplicativo */ applyConfig() { const config = this.getConfig(); try { // Aplicar nome do aplicativo if (config.appName) { document.title = config.appName; const titleElements = document.querySelectorAll('.app-title'); titleElements.forEach(el => el.textContent = config.appName); } // Aplicar subtítulo if (config.appSubtitle) { const subtitleElements = document.querySelectorAll('.app-subtitle'); subtitleElements.forEach(el => el.textContent = config.appSubtitle); } // Aplicar texto do rodapé if (config.footerText) { const footerEl = document.getElementById('appFooter'); if (footerEl) { const p = footerEl.querySelector('p'); if (p) p.textContent = config.footerText; else footerEl.textContent = config.footerText; } } // Aplicar visibilidade de ferramentas this.applyToolsVisibility(config.toolsVisibility); // Aplicar tema padrão if (config.themeDefault && window.themeManager) { window.themeManager.setTheme(config.themeDefault); } // Aplicar modo padrão (simples/expert) if (config.modeDefault) { const isExpertDesired = ['expert', 'experto'].includes(config.modeDefault); const isExpertActive = document.documentElement.classList.contains('expert-mode'); if (typeof window.toggleExpertMode === 'function' && isExpertDesired !== isExpertActive) { window.toggleExpertMode(); } } // Aplicar branding (logotipo, favicon, manifest) this.applyBranding(); // Refiltrar ferramentas após aplicar modo/tema if (typeof window.filterToolsByMode === 'function') { window.filterToolsByMode(); } console.log('✅ Configurações aplicadas com sucesso'); } catch (error) { console.error('❌ Erro ao aplicar configurações:', error); } } /** * Aplica visibilidade de ferramentas * @param {Object} toolsVisibility - Objeto com visibilidade das ferramentas */ applyToolsVisibility(toolsVisibility) { const expertActive = document.documentElement.classList.contains('expert-mode'); Object.entries(toolsVisibility).forEach(([tool, visible]) => { const displayValue = expertActive ? '' : (visible ? '' : 'none'); const toolElement = document.querySelector(`[data-tool="${tool}"]`); if (toolElement) { toolElement.style.display = displayValue; } // Também atualizar navegação se existir const navElement = document.querySelector(`[data-nav="${tool}"]`); if (navElement) { navElement.style.display = displayValue; } }); } /** * Valida a estrutura das configurações * @param {Object} config - Configurações a validar * @returns {boolean} Se é válido */ validateConfig(config) { if (!config || typeof config !== 'object') { return false; } // Verificar campos obrigatórios const requiredFields = ['appName', 'version']; for (const field of requiredFields) { if (!config[field]) { console.warn(`⚠️ Campo obrigatório ausente: ${field}`); return false; } } return true; } /** * Valida chave e valor específicos * @param {string} key - Chave a validar * @param {*} value - Valor a validar * @returns {boolean} Se é válido */ validateKeyValue(key, value) { // Validações específicas por tipo de config switch (key) { case 'themeDefault': return ['escuro', 'claro'].includes(value); case 'modeDefault': return ['simples', 'experto', 'expert'].includes(value); case 'dataRefreshInterval': return typeof value === 'number' && value >= 1 && value <= 168; // 1 hora a 1 semana case 'backupInterval': return typeof value === 'number' && value >= 1 && value <= 30; // 1 a 30 dias case 'autoBackup': return typeof value === 'boolean'; case 'branding.logo': return value === null || typeof value === 'string'; default: return true; // Aceitar outras chaves } } /** * Define valor em propriedade aninhada * @param {Object} obj - Objeto alvo * @param {string} path - Caminho (ex: 'toolsVisibility.cev') * @param {*} value - Valor a definir */ setNestedValue(obj, path, value) { const keys = path.split('.'); let current = obj; for (let i = 0; i < keys.length - 1; i++) { if (!current[keys[i]]) { current[keys[i]] = {}; } current = current[keys[i]]; } current[keys[keys.length - 1]] = value; } /** * Obtém valor de propriedade aninhada * @param {Object} obj - Objeto alvo * @param {string} path - Caminho (ex: 'toolsVisibility.cev') * @param {*} defaultValue - Valor padrão * @returns {*} Valor obtido */ getNestedValue(obj, path, defaultValue = null) { const keys = path.split('.'); let current = obj; for (const key of keys) { if (current && typeof current === 'object' && key in current) { current = current[key]; } else { return defaultValue; } } return current; } /** * Notifica mudanças de configuração via eventos customizados * @param {Object} newConfig - Nova configuração */ notifyConfigChange(newConfig) { const event = new CustomEvent('adminConfigChanged', { detail: { config: newConfig }, bubbles: true }); document.dispatchEvent(event); } /** * Exporta as configurações como JSON * @returns {string} Configurações em formato JSON */ exportConfig() { const config = this.getConfig(); return JSON.stringify(config, null, 2); } /** * Importa configurações de JSON * @param {string} jsonString - Configurações em formato JSON * @returns {Object} Configurações importadas */ importConfig(jsonString) { try { const importedConfig = JSON.parse(jsonString); if (!this.validateConfig(importedConfig)) { throw new Error('Configurações inválidas'); } // Atualizar versão para a atual importedConfig.version = this.version; return this.saveConfig(importedConfig); } catch (error) { console.error('❌ Erro ao importar configurações:', error); throw new Error('Falha ao importar configurações: ' + error.message); } } /** * Obtém estatísticas do sistema de config * @returns {Object} Estatísticas */ getStats() { const config = this.getConfig(); const savedSize = localStorage.getItem(this.configKey)?.length || 0; return { version: this.version, savedVersion: config.version, lastModified: config.lastModified || null, size: savedSize, toolsCount: Object.keys(config.toolsVisibility).length, visibleTools: Object.values(config.toolsVisibility).filter(v => v).length }; } /** * Aplica branding: logotipo no header, favicon e manifest PWA dinamicamente */ applyBranding() { try { const config = this.getConfig(); const branding = config.branding || {}; // Atualizar tema/cores meta if (branding.themeColor) { const metaTheme = document.querySelector('meta[name="theme-color"]'); if (metaTheme) metaTheme.setAttribute('content', branding.themeColor); } // Header logo const logoEl = document.getElementById('appLogo'); if (logoEl) { if (branding.logo) { logoEl.innerHTML = `${branding.logoAlt || 'Logo'}`; } else { // Fallback texto padrão logoEl.textContent = '🏗️ ' + (config.appName || 'SteelBase'); } } // Favicon if (branding.logo) { let favicon = document.querySelector('link[rel="icon"]'); if (!favicon) { favicon = document.createElement('link'); favicon.setAttribute('rel', 'icon'); document.head.appendChild(favicon); } favicon.setAttribute('href', branding.logo); } // Manifest PWA dinâmico (ícones com base no logo) if (branding.useInPWA) { const icons = []; if (branding.logo) { const isSvg = branding.logo.startsWith('data:image/svg'); const type = isSvg ? 'image/svg+xml' : 'image/png'; icons.push({ src: branding.logo, sizes: '192x192', type, purpose: 'any maskable' }); icons.push({ src: branding.logo, sizes: '512x512', type, purpose: 'any maskable' }); } const manifestObj = { name: config.appName || 'SteelBase', short_name: (config.appName || 'SteelBase'), description: 'Plataforma profissional de cálculos de engenharia estrutural', start_url: '/', display: 'standalone', background_color: branding.backgroundColor || '#fcfcf9', theme_color: branding.themeColor || '#21808d', orientation: 'any', icons: icons.length ? icons : undefined, categories: ['productivity', 'utilities', 'business'], lang: 'pt-BR', dir: 'ltr' }; const manifestJson = JSON.stringify(manifestObj); const blob = new Blob([manifestJson], { type: 'application/json' }); const url = URL.createObjectURL(blob); let manifestLink = document.querySelector('link[rel="manifest"]'); if (!manifestLink) { manifestLink = document.createElement('link'); manifestLink.setAttribute('rel', 'manifest'); document.head.appendChild(manifestLink); } manifestLink.setAttribute('href', url); } } catch (error) { console.warn('⚠️ Erro ao aplicar branding:', error); } } // Placeholder: caso necessário, futuramente podemos redimensionar ícones. } // Criar instância global window.adminConfigManager = new AdminConfigManager(); // Função global para aplicar configurações (chamada pelo main.js) window.applyAdminConfig = async function() { try { // Aplicar config padrão window.adminConfigManager.applyConfig(); // Branding pode ter necessidade de gerar ícones async // Se os ícones forem Promises, aguardar (método atual retorna sync exceto resize async) // Para simplificar, reaplicar branding após pequeno atraso se houver logo const cfg = window.adminConfigManager.getConfig(); if (cfg.branding && cfg.branding.logo) { // Pequeno atraso para garantir DOM pronto setTimeout(() => window.adminConfigManager.applyBranding(), 50); } } catch (err) { console.warn('⚠️ Erro em applyAdminConfig:', err); } }; console.log('🚀 AdminConfigManager inicializado com sucesso');