643 lines
23 KiB
JavaScript
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'); |