Fix script paths and move assets to public/ folder for Vite build compatibility
This commit is contained in:
643
public/js/core/admin-config-manager.js
Normal file
643
public/js/core/admin-config-manager.js
Normal file
@@ -0,0 +1,643 @@
|
||||
/**
|
||||
* 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');
|
||||
669
public/js/core/backup-manager.js
Normal file
669
public/js/core/backup-manager.js
Normal file
@@ -0,0 +1,669 @@
|
||||
/**
|
||||
* BackupManager - Gerenciador de Backup e Restauração
|
||||
* Responsável por criar, gerenciar e restaurar backups do sistema
|
||||
*/
|
||||
class BackupManager {
|
||||
constructor() {
|
||||
this.backupKey = 'acoCalcPro_backups';
|
||||
this.maxBackups = 5;
|
||||
this.autoBackupKey = 'acoCalcPro_auto_backup';
|
||||
this.version = '1.0.0';
|
||||
|
||||
// Inicializar
|
||||
this.initialize();
|
||||
}
|
||||
|
||||
/**
|
||||
* Inicializa o gerenciador de backup
|
||||
*/
|
||||
initialize() {
|
||||
try {
|
||||
// Limpar backups antigos se necessário
|
||||
this.cleanupOldBackups();
|
||||
console.log('🔄 BackupManager inicializado com sucesso');
|
||||
} catch (error) {
|
||||
console.error('❌ Erro ao inicializar BackupManager:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Cria um backup completo do sistema
|
||||
* @param {string} description - Descrição opcional do backup
|
||||
* @returns {Object} Backup criado
|
||||
*/
|
||||
async createBackup(description = 'Backup manual') {
|
||||
try {
|
||||
console.log('📦 Criando backup do sistema...');
|
||||
|
||||
// Coletar todos os dados necessários
|
||||
const now = Date.now();
|
||||
const backupData = {
|
||||
id: `bkp-${now}`,
|
||||
timestamp: Number(now),
|
||||
createdAt: Number(now),
|
||||
type: description && description.toLowerCase().includes('auto') ? 'automatic' : 'manual',
|
||||
version: this.version,
|
||||
description: description,
|
||||
data: {
|
||||
// Configurações administrativas
|
||||
adminConfig: window.adminConfigManager ? window.adminConfigManager.getConfig() : null,
|
||||
|
||||
// Preferências do usuário
|
||||
userPreferences: this.getUserPreferences(),
|
||||
|
||||
// Cache stats do DataManager
|
||||
cacheStats: window.dataManager ? window.dataManager.getCacheStats() : null,
|
||||
|
||||
// Estado da aplicação
|
||||
appState: this.getAppState(),
|
||||
|
||||
// Dados do State Manager
|
||||
stateData: window.stateManager ? window.stateManager.getAllState() : null,
|
||||
|
||||
// Dados do Cache Manager
|
||||
cacheData: window.cacheManager ? await this.getCacheData() : null
|
||||
}
|
||||
};
|
||||
const sizeBytes = Number(JSON.stringify(backupData).length);
|
||||
backupData.sizeBytes = sizeBytes;
|
||||
backupData.size = this.formatBytes(sizeBytes);
|
||||
|
||||
// Obter backups existentes
|
||||
const backups = this.getBackups();
|
||||
|
||||
// Adicionar novo backup no início
|
||||
backups.unshift(backupData);
|
||||
|
||||
// Limitar número de backups
|
||||
if (backups.length > this.maxBackups) {
|
||||
const removed = backups.splice(this.maxBackups);
|
||||
console.log(`🗑️ ${removed.length} backup(s) antigo(s) removido(s)`);
|
||||
}
|
||||
|
||||
// Salvar no localStorage
|
||||
localStorage.setItem(this.backupKey, JSON.stringify(backups));
|
||||
|
||||
// Atualizar último backup nas configurações
|
||||
if (window.adminConfigManager) {
|
||||
window.adminConfigManager.updateConfig('lastBackup', backupData.timestamp);
|
||||
}
|
||||
|
||||
console.log('✅ Backup criado com sucesso:', new Date(backupData.timestamp).toLocaleString());
|
||||
|
||||
// Notificar sucesso
|
||||
if (window.toastManager) {
|
||||
window.toastManager.success('Backup criado com sucesso!');
|
||||
}
|
||||
|
||||
return backupData;
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ Erro ao criar backup:', error);
|
||||
|
||||
// Notificar erro
|
||||
if (window.toastManager) {
|
||||
window.toastManager.error('Erro ao criar backup: ' + error.message);
|
||||
}
|
||||
|
||||
throw new Error('Falha ao criar backup: ' + error.message);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtém todos os backups salvos
|
||||
* @returns {Array} Lista de backups
|
||||
*/
|
||||
getBackups() {
|
||||
try {
|
||||
const saved = localStorage.getItem(this.backupKey);
|
||||
if (saved) {
|
||||
const raw = JSON.parse(saved);
|
||||
if (Array.isArray(raw)) {
|
||||
const normalized = raw.map(b => {
|
||||
const tsRaw = (b && b.timestamp != null) ? b.timestamp : Date.now();
|
||||
let ts;
|
||||
if (typeof tsRaw === 'number') {
|
||||
ts = tsRaw;
|
||||
} else if (typeof tsRaw === 'string') {
|
||||
const parsed = Date.parse(tsRaw);
|
||||
ts = isNaN(parsed) ? Number(tsRaw) : parsed;
|
||||
} else {
|
||||
ts = Date.now();
|
||||
}
|
||||
if (isNaN(ts)) ts = Date.now();
|
||||
const id = (b && b.id) ? b.id : `bkp-${ts}`;
|
||||
const createdAtRaw = (b && b.createdAt != null) ? b.createdAt : ts;
|
||||
let createdAt;
|
||||
if (typeof createdAtRaw === 'number') {
|
||||
createdAt = createdAtRaw;
|
||||
} else if (typeof createdAtRaw === 'string') {
|
||||
const parsedCA = Date.parse(createdAtRaw);
|
||||
createdAt = isNaN(parsedCA) ? Number(createdAtRaw) : parsedCA;
|
||||
} else {
|
||||
createdAt = ts;
|
||||
}
|
||||
if (isNaN(createdAt)) createdAt = ts;
|
||||
const type = b.type || 'manual';
|
||||
const version = b.version || this.version;
|
||||
const description = b.description || '';
|
||||
const data = b.data || {};
|
||||
let sizeBytes = Number(b.sizeBytes);
|
||||
if (isNaN(sizeBytes) || !sizeBytes) {
|
||||
sizeBytes = JSON.stringify(b).length;
|
||||
}
|
||||
const size = b.size || this.formatBytes(sizeBytes);
|
||||
return { id, timestamp: ts, createdAt, type, version, description, data, sizeBytes, size };
|
||||
});
|
||||
return normalized.filter(backup => this.isValidBackup(backup));
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('⚠️ Erro ao carregar backups:', error);
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Restaura um backup específico
|
||||
* @param {Object|string} backup - Backup a restaurar ou timestamp
|
||||
* @returns {boolean} Sucesso da restauração
|
||||
*/
|
||||
async restoreBackup(backup) {
|
||||
try {
|
||||
let backupToRestore;
|
||||
|
||||
// Se for timestamp, encontrar o backup correspondente
|
||||
if (typeof backup === 'string' || typeof backup === 'number') {
|
||||
const backups = this.getBackups();
|
||||
if (typeof backup === 'string' && isNaN(parseInt(backup))) {
|
||||
backupToRestore = backups.find(b => b.id === backup);
|
||||
} else {
|
||||
const timestamp = typeof backup === 'string' ? parseInt(backup) : backup;
|
||||
backupToRestore = backups.find(b => b.timestamp === timestamp);
|
||||
}
|
||||
|
||||
if (!backupToRestore) {
|
||||
throw new Error('Backup não encontrado');
|
||||
}
|
||||
} else {
|
||||
backupToRestore = backup;
|
||||
}
|
||||
|
||||
console.log('📤 Restaurando backup de:', new Date(backupToRestore.createdAt || backupToRestore.timestamp).toLocaleString());
|
||||
|
||||
// Confirmar com usuário se for restauração manual
|
||||
const isConfirmed = await this.confirmRestoration(backupToRestore);
|
||||
if (!isConfirmed) {
|
||||
console.log('❌ Restauração cancelada pelo usuário');
|
||||
return false;
|
||||
}
|
||||
|
||||
// Criar backup antes de restaurar (precaução)
|
||||
await this.createBackup('Backup pré-restauração');
|
||||
|
||||
// Restaurar configurações administrativas
|
||||
if (backupToRestore.data.adminConfig && window.adminConfigManager) {
|
||||
window.adminConfigManager.saveConfig(backupToRestore.data.adminConfig);
|
||||
console.log('✅ Configurações administrativas restauradas');
|
||||
}
|
||||
|
||||
// Restaurar preferências do usuário
|
||||
if (backupToRestore.data.userPreferences) {
|
||||
this.setUserPreferences(backupToRestore.data.userPreferences);
|
||||
console.log('✅ Preferências do usuário restauradas');
|
||||
}
|
||||
|
||||
// Restaurar estado da aplicação
|
||||
if (backupToRestore.data.appState) {
|
||||
this.setAppState(backupToRestore.data.appState);
|
||||
console.log('✅ Estado da aplicação restaurado');
|
||||
}
|
||||
|
||||
// Restaurar dados do State Manager
|
||||
if (backupToRestore.data.stateData && window.stateManager) {
|
||||
window.stateManager.setAllState(backupToRestore.data.stateData);
|
||||
console.log('✅ Dados do State Manager restaurados');
|
||||
}
|
||||
|
||||
// Notificar sucesso
|
||||
if (window.toastManager) {
|
||||
window.toastManager.success('Backup restaurado com sucesso!');
|
||||
}
|
||||
|
||||
console.log('✅ Backup restaurado com sucesso');
|
||||
|
||||
// Disparar evento de restauração
|
||||
this.notifyRestorationComplete(backupToRestore);
|
||||
|
||||
return true;
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ Erro ao restaurar backup:', error);
|
||||
|
||||
// Notificar erro
|
||||
if (window.toastManager) {
|
||||
window.toastManager.error('Erro ao restaurar backup: ' + error.message);
|
||||
}
|
||||
|
||||
throw new Error('Falha ao restaurar backup: ' + error.message);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove um backup específico
|
||||
* @param {string|number} timestamp - Timestamp do backup a remover
|
||||
* @returns {boolean} Sucesso da remoção
|
||||
*/
|
||||
removeBackup(idOrTimestamp) {
|
||||
try {
|
||||
const backups = this.getBackups();
|
||||
let filteredBackups;
|
||||
if (typeof idOrTimestamp === 'string' && isNaN(parseInt(idOrTimestamp))) {
|
||||
filteredBackups = backups.filter(backup => backup.id !== idOrTimestamp);
|
||||
} else {
|
||||
const timestampNum = typeof idOrTimestamp === 'string' ? parseInt(idOrTimestamp) : idOrTimestamp;
|
||||
filteredBackups = backups.filter(backup => backup.timestamp !== timestampNum);
|
||||
}
|
||||
|
||||
if (filteredBackups.length === backups.length) {
|
||||
throw new Error('Backup não encontrado');
|
||||
}
|
||||
|
||||
localStorage.setItem(this.backupKey, JSON.stringify(filteredBackups));
|
||||
|
||||
console.log('🗑️ Backup removido');
|
||||
|
||||
if (window.toastManager) {
|
||||
window.toastManager.info('Backup removido com sucesso');
|
||||
}
|
||||
|
||||
return true;
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ Erro ao remover backup:', error);
|
||||
throw new Error('Falha ao remover backup: ' + error.message);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Limpa todos os backups
|
||||
* @returns {boolean} Sucesso da limpeza
|
||||
*/
|
||||
clearAllBackups() {
|
||||
try {
|
||||
localStorage.removeItem(this.backupKey);
|
||||
console.log('🗑️ Todos os backups foram removidos');
|
||||
|
||||
if (window.toastManager) {
|
||||
window.toastManager.info('Todos os backups foram removidos');
|
||||
}
|
||||
|
||||
return true;
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ Erro ao limpar backups:', error);
|
||||
throw new Error('Falha ao limpar backups: ' + error.message);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Exporta um backup como arquivo JSON
|
||||
* @param {Object|string} backup - Backup a exportar ou timestamp
|
||||
*/
|
||||
exportBackup(backup) {
|
||||
try {
|
||||
let backupToExport;
|
||||
|
||||
// Se for timestamp, encontrar o backup correspondente
|
||||
if (typeof backup === 'string' || typeof backup === 'number') {
|
||||
const timestamp = typeof backup === 'string' ? parseInt(backup) : backup;
|
||||
const backups = this.getBackups();
|
||||
backupToExport = backups.find(b => b.timestamp === timestamp);
|
||||
|
||||
if (!backupToExport) {
|
||||
throw new Error('Backup não encontrado');
|
||||
}
|
||||
} else {
|
||||
backupToExport = backup;
|
||||
}
|
||||
|
||||
const jsonString = JSON.stringify(backupToExport, null, 2);
|
||||
const blob = new Blob([jsonString], { type: 'application/json' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = `steelbase-backup-${backupToExport.timestamp}.json`;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
|
||||
URL.revokeObjectURL(url);
|
||||
|
||||
console.log('📤 Backup exportado com sucesso');
|
||||
|
||||
if (window.toastManager) {
|
||||
window.toastManager.success('Backup exportado com sucesso!');
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ Erro ao exportar backup:', error);
|
||||
|
||||
if (window.toastManager) {
|
||||
window.toastManager.error('Erro ao exportar backup: ' + error.message);
|
||||
}
|
||||
|
||||
throw new Error('Falha ao exportar backup: ' + error.message);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Importa um backup de arquivo JSON
|
||||
* @param {File} file - Arquivo JSON a importar
|
||||
* @returns {Object} Backup importado
|
||||
*/
|
||||
async importBackup(file) {
|
||||
try {
|
||||
console.log('📥 Importando backup de arquivo...');
|
||||
|
||||
const text = await file.text();
|
||||
const backup = JSON.parse(text);
|
||||
|
||||
if (!this.isValidBackup(backup)) {
|
||||
throw new Error('Arquivo de backup inválido');
|
||||
}
|
||||
|
||||
// Adicionar aos backups existentes
|
||||
const backups = this.getBackups();
|
||||
backups.unshift(backup);
|
||||
|
||||
// Limitar número de backups
|
||||
if (backups.length > this.maxBackups) {
|
||||
backups.splice(this.maxBackups);
|
||||
}
|
||||
|
||||
localStorage.setItem(this.backupKey, JSON.stringify(backups));
|
||||
|
||||
console.log('✅ Backup importado com sucesso');
|
||||
|
||||
if (window.toastManager) {
|
||||
window.toastManager.success('Backup importado com sucesso!');
|
||||
}
|
||||
|
||||
return backup;
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ Erro ao importar backup:', error);
|
||||
|
||||
if (window.toastManager) {
|
||||
window.toastManager.error('Erro ao importar backup: ' + error.message);
|
||||
}
|
||||
|
||||
throw new Error('Falha ao importar backup: ' + error.message);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Configura backup automático
|
||||
* @param {boolean} enabled - Se deve estar ativado
|
||||
* @param {number} intervalHours - Intervalo em horas
|
||||
*/
|
||||
setupAutoBackup(enabled, intervalHours = 24) {
|
||||
try {
|
||||
const autoBackupConfig = {
|
||||
enabled: enabled,
|
||||
intervalHours: intervalHours,
|
||||
lastBackup: null
|
||||
};
|
||||
|
||||
localStorage.setItem(this.autoBackupKey, JSON.stringify(autoBackupConfig));
|
||||
|
||||
if (enabled) {
|
||||
console.log(`🔄 Backup automático configurado: a cada ${intervalHours} horas`);
|
||||
this.startAutoBackupTimer();
|
||||
} else {
|
||||
console.log('⏹️ Backup automático desativado');
|
||||
this.stopAutoBackupTimer();
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ Erro ao configurar backup automático:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Inicia o timer de backup automático
|
||||
*/
|
||||
startAutoBackupTimer() {
|
||||
// Implementar timer de backup automático
|
||||
// Isso seria chamado na inicialização da aplicação
|
||||
console.log('⏰ Timer de backup automático iniciado');
|
||||
}
|
||||
|
||||
/**
|
||||
* Para o timer de backup automático
|
||||
*/
|
||||
stopAutoBackupTimer() {
|
||||
console.log('⏹️ Timer de backup automático parado');
|
||||
}
|
||||
|
||||
/**
|
||||
* Valida se um backup é válido
|
||||
* @param {Object} backup - Backup a validar
|
||||
* @returns {boolean} Se é válido
|
||||
*/
|
||||
isValidBackup(backup) {
|
||||
if (!backup || typeof backup !== 'object') {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Verificar campos obrigatórios
|
||||
const requiredFields = ['timestamp', 'version', 'data'];
|
||||
for (const field of requiredFields) {
|
||||
if (!backup[field]) {
|
||||
console.warn(`⚠️ Campo obrigatório ausente no backup: ${field}`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Verificar estrutura de dados
|
||||
if (typeof backup.timestamp !== 'number' ||
|
||||
typeof backup.version !== 'string' ||
|
||||
typeof backup.data !== 'object') {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Limpa backups antigos (mantém apenas os mais recentes)
|
||||
*/
|
||||
cleanupOldBackups() {
|
||||
try {
|
||||
const backups = this.getBackups();
|
||||
|
||||
if (backups.length > this.maxBackups) {
|
||||
const keptBackups = backups.slice(0, this.maxBackups);
|
||||
localStorage.setItem(this.backupKey, JSON.stringify(keptBackups));
|
||||
|
||||
console.log(`🗑️ ${backups.length - keptBackups.length} backup(s) antigo(s) removido(s)`);
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.warn('⚠️ Erro ao limpar backups antigos:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtém preferências do usuário
|
||||
* @returns {Object} Preferências
|
||||
*/
|
||||
getUserPreferences() {
|
||||
try {
|
||||
const saved = localStorage.getItem('acoCalcPreferences');
|
||||
return saved ? JSON.parse(saved) : null;
|
||||
} catch (error) {
|
||||
console.warn('⚠️ Erro ao obter preferências do usuário:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Define preferências do usuário
|
||||
* @param {Object} preferences - Preferências a definir
|
||||
*/
|
||||
setUserPreferences(preferences) {
|
||||
try {
|
||||
localStorage.setItem('acoCalcPreferences', JSON.stringify(preferences));
|
||||
} catch (error) {
|
||||
console.warn('⚠️ Erro ao definir preferências do usuário:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtém estado da aplicação
|
||||
* @returns {Object} Estado da aplicação
|
||||
*/
|
||||
getAppState() {
|
||||
try {
|
||||
// Coletar estado relevante da aplicação
|
||||
const state = {
|
||||
currentSection: window.currentSection || null,
|
||||
lastUpdate: window.lastUpdate || null,
|
||||
userSession: window.userSession || null
|
||||
};
|
||||
|
||||
return state;
|
||||
} catch (error) {
|
||||
console.warn('⚠️ Erro ao obter estado da aplicação:', error);
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Define estado da aplicação
|
||||
* @param {Object} state - Estado a definir
|
||||
*/
|
||||
setAppState(state) {
|
||||
try {
|
||||
if (state.currentSection) window.currentSection = state.currentSection;
|
||||
if (state.lastUpdate) window.lastUpdate = state.lastUpdate;
|
||||
if (state.userSession) window.userSession = state.userSession;
|
||||
|
||||
} catch (error) {
|
||||
console.warn('⚠️ Erro ao definir estado da aplicação:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtém dados do cache
|
||||
* @returns {Promise<Object>} Dados do cache
|
||||
*/
|
||||
async getCacheData() {
|
||||
try {
|
||||
if (!window.cacheManager) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Obter dados relevantes do cache
|
||||
const cacheData = {
|
||||
metadata: window.cacheManager.getMetadata ? window.cacheManager.getMetadata() : null,
|
||||
stats: window.cacheManager.getStats ? window.cacheManager.getStats() : null
|
||||
};
|
||||
|
||||
return cacheData;
|
||||
|
||||
} catch (error) {
|
||||
console.warn('⚠️ Erro ao obter dados do cache:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Confirma restauração com o usuário
|
||||
* @param {Object} backup - Backup a restaurar
|
||||
* @returns {Promise<boolean>} Confirmação
|
||||
*/
|
||||
async confirmRestoration(backup) {
|
||||
// Em ambiente real, isso seria um modal/dialog
|
||||
// Por enquanto, retorna true para testes
|
||||
return new Promise((resolve) => {
|
||||
console.log(`🔄 Confirmando restauração do backup: ${new Date(backup.timestamp).toLocaleString()}`);
|
||||
|
||||
// Simular confirmação após 1 segundo
|
||||
setTimeout(() => {
|
||||
resolve(true);
|
||||
}, 1000);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Notifica conclusão da restauração
|
||||
* @param {Object} backup - Backup restaurado
|
||||
*/
|
||||
notifyRestorationComplete(backup) {
|
||||
const event = new CustomEvent('backupRestored', {
|
||||
detail: { backup: backup },
|
||||
bubbles: true
|
||||
});
|
||||
|
||||
document.dispatchEvent(event);
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtém estatísticas dos backups
|
||||
* @returns {Object} Estatísticas
|
||||
*/
|
||||
getStats() {
|
||||
const backups = this.getBackups();
|
||||
const totalSize = backups.reduce((total, backup) => {
|
||||
return total + JSON.stringify(backup).length;
|
||||
}, 0);
|
||||
|
||||
return {
|
||||
totalBackups: backups.length,
|
||||
oldestBackup: backups.length > 0 ? backups[backups.length - 1].timestamp : null,
|
||||
newestBackup: backups.length > 0 ? backups[0].timestamp : null,
|
||||
totalSize: totalSize,
|
||||
maxBackups: this.maxBackups
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Retorna um backup específico por id ou timestamp
|
||||
* @param {string|number} idOrTimestamp
|
||||
* @returns {Object|null}
|
||||
*/
|
||||
getBackup(idOrTimestamp) {
|
||||
const backups = this.getBackups();
|
||||
if (typeof idOrTimestamp === 'string' && isNaN(parseInt(idOrTimestamp))) {
|
||||
return backups.find(b => b.id === idOrTimestamp) || null;
|
||||
}
|
||||
const ts = typeof idOrTimestamp === 'string' ? Number(parseInt(idOrTimestamp)) : Number(idOrTimestamp);
|
||||
return backups.find(b => b.timestamp === ts) || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Formata bytes em string legível (KB, MB, GB)
|
||||
* @param {number} bytes
|
||||
* @returns {string}
|
||||
*/
|
||||
formatBytes(bytes) {
|
||||
if (!bytes || bytes < 1024) return `${bytes || 0} B`;
|
||||
const units = ['KB', 'MB', 'GB'];
|
||||
let value = bytes / 1024;
|
||||
let idx = 0;
|
||||
while (value >= 1024 && idx < units.length - 1) {
|
||||
value /= 1024;
|
||||
idx++;
|
||||
}
|
||||
return `${value.toFixed(1)} ${units[idx]}`;
|
||||
}
|
||||
}
|
||||
|
||||
// Criar instância global
|
||||
window.backupManager = new BackupManager();
|
||||
|
||||
console.log('🔄 BackupManager inicializado com sucesso');
|
||||
209
public/js/core/cache-manager.js
Normal file
209
public/js/core/cache-manager.js
Normal file
@@ -0,0 +1,209 @@
|
||||
/**
|
||||
* CacheManager - Gerenciador central do sistema de cache
|
||||
* Coordena IndexedDB, sincronização e acesso aos dados
|
||||
*/
|
||||
|
||||
class CacheManager {
|
||||
constructor(config = {}) {
|
||||
this.dbName = config.dbName || 'AcoCalcProDB';
|
||||
this.version = config.version || 1;
|
||||
this.db = null;
|
||||
this.config = {
|
||||
debug: config.debug || false,
|
||||
autoSync: config.autoSync || false,
|
||||
cacheExpiry: config.cacheExpiry || (7 * 24 * 60 * 60 * 1000), // 7 dias
|
||||
...config
|
||||
};
|
||||
|
||||
this.stores = [
|
||||
'cantoneiras',
|
||||
'barras',
|
||||
'barras_chatas',
|
||||
'barras_roscadas',
|
||||
'tubos_circulares',
|
||||
'tubos_rhs',
|
||||
'chapas',
|
||||
'perfis_i',
|
||||
'perfis_w',
|
||||
'perfis_hp',
|
||||
'_metadata',
|
||||
'_config'
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Inicializa o IndexedDB
|
||||
*/
|
||||
async init() {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (!window.indexedDB) {
|
||||
console.warn('⚠️ IndexedDB não disponível - usando fallback para CSV');
|
||||
resolve(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const request = indexedDB.open(this.dbName, this.version);
|
||||
|
||||
request.onerror = () => {
|
||||
console.error('❌ Erro ao abrir IndexedDB:', request.error);
|
||||
reject(request.error);
|
||||
};
|
||||
|
||||
request.onsuccess = () => {
|
||||
this.db = request.result;
|
||||
if (this.config.debug) {
|
||||
console.log('✅ IndexedDB inicializado:', this.dbName);
|
||||
}
|
||||
resolve(true);
|
||||
};
|
||||
|
||||
request.onupgradeneeded = (event) => {
|
||||
const db = event.target.result;
|
||||
|
||||
// Criar stores para cada tipo de perfil
|
||||
this.stores.forEach(storeName => {
|
||||
if (!db.objectStoreNames.contains(storeName)) {
|
||||
const store = db.createObjectStore(storeName, { keyPath: 'id' });
|
||||
|
||||
// Criar índices para busca rápida
|
||||
if (storeName !== '_metadata' && storeName !== '_config') {
|
||||
store.createIndex('nome', 'nome', { unique: false });
|
||||
store.createIndex('tipo', 'tipo', { unique: false });
|
||||
}
|
||||
|
||||
if (this.config.debug) {
|
||||
console.log(`✅ Store criada: ${storeName}`);
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifica saúde do cache
|
||||
*/
|
||||
async checkHealth() {
|
||||
if (!this.db) {
|
||||
return {
|
||||
healthy: false,
|
||||
error: 'Database not initialized'
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
const stats = {};
|
||||
|
||||
for (const storeName of this.stores) {
|
||||
if (storeName.startsWith('_')) continue;
|
||||
|
||||
const count = await this.count(storeName);
|
||||
const metadata = await this.getMetadata(storeName);
|
||||
|
||||
stats[storeName] = {
|
||||
count,
|
||||
lastSync: metadata?.lastSync || null,
|
||||
size: metadata?.size || 0
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
healthy: true,
|
||||
stats,
|
||||
totalSize: Object.values(stats).reduce((sum, s) => sum + s.size, 0)
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
healthy: false,
|
||||
error: error.message
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Limpa todo o cache
|
||||
*/
|
||||
async clearAll() {
|
||||
if (!this.db) {
|
||||
throw new Error('Database not initialized');
|
||||
}
|
||||
|
||||
const promises = this.stores
|
||||
.filter(s => !s.startsWith('_'))
|
||||
.map(storeName => this.clear(storeName));
|
||||
|
||||
await Promise.all(promises);
|
||||
|
||||
if (this.config.debug) {
|
||||
console.log('✅ Todo o cache foi limpo');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Conta registros em uma store
|
||||
*/
|
||||
async count(storeName) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const transaction = this.db.transaction([storeName], 'readonly');
|
||||
const store = transaction.objectStore(storeName);
|
||||
const request = store.count();
|
||||
|
||||
request.onsuccess = () => resolve(request.result);
|
||||
request.onerror = () => reject(request.error);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Limpa uma store específica
|
||||
*/
|
||||
async clear(storeName) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const transaction = this.db.transaction([storeName], 'readwrite');
|
||||
const store = transaction.objectStore(storeName);
|
||||
const request = store.clear();
|
||||
|
||||
request.onsuccess = () => resolve();
|
||||
request.onerror = () => reject(request.error);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Busca metadados de um tipo
|
||||
*/
|
||||
async getMetadata(tipo) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const transaction = this.db.transaction(['_metadata'], 'readonly');
|
||||
const store = transaction.objectStore('_metadata');
|
||||
const request = store.get(tipo);
|
||||
|
||||
request.onsuccess = () => resolve(request.result || null);
|
||||
request.onerror = () => reject(request.error);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Salva metadados de um tipo
|
||||
*/
|
||||
async setMetadata(tipo, metadata) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const transaction = this.db.transaction(['_metadata'], 'readwrite');
|
||||
const store = transaction.objectStore('_metadata');
|
||||
const request = store.put({ tipo, ...metadata });
|
||||
|
||||
request.onsuccess = () => resolve();
|
||||
request.onerror = () => reject(request.error);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Retorna estatísticas de uso
|
||||
*/
|
||||
getStats() {
|
||||
return this.checkHealth();
|
||||
}
|
||||
}
|
||||
|
||||
// Exportar para uso global
|
||||
window.CacheManager = CacheManager;
|
||||
|
||||
console.log('✅ CacheManager carregado');
|
||||
91
public/js/core/state.js
Normal file
91
public/js/core/state.js
Normal file
@@ -0,0 +1,91 @@
|
||||
/**
|
||||
* Application State Management
|
||||
* Central state for the entire application
|
||||
*/
|
||||
|
||||
// Main application state
|
||||
export const appState = {
|
||||
history: [],
|
||||
favorites: [],
|
||||
budgetItems: [],
|
||||
currentSection: 'cev',
|
||||
currentTheme: 'dark',
|
||||
expertMode: false,
|
||||
currentSidebarTab: 0
|
||||
};
|
||||
|
||||
// User preferences (persisted to localStorage)
|
||||
export const userPreferences = {
|
||||
theme: 'dark',
|
||||
colorScheme: 'default', // default, blue, green, purple, orange
|
||||
fontSize: 'medium', // small, medium, large, xlarge
|
||||
fontFamily: 'default' // default, modern, classic, mono
|
||||
};
|
||||
|
||||
// Admin configuration
|
||||
export const adminConfig = {
|
||||
appName: 'SteelBase',
|
||||
appSubtitle: 'Plataforma Técnica com Base de Dados de Materiais Brasileiros',
|
||||
footerText: '© 2025 SteelBase v7.5 PROFESSIONAL EDITION - Plataforma Técnica com Base de Dados de Materiais Brasileiros',
|
||||
themeDefault: 'escuro',
|
||||
modeDefault: 'simples',
|
||||
toolsVisibility: {
|
||||
'cev': true,
|
||||
'seletor': true,
|
||||
'equivalencias': false,
|
||||
'comparativo': false,
|
||||
'parafusos': true,
|
||||
'layout': true,
|
||||
'parafuso-vs-solda': false,
|
||||
'preaquecimento': true,
|
||||
'dureza': true,
|
||||
'charpy': true,
|
||||
'certificado': false,
|
||||
'ultrassom': false,
|
||||
'area-pintura': true,
|
||||
'consumo-tinta': true,
|
||||
'galvanizacao': false,
|
||||
'custo-pintura': true,
|
||||
'secagem': false,
|
||||
'inspecao-pintura': false,
|
||||
'orcamento': true,
|
||||
'peso-rigging': false,
|
||||
'referencia': false
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Update app state
|
||||
* @param {string} key - State key
|
||||
* @param {any} value - New value
|
||||
*/
|
||||
export function updateState(key, value) {
|
||||
appState[key] = value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get state value
|
||||
* @param {string} key - State key
|
||||
* @returns {any} State value
|
||||
*/
|
||||
export function getState(key) {
|
||||
return appState[key];
|
||||
}
|
||||
|
||||
/**
|
||||
* Update user preferences
|
||||
* @param {string} key - Preference key
|
||||
* @param {any} value - New value
|
||||
*/
|
||||
export function updatePreference(key, value) {
|
||||
userPreferences[key] = value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get preference value
|
||||
* @param {string} key - Preference key
|
||||
* @returns {any} Preference value
|
||||
*/
|
||||
export function getPreference(key) {
|
||||
return userPreferences[key];
|
||||
}
|
||||
86
public/js/core/storage.js
Normal file
86
public/js/core/storage.js
Normal file
@@ -0,0 +1,86 @@
|
||||
/**
|
||||
* LocalStorage Management
|
||||
* Handles persistence of user preferences
|
||||
*/
|
||||
|
||||
import { userPreferences } from './state.js';
|
||||
|
||||
const STORAGE_KEY = 'acoCalcPreferences';
|
||||
|
||||
/**
|
||||
* Load preferences from localStorage
|
||||
* @returns {boolean} Success status
|
||||
*/
|
||||
export function loadPreferences() {
|
||||
try {
|
||||
const saved = localStorage.getItem(STORAGE_KEY);
|
||||
if (saved) {
|
||||
const parsed = JSON.parse(saved);
|
||||
Object.assign(userPreferences, parsed);
|
||||
console.log('✅ Preferências carregadas:', userPreferences);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
} catch (error) {
|
||||
console.warn('⚠️ Não foi possível carregar preferências:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Save preferences to localStorage
|
||||
* @returns {boolean} Success status
|
||||
*/
|
||||
export function savePreferences() {
|
||||
try {
|
||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(userPreferences));
|
||||
console.log('✅ Preferências salvas');
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.warn('⚠️ Não foi possível salvar preferências:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all preferences
|
||||
* @returns {boolean} Success status
|
||||
*/
|
||||
export function clearPreferences() {
|
||||
try {
|
||||
localStorage.removeItem(STORAGE_KEY);
|
||||
console.log('✅ Preferências limpas');
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.warn('⚠️ Não foi possível limpar preferências:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get storage size in bytes
|
||||
* @returns {number} Size in bytes
|
||||
*/
|
||||
export function getStorageSize() {
|
||||
try {
|
||||
const data = localStorage.getItem(STORAGE_KEY);
|
||||
return data ? new Blob([data]).size : 0;
|
||||
} catch (error) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if localStorage is available
|
||||
* @returns {boolean} Availability status
|
||||
*/
|
||||
export function isStorageAvailable() {
|
||||
try {
|
||||
const test = '__storage_test__';
|
||||
localStorage.setItem(test, test);
|
||||
localStorage.removeItem(test);
|
||||
return true;
|
||||
} catch (error) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
228
public/js/core/toast-manager.js
Normal file
228
public/js/core/toast-manager.js
Normal file
@@ -0,0 +1,228 @@
|
||||
/**
|
||||
* ToastManager - Sistema de Notificações Visuais
|
||||
* Gerencia notificações toast não-intrusivas para o usuário
|
||||
*/
|
||||
class ToastManager {
|
||||
constructor() {
|
||||
this.container = null;
|
||||
this.toasts = new Map();
|
||||
this.config = {
|
||||
position: 'top-right',
|
||||
duration: 5000,
|
||||
maxToasts: 5,
|
||||
animationDuration: 300
|
||||
};
|
||||
this.init();
|
||||
}
|
||||
|
||||
init() {
|
||||
this.createContainer();
|
||||
console.log('🍞 ToastManager inicializado');
|
||||
}
|
||||
|
||||
createContainer() {
|
||||
// Remover container existente se houver
|
||||
const existingContainer = document.querySelector('.toast-container');
|
||||
if (existingContainer) {
|
||||
existingContainer.remove();
|
||||
}
|
||||
|
||||
// Criar novo container
|
||||
this.container = document.createElement('div');
|
||||
this.container.className = `toast-container toast-${this.config.position}`;
|
||||
this.container.style.cssText = `
|
||||
position: fixed;
|
||||
z-index: 10000;
|
||||
pointer-events: none;
|
||||
`;
|
||||
|
||||
// Definir posição baseada na configuração
|
||||
this.setContainerPosition();
|
||||
|
||||
document.body.appendChild(this.container);
|
||||
}
|
||||
|
||||
setContainerPosition() {
|
||||
if (!this.container) return;
|
||||
|
||||
const positions = {
|
||||
'top-right': { top: '20px', right: '20px', bottom: 'auto', left: 'auto' },
|
||||
'top-left': { top: '20px', left: '20px', bottom: 'auto', right: 'auto' },
|
||||
'top-center': { top: '20px', left: '50%', transform: 'translateX(-50%)', bottom: 'auto', right: 'auto' },
|
||||
'bottom-right': { bottom: '20px', right: '20px', top: 'auto', left: 'auto' },
|
||||
'bottom-left': { bottom: '20px', left: '20px', top: 'auto', right: 'auto' },
|
||||
'bottom-center': { bottom: '20px', left: '50%', transform: 'translateX(-50%)', top: 'auto', right: 'auto' }
|
||||
};
|
||||
|
||||
const pos = positions[this.config.position] || positions['top-right'];
|
||||
Object.assign(this.container.style, pos);
|
||||
}
|
||||
|
||||
show(message, type = 'info', duration = null, options = {}) {
|
||||
const toast = this.createToast(message, type, options);
|
||||
const toastId = toast.dataset.toastId;
|
||||
|
||||
// Adicionar ao container
|
||||
this.container.appendChild(toast);
|
||||
|
||||
// Armazenar referência
|
||||
this.toasts.set(toastId, toast);
|
||||
|
||||
// Limitar número de toasts
|
||||
this.limitToasts();
|
||||
|
||||
// Animar entrada
|
||||
requestAnimationFrame(() => {
|
||||
toast.classList.add('show');
|
||||
});
|
||||
|
||||
// Configurar tempo de remoção
|
||||
const autoHideDuration = duration !== null ? duration : this.config.duration;
|
||||
if (autoHideDuration > 0) {
|
||||
setTimeout(() => {
|
||||
this.hide(toastId);
|
||||
}, autoHideDuration);
|
||||
}
|
||||
|
||||
return toastId;
|
||||
}
|
||||
|
||||
createToast(message, type, options) {
|
||||
const toast = document.createElement('div');
|
||||
const toastId = 'toast_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9);
|
||||
|
||||
toast.dataset.toastId = toastId;
|
||||
toast.className = `toast toast-${type}`;
|
||||
toast.style.cssText = `
|
||||
background: var(--toast-bg, #333);
|
||||
color: var(--toast-color, #fff);
|
||||
padding: 16px 20px;
|
||||
margin-bottom: 10px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
font-size: 14px;
|
||||
line-height: 1.4;
|
||||
max-width: 400px;
|
||||
min-width: 300px;
|
||||
opacity: 0;
|
||||
transform: translateX(100%);
|
||||
transition: all ${this.config.animationDuration}ms ease;
|
||||
pointer-events: auto;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
`;
|
||||
|
||||
// Definir cores baseadas no tipo
|
||||
const colors = {
|
||||
success: { bg: '#22c55e', color: '#fff' },
|
||||
error: { bg: '#ef4444', color: '#fff' },
|
||||
warning: { bg: '#f59e0b', color: '#fff' },
|
||||
info: { bg: '#3b82f6', color: '#fff' }
|
||||
};
|
||||
|
||||
const colorConfig = colors[type] || colors.info;
|
||||
toast.style.setProperty('--toast-bg', colorConfig.bg);
|
||||
toast.style.setProperty('--toast-color', colorConfig.color);
|
||||
|
||||
// Adicionar ícone baseado no tipo
|
||||
const icons = {
|
||||
success: '✅',
|
||||
error: '❌',
|
||||
warning: '⚠️',
|
||||
info: 'ℹ️'
|
||||
};
|
||||
|
||||
const icon = options.icon || icons[type] || icons.info;
|
||||
|
||||
toast.innerHTML = `
|
||||
<div style="display: flex; align-items: flex-start; gap: 12px;">
|
||||
<span style="font-size: 16px; flex-shrink: 0;">${icon}</span>
|
||||
<div style="flex: 1;">${message}</div>
|
||||
<button onclick="toastManager.hide('${toastId}')" style="
|
||||
background: none;
|
||||
border: none;
|
||||
color: inherit;
|
||||
font-size: 18px;
|
||||
cursor: pointer;
|
||||
padding: 0;
|
||||
margin-left: 8px;
|
||||
opacity: 0.7;
|
||||
transition: opacity 0.2s;
|
||||
" onmouseover="this.style.opacity=1" onmouseout="this.style.opacity=0.7">×</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
return toast;
|
||||
}
|
||||
|
||||
limitToasts() {
|
||||
if (this.toasts.size <= this.config.maxToasts) return;
|
||||
|
||||
const toastsArray = Array.from(this.toasts.entries());
|
||||
const toRemove = toastsArray.slice(0, toastsArray.length - this.config.maxToasts);
|
||||
|
||||
toRemove.forEach(([toastId]) => {
|
||||
this.hide(toastId);
|
||||
});
|
||||
}
|
||||
|
||||
hide(toastId) {
|
||||
const toast = this.toasts.get(toastId);
|
||||
if (!toast) return;
|
||||
|
||||
toast.classList.remove('show');
|
||||
|
||||
setTimeout(() => {
|
||||
if (toast.parentNode) {
|
||||
toast.parentNode.removeChild(toast);
|
||||
}
|
||||
this.toasts.delete(toastId);
|
||||
}, this.config.animationDuration);
|
||||
}
|
||||
|
||||
hideAll() {
|
||||
this.toasts.forEach((toast, toastId) => {
|
||||
this.hide(toastId);
|
||||
});
|
||||
}
|
||||
|
||||
// Métodos de conveniência
|
||||
success(message, duration, options) {
|
||||
return this.show(message, 'success', duration, options);
|
||||
}
|
||||
|
||||
error(message, duration, options) {
|
||||
return this.show(message, 'error', duration, options);
|
||||
}
|
||||
|
||||
warning(message, duration, options) {
|
||||
return this.show(message, 'warning', duration, options);
|
||||
}
|
||||
|
||||
info(message, duration, options) {
|
||||
return this.show(message, 'info', duration, options);
|
||||
}
|
||||
|
||||
// Configurar posição
|
||||
setPosition(position) {
|
||||
if (['top-right', 'top-left', 'top-center', 'bottom-right', 'bottom-left', 'bottom-center'].includes(position)) {
|
||||
this.config.position = position;
|
||||
this.createContainer();
|
||||
}
|
||||
}
|
||||
|
||||
// Obter estatísticas
|
||||
getStats() {
|
||||
return {
|
||||
activeToasts: this.toasts.size,
|
||||
maxToasts: this.config.maxToasts,
|
||||
position: this.config.position,
|
||||
duration: this.config.duration
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Tornar disponível globalmente
|
||||
window.ToastManager = ToastManager;
|
||||
window.toastManager = new ToastManager();
|
||||
Reference in New Issue
Block a user