Files
SteelBase/js/core/admin-config-manager.js

643 lines
23 KiB
JavaScript

/**
* 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 = `<img src="${branding.logo}" alt="${branding.logoAlt || 'Logo'}" class="app-logo-img">`;
} 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');