Fix script paths and move assets to public/ folder for Vite build compatibility

This commit is contained in:
Marcos
2026-03-22 20:45:20 -03:00
parent 304504b758
commit 57ba9d1c5f
155 changed files with 10614 additions and 26 deletions

View 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');

View 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');

View 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
View 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
View 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;
}
}

View 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();