Initial commit SteelBase - Oficiais e Funcionando
This commit is contained in:
643
js/core/admin-config-manager.js
Normal file
643
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
js/core/backup-manager.js
Normal file
669
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
js/core/cache-manager.js
Normal file
209
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
js/core/state.js
Normal file
91
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
js/core/storage.js
Normal file
86
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
js/core/toast-manager.js
Normal file
228
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();
|
||||
1279
js/database/admin-panel.js
Normal file
1279
js/database/admin-panel.js
Normal file
File diff suppressed because it is too large
Load Diff
4443
js/database/banco-dados-completo.js
Normal file
4443
js/database/banco-dados-completo.js
Normal file
File diff suppressed because it is too large
Load Diff
324
js/database/carregador-universal.js
Normal file
324
js/database/carregador-universal.js
Normal file
@@ -0,0 +1,324 @@
|
||||
/**
|
||||
* CARREGADOR UNIVERSAL DE PERFIS
|
||||
* Sistema que carrega automaticamente TODOS os tipos de perfis
|
||||
* usando o banco de dados embutido
|
||||
*/
|
||||
|
||||
// Configuração de mapeamento de tipos
|
||||
const MAPEAMENTO_PERFIS = {
|
||||
'cantoneiras': {
|
||||
tbodyId: 'cantoneiras-tbody',
|
||||
totalId: 'cant-total',
|
||||
colunas: [
|
||||
{ key: 'nome', label: 'Designação', bold: true },
|
||||
{ key: 'lado_mm', label: 'Lado (mm)' },
|
||||
{ key: 'espessura_mm', label: 'Espessura (mm)' },
|
||||
{ key: 'peso_kg_m', label: 'Peso (kg/m)', decimals: 2 },
|
||||
{ key: 'area_cm2', label: 'Área (cm²)', decimals: 2 },
|
||||
{ key: 'momento_inercia_cm4', label: 'Momento Inércia (cm⁴)', decimals: 2 },
|
||||
{ key: 'raio_giracao_cm', label: 'Raio Giração (cm)', decimals: 2 },
|
||||
{ key: 'tipo', label: 'Categoria', badge: true }
|
||||
]
|
||||
},
|
||||
'barras_redondas': {
|
||||
tbodyId: 'barras_redondas-tbody',
|
||||
totalId: 'barras_redondas-total',
|
||||
colunas: [
|
||||
{ key: 'nome', label: 'Designação', bold: true },
|
||||
{ key: 'diametro_mm', label: 'Diâmetro (mm)' },
|
||||
{ key: 'peso_kg_m', label: 'Peso (kg/m)', decimals: 3 },
|
||||
{ key: 'area_cm2', label: 'Área (cm²)', decimals: 3 },
|
||||
{ key: 'momento_inercia_cm4', label: 'Momento Inércia (cm⁴)', decimals: 2 },
|
||||
{ key: 'raio_giracao_cm', label: 'Raio Giração (cm)', decimals: 2 },
|
||||
{ key: 'aplicacao', label: 'Aplicação' },
|
||||
{ key: 'tipo', label: 'Categoria', badge: true }
|
||||
]
|
||||
},
|
||||
'tubos_circulares': {
|
||||
tbodyId: 'tubos_circulares-tbody',
|
||||
totalId: 'tubos_circulares-total',
|
||||
colunas: [
|
||||
{ key: 'nome', label: 'Designação', bold: true },
|
||||
{ key: 'diametro_mm', label: 'Ø Externo (mm)' },
|
||||
{ key: 'espessura_mm', label: 'Espessura (mm)' },
|
||||
{ key: 'peso_kg_m', label: 'Peso (kg/m)', decimals: 2 },
|
||||
{ key: 'area_cm2', label: 'Área (cm²)', decimals: 2 },
|
||||
{ key: 'momento_inercia_cm4', label: 'Momento Inércia (cm⁴)', decimals: 2 },
|
||||
{ key: 'raio_giracao_cm', label: 'Raio Giração (cm)', decimals: 2 },
|
||||
{ key: 'aplicacao', label: 'Aplicação' },
|
||||
{ key: 'tipo', label: 'Categoria', badge: true }
|
||||
]
|
||||
},
|
||||
'perfis_i': {
|
||||
tbodyId: 'perfis_i-tbody',
|
||||
totalId: 'perfis_i-total',
|
||||
colunas: [
|
||||
{ key: 'nome', label: 'Designação', bold: true },
|
||||
{ key: 'altura_mm', label: 'Altura (mm)' },
|
||||
{ key: 'largura_mm', label: 'Largura (mm)' },
|
||||
{ key: 'espessura_alma_mm', label: 'Esp. Alma (mm)' },
|
||||
{ key: 'espessura_mesa_mm', label: 'Esp. Mesa (mm)' },
|
||||
{ key: 'peso_kg_m', label: 'Peso (kg/m)', decimals: 2 },
|
||||
{ key: 'area_cm2', label: 'Área (cm²)', decimals: 2 },
|
||||
{ key: 'tipo', label: 'Categoria', badge: true }
|
||||
]
|
||||
},
|
||||
'perfis_w': {
|
||||
tbodyId: 'perfis_w-tbody',
|
||||
totalId: 'perfis_w-total',
|
||||
colunas: [
|
||||
{ key: 'nome', label: 'Designação', bold: true },
|
||||
{ key: 'altura_mm', label: 'Altura (mm)' },
|
||||
{ key: 'largura_mm', label: 'Largura (mm)' },
|
||||
{ key: 'espessura_alma_mm', label: 'Esp. Alma (mm)' },
|
||||
{ key: 'espessura_mesa_mm', label: 'Esp. Mesa (mm)' },
|
||||
{ key: 'peso_kg_m', label: 'Peso (kg/m)', decimals: 2 },
|
||||
{ key: 'area_cm2', label: 'Área (cm²)', decimals: 2 },
|
||||
{ key: 'tipo', label: 'Categoria', badge: true }
|
||||
]
|
||||
},
|
||||
'tubos_rhs': {
|
||||
tbodyId: 'tubos_rhs-tbody',
|
||||
totalId: 'tubos_rhs-total',
|
||||
colunas: [
|
||||
{ key: 'nome', label: 'Designação', bold: true },
|
||||
{ key: 'largura_mm', label: 'Largura (mm)' },
|
||||
{ key: 'altura_mm', label: 'Altura (mm)' },
|
||||
{ key: 'espessura_mm', label: 'Espessura (mm)' },
|
||||
{ key: 'peso_kg_m', label: 'Peso (kg/m)', decimals: 2 },
|
||||
{ key: 'area_cm2', label: 'Área (cm²)', decimals: 2 },
|
||||
{ key: 'tipo', label: 'Categoria', badge: true }
|
||||
]
|
||||
},
|
||||
'chapas': {
|
||||
tbodyId: 'chapas-tbody',
|
||||
totalId: 'chapas-total',
|
||||
colunas: [
|
||||
{ key: 'nome', label: 'Designação', bold: true },
|
||||
{ key: 'espessura_mm', label: 'Espessura (mm)' },
|
||||
{ key: 'peso_kg_m2', label: 'Peso (kg/m²)', decimals: 2 },
|
||||
{ key: 'tipo', label: 'Categoria', badge: true }
|
||||
]
|
||||
},
|
||||
'perfis_hp': {
|
||||
tbodyId: 'perfis_hp-tbody',
|
||||
totalId: 'perfis_hp-total',
|
||||
colunas: [
|
||||
{ key: 'nome', label: 'Designação', bold: true },
|
||||
{ key: 'altura_mm', label: 'Altura (mm)' },
|
||||
{ key: 'aba_mm', label: 'Aba (mm)' },
|
||||
{ key: 'espessura_alma_mm', label: 'Esp. Alma (mm)', decimals: 1 },
|
||||
{ key: 'espessura_aba_mm', label: 'Esp. Aba (mm)', decimals: 1 },
|
||||
{ key: 'peso_kg_m', label: 'Peso (kg/m)', decimals: 1 },
|
||||
{ key: 'area_cm2', label: 'Área (cm²)', decimals: 1 },
|
||||
{ key: 'tipo', label: 'Categoria', badge: true }
|
||||
]
|
||||
},
|
||||
'barras_roscadas': {
|
||||
tbodyId: 'barras_roscadas-tbody',
|
||||
totalId: 'barras_roscadas-total',
|
||||
colunas: [
|
||||
{ key: 'tipo_rosca', label: 'Tipo', badge: true },
|
||||
{ key: 'diametro_nominal_mm', label: 'Diâmetro (mm)', decimals: 2 },
|
||||
{ key: 'passo_mm', label: 'Passo (mm)', decimals: 2 },
|
||||
{ key: 'peso_kg_m', label: 'Peso (kg/m)', decimals: 3 },
|
||||
{ key: 'aplicacao', label: 'Aplicação' },
|
||||
{ key: 'tamanho', label: 'Categoria', badge: true }
|
||||
]
|
||||
},
|
||||
'barras_chatas': {
|
||||
tbodyId: 'barras_chatas-tbody',
|
||||
totalId: 'barras_chatas-total',
|
||||
colunas: [
|
||||
{ key: 'nome', label: 'Designação', bold: true },
|
||||
{ key: 'largura_mm', label: 'Largura (mm)', decimals: 1 },
|
||||
{ key: 'espessura_mm', label: 'Espessura (mm)', decimals: 1 },
|
||||
{ key: 'peso_kg_m', label: 'Peso (kg/m)', decimals: 3 },
|
||||
{ key: 'area_cm2', label: 'Área (cm²)', decimals: 2 },
|
||||
{ key: 'tamanho', label: 'Categoria', badge: true }
|
||||
]
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Carrega dados de um tipo específico de perfil
|
||||
*/
|
||||
function carregarPerfilUniversal(tipo) {
|
||||
console.log(`🚀 Carregando perfil: ${tipo}`);
|
||||
|
||||
const config = MAPEAMENTO_PERFIS[tipo];
|
||||
if (!config) {
|
||||
console.error(`❌ Configuração não encontrada para: ${tipo}`);
|
||||
return false;
|
||||
}
|
||||
|
||||
const tbody = document.getElementById(config.tbodyId);
|
||||
if (!tbody) {
|
||||
console.warn(`⚠️ Elemento ${config.tbodyId} não encontrado`);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Obter dados do banco
|
||||
const dados = window.BANCO_DADOS_PERFIS?.[tipo];
|
||||
if (!dados || dados.length === 0) {
|
||||
console.error(`❌ Dados não encontrados para: ${tipo}`);
|
||||
tbody.innerHTML = `
|
||||
<tr>
|
||||
<td colspan="${config.colunas.length + 1}" style="text-align: center; padding: 40px;">
|
||||
<div style="font-size: 48px; margin-bottom: 16px;">❌</div>
|
||||
<div style="font-size: 18px; font-weight: bold; color: #ef4444;">Dados não encontrados</div>
|
||||
<div style="font-size: 14px; color: #666; margin-top: 8px;">Tipo: ${tipo}</div>
|
||||
</td>
|
||||
</tr>
|
||||
`;
|
||||
return false;
|
||||
}
|
||||
|
||||
// Gerar HTML da tabela
|
||||
const html = dados.map(item => {
|
||||
const colunas = config.colunas.map(col => {
|
||||
let valor = item[col.key];
|
||||
|
||||
// Formatação
|
||||
if (col.decimals && typeof valor === 'number') {
|
||||
valor = valor.toFixed(col.decimals);
|
||||
}
|
||||
|
||||
// Estilo
|
||||
if (col.bold) {
|
||||
return `<td><strong>${valor}</strong></td>`;
|
||||
} else if (col.badge) {
|
||||
return `<td><span class="badge badge-info">${valor}</span></td>`;
|
||||
} else {
|
||||
return `<td>${valor}</td>`;
|
||||
}
|
||||
}).join('');
|
||||
|
||||
return `<tr>${colunas}<td><button class="btn btn-sm btn-primary">👁️ Ver</button></td></tr>`;
|
||||
}).join('');
|
||||
|
||||
tbody.innerHTML = html;
|
||||
|
||||
// Atualizar contador
|
||||
const totalEl = document.getElementById(config.totalId);
|
||||
if (totalEl) {
|
||||
totalEl.textContent = dados.length;
|
||||
}
|
||||
|
||||
// Armazenar globalmente
|
||||
window[`${tipo}Data`] = dados;
|
||||
|
||||
console.log(`✅ ${tipo}: ${dados.length} itens carregados`);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Observer universal que detecta qualquer tabela de perfil
|
||||
*/
|
||||
function iniciarObserverUniversal() {
|
||||
console.log('👁️ Iniciando observer universal para todos os perfis');
|
||||
|
||||
const observer = new MutationObserver((mutations) => {
|
||||
// Verificar cada tipo de perfil
|
||||
Object.keys(MAPEAMENTO_PERFIS).forEach(tipo => {
|
||||
const config = MAPEAMENTO_PERFIS[tipo];
|
||||
const tbody = document.getElementById(config.tbodyId);
|
||||
|
||||
if (tbody) {
|
||||
// Verificar se está vazio
|
||||
const conteudo = tbody.textContent.trim();
|
||||
const estaVazio = tbody.children.length === 0 ||
|
||||
conteudo.includes('Será preenchido') ||
|
||||
conteudo.includes('preenchido via JavaScript') ||
|
||||
(tbody.children.length === 1 && tbody.children[0].children.length === 1);
|
||||
|
||||
if (estaVazio) {
|
||||
console.log(`🎯 Tabela vazia detectada: ${tipo}`);
|
||||
|
||||
// Aguardar um pouco e carregar
|
||||
setTimeout(() => {
|
||||
const sucesso = carregarPerfilUniversal(tipo);
|
||||
if (sucesso) {
|
||||
console.log(`✅ Auto-carregamento concluído: ${tipo}`);
|
||||
}
|
||||
}, 300);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Observar mudanças no body
|
||||
observer.observe(document.body, {
|
||||
childList: true,
|
||||
subtree: true
|
||||
});
|
||||
|
||||
// Tentar carregar imediatamente também
|
||||
setTimeout(() => {
|
||||
Object.keys(MAPEAMENTO_PERFIS).forEach(tipo => {
|
||||
const config = MAPEAMENTO_PERFIS[tipo];
|
||||
const tbody = document.getElementById(config.tbodyId);
|
||||
if (tbody) {
|
||||
carregarPerfilUniversal(tipo);
|
||||
}
|
||||
});
|
||||
}, 1000);
|
||||
|
||||
console.log('✅ Observer universal ativo para todos os perfis');
|
||||
}
|
||||
|
||||
// Gerar funções de carregamento forçado para cada tipo
|
||||
Object.keys(MAPEAMENTO_PERFIS).forEach(tipo => {
|
||||
const funcName = `forcarCarregamento${tipo.split('_').map(word =>
|
||||
word.charAt(0).toUpperCase() + word.slice(1)
|
||||
).join('')}`;
|
||||
|
||||
window[funcName] = function() {
|
||||
console.log(`🚨 Carregamento forçado: ${tipo}`);
|
||||
|
||||
const config = MAPEAMENTO_PERFIS[tipo];
|
||||
const tbody = document.getElementById(config.tbodyId);
|
||||
|
||||
if (!tbody) {
|
||||
alert(`❌ Erro: Tabela não encontrada para ${tipo}`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Mostrar loading
|
||||
tbody.innerHTML = `
|
||||
<tr>
|
||||
<td colspan="${config.colunas.length + 1}" style="text-align: center; padding: 40px;">
|
||||
<div style="font-size: 48px; margin-bottom: 16px;">⏳</div>
|
||||
<div style="font-size: 18px; font-weight: bold;">Carregando ${tipo}...</div>
|
||||
</td>
|
||||
</tr>
|
||||
`;
|
||||
|
||||
// Carregar após delay
|
||||
setTimeout(() => {
|
||||
const sucesso = carregarPerfilUniversal(tipo);
|
||||
if (sucesso) {
|
||||
const dados = window.BANCO_DADOS_PERFIS[tipo];
|
||||
alert(`✅ ${dados.length} ${tipo} carregados com sucesso!`);
|
||||
} else {
|
||||
alert(`❌ Erro ao carregar ${tipo}`);
|
||||
}
|
||||
}, 500);
|
||||
};
|
||||
|
||||
console.log(`✅ Função criada: ${funcName}`);
|
||||
});
|
||||
|
||||
// Inicializar quando DOM estiver pronto
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', iniciarObserverUniversal);
|
||||
} else {
|
||||
iniciarObserverUniversal();
|
||||
}
|
||||
|
||||
// Exportar funções
|
||||
window.carregarPerfilUniversal = carregarPerfilUniversal;
|
||||
window.MAPEAMENTO_PERFIS = MAPEAMENTO_PERFIS;
|
||||
|
||||
console.log('✅ Carregador universal configurado para todos os perfis');
|
||||
console.log('📊 Perfis suportados:', Object.keys(MAPEAMENTO_PERFIS));
|
||||
177
js/database/dados-embutidos.js
Normal file
177
js/database/dados-embutidos.js
Normal file
@@ -0,0 +1,177 @@
|
||||
/**
|
||||
* DADOS EMBUTIDOS - Todos os perfis em JavaScript
|
||||
* Solução autônoma que funciona sem servidor e sem CSVs
|
||||
* Pronto para desktop, mobile e deploy
|
||||
*/
|
||||
|
||||
const DADOS_PERFIS = {
|
||||
cantoneiras: [
|
||||
{id: 'l25_25_3', nome: 'L25x25x3', lado_mm: 25, espessura_mm: 3, peso_kg_m: 1.12, area_cm2: 1.43, momento_inercia_cm4: 0.38, raio_giracao_cm: 0.52, tipo: 'Pequena'},
|
||||
{id: 'l25_25_4', nome: 'L25x25x4', lado_mm: 25, espessura_mm: 4, peso_kg_m: 1.47, area_cm2: 1.87, momento_inercia_cm4: 0.47, raio_giracao_cm: 0.5, tipo: 'Pequena'},
|
||||
{id: 'l32_32_3', nome: 'L32x32x3', lado_mm: 32, espessura_mm: 3, peso_kg_m: 1.45, area_cm2: 1.85, momento_inercia_cm4: 0.69, raio_giracao_cm: 0.61, tipo: 'Pequena'},
|
||||
{id: 'l32_32_4', nome: 'L32x32x4', lado_mm: 32, espessura_mm: 4, peso_kg_m: 1.91, area_cm2: 2.43, momento_inercia_cm4: 0.86, raio_giracao_cm: 0.59, tipo: 'Pequena'},
|
||||
{id: 'l40_40_3', nome: 'L40x40x3', lado_mm: 40, espessura_mm: 3, peso_kg_m: 1.86, area_cm2: 2.37, momento_inercia_cm4: 1.4, raio_giracao_cm: 0.77, tipo: 'Pequena'},
|
||||
{id: 'l40_40_4', nome: 'L40x40x4', lado_mm: 40, espessura_mm: 4, peso_kg_m: 2.46, area_cm2: 3.13, momento_inercia_cm4: 1.74, raio_giracao_cm: 0.75, tipo: 'Pequena'},
|
||||
{id: 'l40_40_5', nome: 'L40x40x5', lado_mm: 40, espessura_mm: 5, peso_kg_m: 3.03, area_cm2: 3.86, momento_inercia_cm4: 2.06, raio_giracao_cm: 0.73, tipo: 'Pequena'},
|
||||
{id: 'l50_50_3', nome: 'L50x50x3', lado_mm: 50, espessura_mm: 3, peso_kg_m: 2.36, area_cm2: 3.0, momento_inercia_cm4: 2.7, raio_giracao_cm: 0.95, tipo: 'Pequena'},
|
||||
{id: 'l50_50_4', nome: 'L50x50x4', lado_mm: 50, espessura_mm: 4, peso_kg_m: 3.14, area_cm2: 4.0, momento_inercia_cm4: 3.5, raio_giracao_cm: 0.93, tipo: 'Pequena'},
|
||||
{id: 'l50_50_5', nome: 'L50x50x5', lado_mm: 50, espessura_mm: 5, peso_kg_m: 3.89, area_cm2: 4.96, momento_inercia_cm4: 4.25, raio_giracao_cm: 0.93, tipo: 'Pequena'},
|
||||
{id: 'l50_50_6', nome: 'L50x50x6', lado_mm: 50, espessura_mm: 6, peso_kg_m: 4.62, area_cm2: 5.89, momento_inercia_cm4: 4.95, raio_giracao_cm: 0.92, tipo: 'Pequena'},
|
||||
{id: 'l63_63_5', nome: 'L63x63x5', lado_mm: 63, espessura_mm: 5, peso_kg_m: 4.96, area_cm2: 6.31, momento_inercia_cm4: 8.47, raio_giracao_cm: 1.16, tipo: 'Média'},
|
||||
{id: 'l63_63_6', nome: 'L63x63x6', lado_mm: 63, espessura_mm: 6, peso_kg_m: 5.91, area_cm2: 7.53, momento_inercia_cm4: 9.9, raio_giracao_cm: 1.15, tipo: 'Média'},
|
||||
{id: 'l63_63_8', nome: 'L63x63x8', lado_mm: 63, espessura_mm: 8, peso_kg_m: 7.73, area_cm2: 9.85, momento_inercia_cm4: 12.6, raio_giracao_cm: 1.13, tipo: 'Média'},
|
||||
{id: 'l75_75_5', nome: 'L75x75x5', lado_mm: 75, espessura_mm: 5, peso_kg_m: 5.91, area_cm2: 7.53, momento_inercia_cm4: 13.5, raio_giracao_cm: 1.34, tipo: 'Média'},
|
||||
{id: 'l75_75_6', nome: 'L75x75x6', lado_mm: 75, espessura_mm: 6, peso_kg_m: 7.05, area_cm2: 8.98, momento_inercia_cm4: 15.8, raio_giracao_cm: 1.33, tipo: 'Média'},
|
||||
{id: 'l75_75_7', nome: 'L75x75x7', lado_mm: 75, espessura_mm: 7, peso_kg_m: 8.17, area_cm2: 10.4, momento_inercia_cm4: 18.0, raio_giracao_cm: 1.32, tipo: 'Média'},
|
||||
{id: 'l75_75_8', nome: 'L75x75x8', lado_mm: 75, espessura_mm: 8, peso_kg_m: 9.27, area_cm2: 11.8, momento_inercia_cm4: 20.1, raio_giracao_cm: 1.31, tipo: 'Média'},
|
||||
{id: 'l100_100_6', nome: 'L100x100x6', lado_mm: 100, espessura_mm: 6, peso_kg_m: 9.46, area_cm2: 12.1, momento_inercia_cm4: 37.4, raio_giracao_cm: 1.76, tipo: 'Grande'},
|
||||
{id: 'l100_100_8', nome: 'L100x100x8', lado_mm: 100, espessura_mm: 8, peso_kg_m: 12.5, area_cm2: 15.9, momento_inercia_cm4: 48.8, raio_giracao_cm: 1.75, tipo: 'Grande'},
|
||||
{id: 'l100_100_10', nome: 'L100x100x10', lado_mm: 100, espessura_mm: 10, peso_kg_m: 15.4, area_cm2: 19.6, momento_inercia_cm4: 59.5, raio_giracao_cm: 1.74, tipo: 'Grande'},
|
||||
{id: 'l100_100_12', nome: 'L100x100x12', lado_mm: 100, espessura_mm: 12, peso_kg_m: 18.2, area_cm2: 23.2, momento_inercia_cm4: 69.5, raio_giracao_cm: 1.73, tipo: 'Grande'},
|
||||
{id: 'l125_125_8', nome: 'L125x125x8', lado_mm: 125, espessura_mm: 8, peso_kg_m: 15.7, area_cm2: 20.0, momento_inercia_cm4: 95.3, raio_giracao_cm: 2.18, tipo: 'Muito Grande'},
|
||||
{id: 'l125_125_10', nome: 'L125x125x10', lado_mm: 125, espessura_mm: 10, peso_kg_m: 19.5, area_cm2: 24.8, momento_inercia_cm4: 117, raio_giracao_cm: 2.17, tipo: 'Muito Grande'},
|
||||
{id: 'l125_125_12', nome: 'L125x125x12', lado_mm: 125, espessura_mm: 12, peso_kg_m: 23.2, area_cm2: 29.5, momento_inercia_cm4: 137, raio_giracao_cm: 2.16, tipo: 'Muito Grande'},
|
||||
{id: 'l125_125_16', nome: 'L125x125x16', lado_mm: 125, espessura_mm: 16, peso_kg_m: 30.3, area_cm2: 38.6, momento_inercia_cm4: 175, raio_giracao_cm: 2.13, tipo: 'Muito Grande'},
|
||||
{id: 'l150_150_10', nome: 'L150x150x10', lado_mm: 150, espessura_mm: 10, peso_kg_m: 23.6, area_cm2: 30.1, momento_inercia_cm4: 201, raio_giracao_cm: 2.59, tipo: 'Extra-Grande'},
|
||||
{id: 'l150_150_12', nome: 'L150x150x12', lado_mm: 150, espessura_mm: 12, peso_kg_m: 28.1, area_cm2: 35.8, momento_inercia_cm4: 237, raio_giracao_cm: 2.57, tipo: 'Extra-Grande'},
|
||||
{id: 'l150_150_15', nome: 'L150x150x15', lado_mm: 150, espessura_mm: 15, peso_kg_m: 34.8, area_cm2: 44.3, momento_inercia_cm4: 289, raio_giracao_cm: 2.56, tipo: 'Extra-Grande'},
|
||||
{id: 'l150_150_20', nome: 'L150x150x20', lado_mm: 150, espessura_mm: 20, peso_kg_m: 45.6, area_cm2: 58.1, momento_inercia_cm4: 371, raio_giracao_cm: 2.53, tipo: 'Extra-Grande'},
|
||||
{id: 'l200_200_16', nome: 'L200x200x16', lado_mm: 200, espessura_mm: 16, peso_kg_m: 50.3, area_cm2: 64.1, momento_inercia_cm4: 712, raio_giracao_cm: 3.33, tipo: 'Massiva'},
|
||||
{id: 'l200_200_20', nome: 'L200x200x20', lado_mm: 200, espessura_mm: 20, peso_kg_m: 62.4, area_cm2: 79.5, momento_inercia_cm4: 873, raio_giracao_cm: 3.31, tipo: 'Massiva'},
|
||||
{id: 'l200_200_25', nome: 'L200x200x25', lado_mm: 200, espessura_mm: 25, peso_kg_m: 77.3, area_cm2: 98.5, momento_inercia_cm4: 1060, raio_giracao_cm: 3.28, tipo: 'Massiva'}
|
||||
]
|
||||
};
|
||||
|
||||
/**
|
||||
* Função para obter dados de um tipo de perfil
|
||||
*/
|
||||
function obterDadosPerfil(tipo) {
|
||||
console.log(`📊 Obtendo dados embutidos: ${tipo}`);
|
||||
const dados = DADOS_PERFIS[tipo];
|
||||
|
||||
if (!dados) {
|
||||
console.error(`❌ Tipo de perfil não encontrado: ${tipo}`);
|
||||
return [];
|
||||
}
|
||||
|
||||
console.log(`✅ ${dados.length} itens encontrados para ${tipo}`);
|
||||
return dados;
|
||||
}
|
||||
|
||||
/**
|
||||
* Carrega dados automaticamente para cantoneiras
|
||||
*/
|
||||
function carregarCantoneirasAutomatico() {
|
||||
console.log('🚀 Carregamento automático de cantoneiras (dados embutidos)');
|
||||
|
||||
const tbody = document.getElementById('cantoneiras-tbody');
|
||||
if (!tbody) {
|
||||
console.warn('⚠️ Elemento tbody não encontrado ainda');
|
||||
return false;
|
||||
}
|
||||
|
||||
const dados = obterDadosPerfil('cantoneiras');
|
||||
|
||||
if (dados.length === 0) {
|
||||
console.error('❌ Nenhum dado encontrado');
|
||||
return false;
|
||||
}
|
||||
|
||||
// Exibir na tabela
|
||||
tbody.innerHTML = dados.map(item => `
|
||||
<tr>
|
||||
<td><strong>${item.nome}</strong></td>
|
||||
<td>${item.lado_mm}</td>
|
||||
<td>${item.espessura_mm}</td>
|
||||
<td>${item.peso_kg_m.toFixed(2)}</td>
|
||||
<td>${item.area_cm2.toFixed(2)}</td>
|
||||
<td>${item.momento_inercia_cm4.toFixed(2)}</td>
|
||||
<td>${item.raio_giracao_cm.toFixed(2)}</td>
|
||||
<td><span class="badge badge-info">${item.tipo}</span></td>
|
||||
<td><button class="btn btn-sm btn-primary">👁️ Ver</button></td>
|
||||
</tr>
|
||||
`).join('');
|
||||
|
||||
// Atualizar contador
|
||||
const totalEl = document.getElementById('cant-total');
|
||||
if (totalEl) {
|
||||
totalEl.textContent = dados.length;
|
||||
}
|
||||
|
||||
// Armazenar globalmente
|
||||
window.cantoneirasData = dados;
|
||||
|
||||
console.log(`✅ ${dados.length} cantoneiras carregadas automaticamente!`);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Exportar para uso global
|
||||
window.DADOS_PERFIS = DADOS_PERFIS;
|
||||
window.obterDadosPerfil = obterDadosPerfil;
|
||||
window.carregarCantoneirasAutomatico = carregarCantoneirasAutomatico;
|
||||
|
||||
console.log('✅ Dados embutidos carregados - 33 cantoneiras disponíveis');
|
||||
|
||||
|
||||
// ========================================
|
||||
// AUTO-CARREGAMENTO ROBUSTO
|
||||
// ========================================
|
||||
|
||||
/**
|
||||
* Observer que detecta quando a tabela de cantoneiras aparece
|
||||
* e carrega os dados automaticamente
|
||||
*/
|
||||
function iniciarAutoCarregamentoRobusto() {
|
||||
console.log('👁️ Iniciando observer robusto para auto-carregamento');
|
||||
|
||||
const observer = new MutationObserver((mutations) => {
|
||||
const tbody = document.getElementById('cantoneiras-tbody');
|
||||
|
||||
if (tbody) {
|
||||
// Verificar se está vazio ou com placeholder
|
||||
const conteudo = tbody.textContent.trim();
|
||||
const estaVazio = tbody.children.length === 0 ||
|
||||
conteudo.includes('Será preenchido') ||
|
||||
conteudo.includes('preenchido via JavaScript') ||
|
||||
tbody.children.length === 1 && tbody.children[0].children.length === 1;
|
||||
|
||||
if (estaVazio) {
|
||||
console.log('🎯 Tabela vazia detectada - carregando automaticamente...');
|
||||
|
||||
// Aguardar um pouco e carregar
|
||||
setTimeout(() => {
|
||||
const sucesso = carregarCantoneirasAutomatico();
|
||||
if (sucesso) {
|
||||
console.log('✅ Auto-carregamento concluído com sucesso!');
|
||||
observer.disconnect(); // Parar de observar
|
||||
}
|
||||
}, 300);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Observar mudanças no body
|
||||
observer.observe(document.body, {
|
||||
childList: true,
|
||||
subtree: true
|
||||
});
|
||||
|
||||
// Tentar carregar imediatamente também
|
||||
setTimeout(() => {
|
||||
const tbody = document.getElementById('cantoneiras-tbody');
|
||||
if (tbody) {
|
||||
carregarCantoneirasAutomatico();
|
||||
}
|
||||
}, 1000);
|
||||
|
||||
console.log('✅ Observer robusto ativo');
|
||||
}
|
||||
|
||||
// Iniciar quando o DOM estiver pronto
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', iniciarAutoCarregamentoRobusto);
|
||||
} else {
|
||||
iniciarAutoCarregamentoRobusto();
|
||||
}
|
||||
|
||||
console.log('✅ Sistema de auto-carregamento robusto configurado');
|
||||
741
js/database/data-manager.js
Normal file
741
js/database/data-manager.js
Normal file
@@ -0,0 +1,741 @@
|
||||
/**
|
||||
* SISTEMA DE GERENCIAMENTO DE DADOS - AÇO CALC PRO
|
||||
*
|
||||
* Sistema intermediário que gerencia dados dos CSVs,
|
||||
* criando um cache inteligente e permitindo atualizações
|
||||
* dinâmicas sem afetar a performance do aplicativo.
|
||||
*
|
||||
* @author AÇO CALC PRO v7.5
|
||||
* @date 2025
|
||||
*/
|
||||
|
||||
class DataManager {
|
||||
constructor() {
|
||||
this.version = '1.0.0';
|
||||
this.cachePrefix = 'acoCalcPro_cache_';
|
||||
this.metadataKey = 'acoCalcPro_metadata';
|
||||
this.typesMetadataKey = 'acoCalcPro_types_metadata';
|
||||
this.csvConfigs = {
|
||||
cantoneiras: {
|
||||
file: 'BD/perfis/cantoneiras_brasil_completo.csv',
|
||||
columns: ['id', 'nome', 'lado_mm', 'espessura_mm', 'peso_kg_m', 'area_cm2', 'momento_inercia_cm4', 'raio_giracao_cm', 'tipo'],
|
||||
keyField: 'id',
|
||||
displayName: 'Cantoneiras'
|
||||
},
|
||||
barras_redondas: {
|
||||
file: 'BD/perfis/barras_brasil_completo.csv',
|
||||
columns: ['id', 'nome', 'diametro_mm', 'peso_kg_m', 'area_cm2', 'tipo'],
|
||||
keyField: 'id',
|
||||
displayName: 'Barras Redondas'
|
||||
},
|
||||
tubos_circulares: {
|
||||
file: 'BD/perfis/tubos_circulares_brasil_completo.csv',
|
||||
columns: ['id', 'nome', 'diametro_externo_mm', 'espessura_mm', 'peso_kg_m', 'area_cm2', 'tipo'],
|
||||
keyField: 'id',
|
||||
displayName: 'Tubos Circulares'
|
||||
},
|
||||
perfis_i: {
|
||||
file: 'BD/perfis/perfis_i_brasil_completo.csv',
|
||||
columns: ['id', 'nome', 'altura_mm', 'largura_mm', 'espessura_alma_mm', 'espessura_mesa_mm', 'peso_kg_m', 'area_cm2', 'tipo'],
|
||||
keyField: 'id',
|
||||
displayName: 'Perfis I (IPE)'
|
||||
},
|
||||
perfis_w: {
|
||||
file: 'BD/perfis/perfis_w_brasil_completo.csv',
|
||||
columns: ['id', 'nome', 'altura_mm', 'largura_mm', 'espessura_alma_mm', 'espessura_mesa_mm', 'peso_kg_m', 'area_cm2', 'tipo'],
|
||||
keyField: 'id',
|
||||
displayName: 'Perfis W'
|
||||
},
|
||||
tubos_rhs: {
|
||||
file: 'BD/perfis/tubos_rhs_brasil_completo.csv',
|
||||
columns: ['id', 'nome', 'largura_mm', 'altura_mm', 'espessura_mm', 'peso_kg_m', 'area_cm2', 'tipo'],
|
||||
keyField: 'id',
|
||||
displayName: 'Tubos RHS'
|
||||
},
|
||||
chapas: {
|
||||
file: 'BD/perfis/chapas_brasil_completo.csv',
|
||||
columns: ['id', 'nome', 'espessura_mm', 'peso_kg_m2', 'tipo'],
|
||||
keyField: 'id',
|
||||
displayName: 'Chapas'
|
||||
},
|
||||
perfis_hp: {
|
||||
file: 'BD/perfis/perfis_hp_brasil_completo.csv',
|
||||
columns: ['id', 'nome', 'altura_mm', 'largura_mm', 'espessura_alma_mm', 'espessura_mesa_mm', 'peso_kg_m', 'area_cm2', 'tipo'],
|
||||
keyField: 'id',
|
||||
displayName: 'Perfis HP'
|
||||
},
|
||||
barras_roscadas: {
|
||||
file: 'BD/perfis/barras_roscadas_brasil_completo.csv',
|
||||
columns: ['id', 'nome', 'diametro_mm', 'passo_mm', 'peso_kg_m', 'area_cm2', 'tipo'],
|
||||
keyField: 'id',
|
||||
displayName: 'Barras Roscadas'
|
||||
},
|
||||
barras_chatas: {
|
||||
file: 'BD/perfis/barras_chatas_brasil_completo.csv',
|
||||
columns: ['id', 'nome', 'largura_mm', 'espessura_mm', 'peso_kg_m', 'area_cm2', 'tipo'],
|
||||
keyField: 'id',
|
||||
displayName: 'Barras Chatas'
|
||||
}
|
||||
};
|
||||
|
||||
this.init();
|
||||
}
|
||||
|
||||
/**
|
||||
* Inicializa o gerenciador de dados
|
||||
*/
|
||||
async init() {
|
||||
console.log('🗄️ Inicializando Data Manager v' + this.version);
|
||||
|
||||
// Verificar se há dados em cache
|
||||
const metadata = this.getMetadata();
|
||||
if (!metadata || this.needsUpdate(metadata)) {
|
||||
console.log('📥 Cache vazio ou desatualizado. Carregando dados...');
|
||||
await this.updateAllData();
|
||||
} else {
|
||||
console.log('✅ Cache válido encontrado. Dados prontos para uso.');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifica se os dados precisam ser atualizados
|
||||
*/
|
||||
needsUpdate(metadata) {
|
||||
// Verificar versão
|
||||
if (metadata.version !== this.version) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Verificar TTL (24 horas)
|
||||
const now = Date.now();
|
||||
const ttl = 24 * 60 * 60 * 1000; // 24 horas
|
||||
if (now - metadata.lastUpdate > ttl) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Atualiza todos os dados dos CSVs
|
||||
*/
|
||||
async updateAllData() {
|
||||
console.log('🔄 Iniciando atualização completa dos dados...');
|
||||
|
||||
const results = {
|
||||
success: [],
|
||||
errors: []
|
||||
};
|
||||
|
||||
for (const [key, config] of Object.entries(this.csvConfigs)) {
|
||||
try {
|
||||
console.log(`📊 Carregando ${config.displayName}...`);
|
||||
const data = await this.loadCSV(config);
|
||||
this.saveToCache(key, data);
|
||||
const prevMeta = this.getTypeMetadata(key) || {};
|
||||
const normalizedPrevDoc = prevMeta.docSource ? this.normalizeDocSource(prevMeta.docSource) : null;
|
||||
this.setTypeMetadata(key, {
|
||||
source: config.file,
|
||||
lastUpdate: Date.now(),
|
||||
count: data.length,
|
||||
name: config.displayName,
|
||||
docSource: normalizedPrevDoc || null,
|
||||
docStatus: prevMeta.docStatus || null
|
||||
});
|
||||
results.success.push({
|
||||
key,
|
||||
name: config.displayName,
|
||||
count: data.length
|
||||
});
|
||||
console.log(`✅ ${config.displayName}: ${data.length} itens carregados`);
|
||||
} catch (error) {
|
||||
console.error(`❌ Erro ao carregar ${config.displayName}:`, error);
|
||||
results.errors.push({
|
||||
key,
|
||||
name: config.displayName,
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Atualizar metadata
|
||||
this.updateMetadata(results);
|
||||
|
||||
console.log('🎉 Atualização completa finalizada!');
|
||||
console.log(`✅ Sucessos: ${results.success.length}`);
|
||||
console.log(`❌ Erros: ${results.errors.length}`);
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
/**
|
||||
* Atualiza dados de um tipo específico e registra metadados
|
||||
*/
|
||||
async updateTypeData(type, options = {}) {
|
||||
const config = this.csvConfigs[type];
|
||||
if (!config) {
|
||||
throw new Error(`Tipo de dados não configurado: ${type}`);
|
||||
}
|
||||
// Limpa cache anterior do tipo
|
||||
try { localStorage.removeItem(this.cachePrefix + type); } catch(_) {}
|
||||
let data = await this.loadCSV(config);
|
||||
this.saveToCache(type, data);
|
||||
// Preparar metadados, preservando docSource anterior se não fornecido
|
||||
const prevMeta = this.getTypeMetadata(type) || {};
|
||||
const meta = {
|
||||
source: config.file,
|
||||
lastUpdate: Date.now(),
|
||||
count: data.length,
|
||||
name: config.displayName,
|
||||
docSource: options.docSource || prevMeta.docSource || null,
|
||||
docStatus: null
|
||||
};
|
||||
// Normalizar e validar docSource (.md)
|
||||
if (meta.docSource && typeof meta.docSource === 'string' && meta.docSource.trim().length > 0) {
|
||||
const originalDoc = meta.docSource.trim();
|
||||
const normalized = this.normalizeDocSource(originalDoc);
|
||||
meta.docSource = normalized; // armazenar caminho normalizado
|
||||
try {
|
||||
const resp = await fetch(normalized, { cache: 'no-cache' });
|
||||
if (resp.ok) {
|
||||
meta.docStatus = 'ok';
|
||||
const text = await resp.text();
|
||||
// Extrair insights do documento para o tipo
|
||||
const insights = this.parseMarkdownDocForType(type, text);
|
||||
// Se o documento fornecer tabela técnica, mesclar/substituir dados
|
||||
if (insights && insights.technicalItems && insights.technicalItems.length) {
|
||||
data = this.mergeDataWithDoc(type, data, insights.technicalItems);
|
||||
}
|
||||
meta.docInsights = {
|
||||
found: !!insights,
|
||||
sections: insights?.sections || {},
|
||||
priceHints: insights?.priceHints || [],
|
||||
manufacturers: insights?.manufacturers || [],
|
||||
applications: insights?.applications || [],
|
||||
recommendations: insights?.recommendations || []
|
||||
};
|
||||
} else {
|
||||
meta.docStatus = `não encontrado (HTTP ${resp.status})`;
|
||||
}
|
||||
} catch (e) {
|
||||
// Mensagem mais clara para caminhos absolutos ou externos
|
||||
if (originalDoc.toLowerCase().startsWith('file://') || /^[a-zA-Z]:\\/.test(originalDoc)) {
|
||||
meta.docStatus = 'caminho absoluto não suportado; use relativo';
|
||||
} else {
|
||||
meta.docStatus = 'erro ao carregar';
|
||||
}
|
||||
console.warn(`Falha ao carregar documento .md: '${originalDoc}' -> '${normalized}'`, e);
|
||||
}
|
||||
}
|
||||
this.setTypeMetadata(type, meta);
|
||||
// Atualiza metadata global (última operação)
|
||||
const metaGlobal = this.getMetadata() || { version: this.version };
|
||||
metaGlobal.lastUpdate = Date.now();
|
||||
try { localStorage.setItem(this.metadataKey, JSON.stringify(metaGlobal)); } catch(_) {}
|
||||
return { key: type, name: config.displayName, count: data.length, source: config.file, lastUpdate: Date.now(), data, docSource: meta.docSource, docStatus: meta.docStatus, docInsights: meta.docInsights };
|
||||
}
|
||||
|
||||
/**
|
||||
* Carrega e processa um arquivo CSV
|
||||
*/
|
||||
async loadCSV(config) {
|
||||
const response = await fetch(config.file);
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
||||
}
|
||||
|
||||
const csvText = await response.text();
|
||||
const lines = csvText.trim().split('\n');
|
||||
|
||||
if (lines.length < 2) {
|
||||
throw new Error('Arquivo CSV vazio ou inválido');
|
||||
}
|
||||
|
||||
const data = [];
|
||||
const headers = lines[0].split(',').map(h => h.trim());
|
||||
|
||||
// Processar dados
|
||||
for (let i = 1; i < lines.length; i++) {
|
||||
const line = lines[i].trim();
|
||||
if (!line) continue;
|
||||
|
||||
const values = line.split(',').map(v => v.trim());
|
||||
if (values.length < config.columns.length) continue;
|
||||
|
||||
const item = {};
|
||||
config.columns.forEach((col, index) => {
|
||||
let value = values[index] || '';
|
||||
|
||||
// Converter números
|
||||
if (col.includes('_mm') || col.includes('_kg') || col.includes('_cm') || col.includes('_m2')) {
|
||||
value = parseFloat(value) || 0;
|
||||
} else {
|
||||
value = value.replace(/"/g, ''); // Remover aspas
|
||||
}
|
||||
|
||||
item[col] = value;
|
||||
});
|
||||
|
||||
// Adicionar metadados
|
||||
item._metadata = {
|
||||
source: config.file,
|
||||
loadedAt: new Date().toISOString(),
|
||||
version: this.version
|
||||
};
|
||||
|
||||
data.push(item);
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Normaliza diferentes formatos de caminho para arquivos .md
|
||||
* Aceita entradas como:
|
||||
* - file:///C:/projeto/conhecimento/aco/tubos_rhs.md
|
||||
* - C:\projeto\BD\docs\arquivo.md
|
||||
* - conhecimento/aco/tubos_rhs.md
|
||||
* Retorna caminho relativo utilizável pelo servidor estático: ex. "conhecimento/aco/tubos_rhs.md".
|
||||
*/
|
||||
normalizeDocSource(inputPath) {
|
||||
if (!inputPath || typeof inputPath !== 'string') return null;
|
||||
let p = inputPath.trim();
|
||||
// Remover prefixo file:// se existir
|
||||
if (p.toLowerCase().startsWith('file://')) {
|
||||
p = p.replace(/^file:\/\//i, '');
|
||||
}
|
||||
// Substituir barras invertidas por barras normais
|
||||
p = p.replace(/\\/g, '/');
|
||||
// Remover drive letter (ex.: C:/ ou I:/)
|
||||
p = p.replace(/^[a-zA-Z]:\//, '');
|
||||
// Procurar marcadores conhecidos (BD/ ou conhecimento/)
|
||||
const lower = p.toLowerCase();
|
||||
let idx = -1;
|
||||
if ((idx = lower.lastIndexOf('/bd/')) !== -1) {
|
||||
p = p.substring(idx + 1); // manter a partir de BD/
|
||||
} else if ((idx = lower.lastIndexOf('/conhecimento/')) !== -1) {
|
||||
p = p.substring(idx + 1); // manter a partir de conhecimento/
|
||||
}
|
||||
// Garantir que usamos somente partes relativas
|
||||
p = p.replace(/^\/+/, '');
|
||||
// Validar extensão .md
|
||||
if (!p.toLowerCase().endsWith('.md')) {
|
||||
return p; // permitir salvar mesmo sem extensão correta; validação ocorrerá ao tentar buscar
|
||||
}
|
||||
return p;
|
||||
}
|
||||
|
||||
// =====================
|
||||
// PARSERS DE MARKDOWN
|
||||
// =====================
|
||||
|
||||
/**
|
||||
* Converte uma tabela markdown (pipe format) para array de objetos
|
||||
*/
|
||||
parseMarkdownTables(text) {
|
||||
const lines = text.split(/\r?\n/);
|
||||
const tables = [];
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const line = lines[i];
|
||||
// Detecta título imediatamente acima de tabela
|
||||
if (/^\s*\|.+\|\s*$/.test(line) && i > 0) {
|
||||
// Cabeçalho
|
||||
const header = line.trim().slice(1, -1).split('|').map(s => s.trim());
|
||||
// Separador
|
||||
const sep = lines[i + 1] || '';
|
||||
if (!/^\s*\|?\s*[-:]+/.test(sep)) continue;
|
||||
const rows = [];
|
||||
let j = i + 2;
|
||||
while (j < lines.length && /^\s*\|.+\|\s*$/.test(lines[j])) {
|
||||
const cols = lines[j].trim().slice(1, -1).split('|').map(s => s.trim());
|
||||
const obj = {};
|
||||
header.forEach((h, idx) => { obj[h] = cols[idx]; });
|
||||
rows.push(obj);
|
||||
j++;
|
||||
}
|
||||
// Título busca uma linha anterior iniciada por '#', '##' ou '###'
|
||||
let title = '';
|
||||
for (let k = i - 1; k >= Math.max(0, i - 4); k--) {
|
||||
if (/^\s*#{1,6}\s+/.test(lines[k])) { title = lines[k].replace(/^\s*#{1,6}\s+/, '').trim(); break; }
|
||||
if (/\*\*.+\*\*/.test(lines[k])) { title = lines[k].replace(/\*\*/g, '').trim(); break; }
|
||||
}
|
||||
tables.push({ title, header, rows });
|
||||
i = j - 1;
|
||||
}
|
||||
}
|
||||
return tables;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extrai texto de uma seção por título (## Seção)
|
||||
*/
|
||||
extractSection(text, title) {
|
||||
const regex = new RegExp(`^##\s+${title.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\s*$`, 'mi');
|
||||
const match = regex.exec(text);
|
||||
if (!match) return null;
|
||||
const start = match.index + match[0].length;
|
||||
const rest = text.slice(start);
|
||||
// Até próxima seção de nível 2
|
||||
const next = /\n##\s+/m.exec(rest);
|
||||
const sectionText = next ? rest.slice(0, next.index) : rest;
|
||||
return sectionText.trim();
|
||||
}
|
||||
|
||||
/**
|
||||
* Parser específico por tipo
|
||||
*/
|
||||
parseMarkdownDocForType(type, text) {
|
||||
const tables = this.parseMarkdownTables(text);
|
||||
const sections = {};
|
||||
const getBullets = (t) => (t || '').split(/\r?\n/).filter(l => /^\s*[-*•]/.test(l)).map(l => l.replace(/^\s*[-*•]\s*/, '').trim());
|
||||
// Seções comuns
|
||||
sections.visaoGeral = this.extractSection(text, 'VISÃO GERAL RHS');
|
||||
sections.normasTecnicas = this.extractSection(text, 'NORMAS TÉCNICAS');
|
||||
sections.recomendacoesTecnicas = this.extractSection(text, 'Recomendações Técnicas') || this.extractSection(text, 'Recomendações');
|
||||
sections.fabricantesBrasil = this.extractSection(text, 'Fabricantes Brasil') || this.extractSection(text, 'Fabricantes');
|
||||
sections.aplicacoes = this.extractSection(text, 'Seleção por Aplicação') || this.extractSection(text, 'Aplicações');
|
||||
|
||||
const priceHints = [];
|
||||
// Capturar ocorrências de preços (R$ xx ou xx R$/kg)
|
||||
const priceRegex = /(R\$\s*\d+(?:[.,]\d+)?(?:\s*[-–]\s*\d+(?:[.,]\d+)?)?\s*(?:R\$)?\s*(?:\/kg|kg)?)/gi;
|
||||
let m;
|
||||
while ((m = priceRegex.exec(text)) !== null) { priceHints.push(m[0]); }
|
||||
|
||||
if (type === 'tubos_rhs') {
|
||||
const master = tables.find(t => /Tabela Master/i.test(t.title));
|
||||
let technicalItems = [];
|
||||
if (master) {
|
||||
technicalItems = master.rows.map(row => {
|
||||
// Designação: ex. "RHS 120×80×4" → extrair dims
|
||||
const des = (row['Designação'] || row['Designacao'] || row['Nome'] || '').replace(/^RHS\s*/i, '').trim();
|
||||
const parts = des.split(/[×x]/).map(s => s.trim());
|
||||
const largura = parseFloat(parts[0]) || parseFloat(row['Largura (mm)']) || parseFloat(row['Dimensão (mm)']) || null;
|
||||
const altura = parts[1] ? parseFloat(parts[1]) : (parseFloat(row['Altura (mm)']) || null);
|
||||
const esp = parts[2] ? parseFloat(parts[2]) : (parseFloat((row['Espes. (mm)'] || row['Espes.'])) || null);
|
||||
const peso = parseFloat(String(row['Peso (kg/m)'] || '').replace(',', '.'));
|
||||
const area = parseFloat(String(row['Área (cm²)'] || '').replace(',', '.'));
|
||||
const tipo = row['Tipo'] || this.classificarRhsPorDimensao(largura, altura);
|
||||
return {
|
||||
id: row['ID'] || null,
|
||||
nome: des || row['Designação'] || '',
|
||||
largura_mm: largura,
|
||||
altura_mm: altura ?? largura,
|
||||
espessura_mm: esp,
|
||||
peso_kg_m: isNaN(peso) ? null : peso,
|
||||
area_cm2: isNaN(area) ? null : area,
|
||||
tipo: tipo || '—'
|
||||
};
|
||||
});
|
||||
}
|
||||
return {
|
||||
sections,
|
||||
priceHints,
|
||||
technicalItems,
|
||||
manufacturers: getBullets(sections.fabricantesBrasil),
|
||||
applications: getBullets(sections.aplicacoes),
|
||||
recommendations: getBullets(sections.recomendacoesTecnicas)
|
||||
};
|
||||
}
|
||||
// Tipos não suportados ainda
|
||||
return { sections, priceHints };
|
||||
}
|
||||
|
||||
classificarRhsPorDimensao(l, h) {
|
||||
const max = Math.max(l || 0, h || 0);
|
||||
if (max >= 200) return 'Massiva';
|
||||
if (max >= 150) return 'Muito Grande';
|
||||
if (max >= 120) return 'Grande';
|
||||
if (max >= 80) return 'Médio';
|
||||
return 'Pequeno';
|
||||
}
|
||||
|
||||
/**
|
||||
* Mescla dados carregados (CSV) com itens técnicos do documento
|
||||
* Preferência para valores do documento quando disponíveis
|
||||
*/
|
||||
mergeDataWithDoc(type, baseData, docItems) {
|
||||
if (type !== 'tubos_rhs') return baseData;
|
||||
const byNome = new Map((baseData || []).map(it => [String(it.nome).toLowerCase(), it]));
|
||||
docItems.forEach(d => {
|
||||
const key = String(d.nome || '').toLowerCase();
|
||||
if (byNome.has(key)) {
|
||||
const curr = byNome.get(key);
|
||||
byNome.set(key, {
|
||||
...curr,
|
||||
largura_mm: d.largura_mm ?? curr.largura_mm,
|
||||
altura_mm: d.altura_mm ?? curr.altura_mm,
|
||||
espessura_mm: d.espessura_mm ?? curr.espessura_mm,
|
||||
peso_kg_m: d.peso_kg_m ?? curr.peso_kg_m,
|
||||
area_cm2: d.area_cm2 ?? curr.area_cm2,
|
||||
tipo: d.tipo ?? curr.tipo
|
||||
});
|
||||
} else {
|
||||
byNome.set(key, d);
|
||||
}
|
||||
});
|
||||
return Array.from(byNome.values());
|
||||
}
|
||||
|
||||
/**
|
||||
* Salva dados no cache
|
||||
*/
|
||||
saveToCache(key, data) {
|
||||
try {
|
||||
const cacheKey = this.cachePrefix + key;
|
||||
localStorage.setItem(cacheKey, JSON.stringify(data));
|
||||
console.log(`💾 Cache salvo: ${key} (${data.length} itens)`);
|
||||
} catch (error) {
|
||||
console.error(`❌ Erro ao salvar cache ${key}:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Carrega dados do cache
|
||||
*/
|
||||
loadFromCache(key) {
|
||||
try {
|
||||
const cacheKey = this.cachePrefix + key;
|
||||
const data = localStorage.getItem(cacheKey);
|
||||
if (!data) return null;
|
||||
|
||||
const parsed = JSON.parse(data);
|
||||
console.log(`📂 Cache carregado: ${key} (${parsed.length} itens)`);
|
||||
return parsed;
|
||||
} catch (error) {
|
||||
console.error(`❌ Erro ao carregar cache ${key}:`, error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtém dados de um tipo específico
|
||||
*/
|
||||
async getData(type) {
|
||||
// Tentar carregar do cache primeiro
|
||||
let data = this.loadFromCache(type);
|
||||
|
||||
if (!data) {
|
||||
// Se não há cache, carregar do CSV
|
||||
console.log(`📥 Cache não encontrado para ${type}. Carregando do CSV...`);
|
||||
const config = this.csvConfigs[type];
|
||||
if (!config) {
|
||||
throw new Error(`Tipo de dados não configurado: ${type}`);
|
||||
}
|
||||
|
||||
data = await this.loadCSV(config);
|
||||
this.saveToCache(type, data);
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Filtra dados baseado em critérios
|
||||
*/
|
||||
filterData(data, filters = {}) {
|
||||
if (!data || !Array.isArray(data)) return [];
|
||||
|
||||
return data.filter(item => {
|
||||
for (const [key, value] of Object.entries(filters)) {
|
||||
if (value === null || value === undefined || value === '') continue;
|
||||
|
||||
const itemValue = item[key];
|
||||
|
||||
if (typeof value === 'string') {
|
||||
if (!itemValue || !itemValue.toString().toLowerCase().includes(value.toLowerCase())) {
|
||||
return false;
|
||||
}
|
||||
} else if (typeof value === 'number') {
|
||||
if (itemValue !== value) {
|
||||
return false;
|
||||
}
|
||||
} else if (typeof value === 'object' && value.min !== undefined) {
|
||||
if (itemValue < value.min) return false;
|
||||
} else if (typeof value === 'object' && value.max !== undefined) {
|
||||
if (itemValue > value.max) return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Busca dados por texto
|
||||
*/
|
||||
searchData(data, searchTerm, searchFields = ['nome']) {
|
||||
if (!searchTerm || !data) return data;
|
||||
|
||||
const term = searchTerm.toLowerCase();
|
||||
return data.filter(item => {
|
||||
return searchFields.some(field => {
|
||||
const value = item[field];
|
||||
return value && value.toString().toLowerCase().includes(term);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtém metadados do cache
|
||||
*/
|
||||
getMetadata() {
|
||||
try {
|
||||
const data = localStorage.getItem(this.metadataKey);
|
||||
return data ? JSON.parse(data) : null;
|
||||
} catch (error) {
|
||||
console.error('❌ Erro ao carregar metadata:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Atualiza metadados
|
||||
*/
|
||||
updateMetadata(results) {
|
||||
const metadata = {
|
||||
version: this.version,
|
||||
lastUpdate: Date.now(),
|
||||
results: results,
|
||||
totalTypes: Object.keys(this.csvConfigs).length,
|
||||
successCount: results.success.length,
|
||||
errorCount: results.errors.length
|
||||
};
|
||||
|
||||
try {
|
||||
localStorage.setItem(this.metadataKey, JSON.stringify(metadata));
|
||||
console.log('💾 Metadata atualizado');
|
||||
} catch (error) {
|
||||
console.error('❌ Erro ao salvar metadata:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Metadados por tipo (fonte e última atualização)
|
||||
*/
|
||||
getAllTypesMetadata() {
|
||||
try {
|
||||
const raw = localStorage.getItem(this.typesMetadataKey);
|
||||
return raw ? JSON.parse(raw) : {};
|
||||
} catch (e) {
|
||||
console.error('❌ Erro ao ler metadados por tipo:', e);
|
||||
return {};
|
||||
}
|
||||
}
|
||||
getTypeMetadata(type) {
|
||||
const all = this.getAllTypesMetadata();
|
||||
return all[type] || null;
|
||||
}
|
||||
setTypeMetadata(type, metadata) {
|
||||
try {
|
||||
const all = this.getAllTypesMetadata();
|
||||
all[type] = metadata;
|
||||
localStorage.setItem(this.typesMetadataKey, JSON.stringify(all));
|
||||
console.log(`💾 Metadata do tipo salvo: ${type}`);
|
||||
} catch (e) {
|
||||
console.error('❌ Erro ao salvar metadados por tipo:', e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Limpa todo o cache
|
||||
*/
|
||||
clearCache() {
|
||||
console.log('🗑️ Limpando cache...');
|
||||
|
||||
// Remover dados
|
||||
Object.keys(this.csvConfigs).forEach(key => {
|
||||
const cacheKey = this.cachePrefix + key;
|
||||
localStorage.removeItem(cacheKey);
|
||||
});
|
||||
|
||||
// Remover metadata
|
||||
localStorage.removeItem(this.metadataKey);
|
||||
localStorage.removeItem(this.typesMetadataKey);
|
||||
|
||||
console.log('✅ Cache limpo');
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtém estatísticas do cache
|
||||
*/
|
||||
getCacheStats() {
|
||||
const metadata = this.getMetadata();
|
||||
const stats = {
|
||||
version: this.version,
|
||||
hasCache: !!metadata,
|
||||
lastUpdate: metadata?.lastUpdate,
|
||||
types: {}
|
||||
};
|
||||
|
||||
Object.keys(this.csvConfigs).forEach(key => {
|
||||
const data = this.loadFromCache(key);
|
||||
stats.types[key] = {
|
||||
name: this.csvConfigs[key].displayName,
|
||||
count: data ? data.length : 0,
|
||||
cached: !!data,
|
||||
meta: this.getTypeMetadata(key)
|
||||
};
|
||||
});
|
||||
|
||||
return stats;
|
||||
}
|
||||
}
|
||||
|
||||
// Instância global
|
||||
window.dataManager = new DataManager();
|
||||
|
||||
console.log('✅ Data Manager carregado e disponível globalmente');
|
||||
|
||||
|
||||
// ========================================
|
||||
// ATUALIZAÇÃO DO BADGE DE STATUS
|
||||
// ========================================
|
||||
|
||||
/**
|
||||
* Atualiza o badge de status do cache no header
|
||||
*/
|
||||
function atualizarBadgeStatus() {
|
||||
const badge = document.getElementById('cache-status-badge');
|
||||
const icon = document.getElementById('cache-icon');
|
||||
const text = document.getElementById('cache-text');
|
||||
|
||||
if (!badge || !icon || !text) return;
|
||||
|
||||
try {
|
||||
const stats = window.dataManager.getCacheStats();
|
||||
|
||||
// Remover classes antigas
|
||||
badge.classList.remove('cache-active', 'cache-empty');
|
||||
|
||||
if (stats.hasCache) {
|
||||
// Cache ativo
|
||||
icon.textContent = '✅';
|
||||
text.textContent = 'Cache Ativo';
|
||||
badge.classList.add('cache-active');
|
||||
badge.title = `Cache ativo - ${Object.values(stats.types).reduce((sum, t) => sum + t.count, 0)} itens carregados`;
|
||||
} else {
|
||||
// Cache vazio
|
||||
icon.textContent = '❌';
|
||||
text.textContent = 'Sem Cache';
|
||||
badge.classList.add('cache-empty');
|
||||
badge.title = 'Cache vazio - Clique para carregar dados';
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('❌ Erro ao atualizar badge:', error);
|
||||
icon.textContent = '⚠️';
|
||||
text.textContent = 'Erro';
|
||||
badge.title = 'Erro ao verificar cache';
|
||||
}
|
||||
}
|
||||
|
||||
// Atualizar badge quando Data Manager estiver pronto
|
||||
if (window.dataManager) {
|
||||
// Aguardar um pouco para garantir que o DOM está pronto
|
||||
setTimeout(() => {
|
||||
atualizarBadgeStatus();
|
||||
// Atualizar a cada 5 segundos
|
||||
setInterval(atualizarBadgeStatus, 5000);
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
console.log('✅ Badge de status configurado');
|
||||
342
js/database/importador-csv.js
Normal file
342
js/database/importador-csv.js
Normal file
@@ -0,0 +1,342 @@
|
||||
/**
|
||||
* IMPORTADOR DE CSV - Ferramenta de Atualização do BD Interno
|
||||
* Permite carregar CSVs externos e atualizar o banco de dados interno
|
||||
*/
|
||||
|
||||
/**
|
||||
* Abre modal de importação de CSV
|
||||
*/
|
||||
function abrirImportadorCSV() {
|
||||
console.log('📥 Abrindo importador de CSV');
|
||||
|
||||
const modalHTML = `
|
||||
<div class="modal active" id="modal-importador-csv" onclick="fecharImportadorCSV(event)">
|
||||
<div class="modal-content" onclick="event.stopPropagation()" style="max-width: 900px;">
|
||||
<div class="modal-header">
|
||||
<div class="modal-title">📥 Importador de CSV - Atualizar BD Interno</div>
|
||||
<button class="close-btn" onclick="fecharImportadorCSV()">×</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
|
||||
<!-- Instruções -->
|
||||
<div class="card" style="background: var(--color-bg-1); margin-bottom: 20px;">
|
||||
<h3 style="color: var(--color-primary); margin: 0 0 16px 0;">📋 Como Usar</h3>
|
||||
<ol style="margin: 0; padding-left: 20px;">
|
||||
<li>Selecione o tipo de perfil que deseja atualizar</li>
|
||||
<li>Escolha o arquivo CSV com os novos dados</li>
|
||||
<li>Clique em "Importar" para atualizar o BD interno</li>
|
||||
<li>Os dados serão salvos no localStorage e usados imediatamente</li>
|
||||
</ol>
|
||||
</div>
|
||||
|
||||
<!-- Seleção de Tipo -->
|
||||
<div class="card" style="background: var(--color-bg-1); margin-bottom: 20px;">
|
||||
<h3 style="color: var(--color-primary); margin: 0 0 16px 0;">🎯 Selecionar Tipo de Perfil</h3>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Tipo de Perfil:</label>
|
||||
<select class="form-control" id="tipo-perfil-importar">
|
||||
<option value="">Selecione um tipo...</option>
|
||||
<option value="cantoneiras">Cantoneiras</option>
|
||||
<option value="barras_redondas">Barras Redondas</option>
|
||||
<option value="tubos_circulares">Tubos Circulares</option>
|
||||
<option value="perfis_i">Perfis I (IPE)</option>
|
||||
<option value="perfis_w">Perfis W</option>
|
||||
<option value="tubos_rhs">Tubos RHS</option>
|
||||
<option value="chapas">Chapas</option>
|
||||
<option value="perfis_hp">Perfis HP</option>
|
||||
<option value="barras_roscadas">Barras Roscadas</option>
|
||||
<option value="barras_chatas">Barras Chatas</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Upload de Arquivo -->
|
||||
<div class="card" style="background: var(--color-bg-1); margin-bottom: 20px;">
|
||||
<h3 style="color: var(--color-primary); margin: 0 0 16px 0;">📁 Selecionar Arquivo CSV</h3>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Arquivo CSV:</label>
|
||||
<input type="file" class="form-control" id="arquivo-csv" accept=".csv" onchange="visualizarCSV()">
|
||||
<small style="color: var(--color-text-secondary); margin-top: 8px; display: block;">
|
||||
Selecione um arquivo CSV com os dados dos perfis
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Preview do CSV -->
|
||||
<div class="card" style="background: var(--color-bg-1); margin-bottom: 20px; display: none;" id="preview-csv">
|
||||
<h3 style="color: var(--color-primary); margin: 0 0 16px 0;">👁️ Preview do Arquivo</h3>
|
||||
<div id="preview-conteudo" style="max-height: 300px; overflow: auto;"></div>
|
||||
<div id="preview-stats" style="margin-top: 12px; padding: 12px; background: var(--color-bg-2); border-radius: 6px;"></div>
|
||||
</div>
|
||||
|
||||
<!-- Log de Importação -->
|
||||
<div class="card" style="background: var(--color-bg-1); display: none;" id="log-importacao">
|
||||
<h3 style="color: var(--color-primary); margin: 0 0 16px 0;">📝 Log de Importação</h3>
|
||||
<div id="log-conteudo" style="background: #000; color: #0f0; padding: 16px; border-radius: 8px; font-family: monospace; font-size: 12px; max-height: 200px; overflow-y: auto;"></div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button class="btn btn-secondary" onclick="fecharImportadorCSV()">Cancelar</button>
|
||||
<button class="btn btn-primary" onclick="executarImportacao()" id="btn-importar" disabled>
|
||||
📥 Importar Dados
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Remover modal existente
|
||||
const existingModal = document.getElementById('modal-importador-csv');
|
||||
if (existingModal) {
|
||||
existingModal.remove();
|
||||
}
|
||||
|
||||
// Adicionar novo modal
|
||||
document.body.insertAdjacentHTML('beforeend', modalHTML);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fecha o importador de CSV
|
||||
*/
|
||||
function fecharImportadorCSV(event) {
|
||||
if (event && event.target !== event.currentTarget) return;
|
||||
|
||||
const modal = document.getElementById('modal-importador-csv');
|
||||
if (modal) {
|
||||
modal.remove();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Visualiza o conteúdo do CSV selecionado
|
||||
*/
|
||||
function visualizarCSV() {
|
||||
const arquivo = document.getElementById('arquivo-csv').files[0];
|
||||
const tipoSelecionado = document.getElementById('tipo-perfil-importar').value;
|
||||
|
||||
if (!arquivo) {
|
||||
document.getElementById('preview-csv').style.display = 'none';
|
||||
document.getElementById('btn-importar').disabled = true;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!tipoSelecionado) {
|
||||
alert('⚠️ Selecione o tipo de perfil primeiro!');
|
||||
return;
|
||||
}
|
||||
|
||||
const reader = new FileReader();
|
||||
reader.onload = function(e) {
|
||||
const csvText = e.target.result;
|
||||
const linhas = csvText.trim().split('\n');
|
||||
|
||||
// Mostrar preview
|
||||
const previewDiv = document.getElementById('preview-conteudo');
|
||||
const statsDiv = document.getElementById('preview-stats');
|
||||
|
||||
// Primeiras 5 linhas
|
||||
const preview = linhas.slice(0, 6).map((linha, index) => {
|
||||
const colunas = linha.split(',');
|
||||
const isHeader = index === 0;
|
||||
return `
|
||||
<div style="padding: 8px; background: ${isHeader ? 'var(--color-primary)' : 'var(--color-bg-2)'};
|
||||
color: ${isHeader ? 'white' : 'inherit'}; margin-bottom: 2px; border-radius: 4px;">
|
||||
<strong>${isHeader ? 'CABEÇALHO:' : `Linha ${index}:`}</strong>
|
||||
${colunas.join(' | ')}
|
||||
</div>
|
||||
`;
|
||||
}).join('');
|
||||
|
||||
previewDiv.innerHTML = preview;
|
||||
|
||||
// Estatísticas
|
||||
statsDiv.innerHTML = `
|
||||
<strong>📊 Estatísticas:</strong><br>
|
||||
• Total de linhas: ${linhas.length}<br>
|
||||
• Linhas de dados: ${linhas.length - 1}<br>
|
||||
• Colunas: ${linhas[0] ? linhas[0].split(',').length : 0}<br>
|
||||
• Tamanho: ${(csvText.length / 1024).toFixed(2)} KB
|
||||
`;
|
||||
|
||||
// Mostrar preview
|
||||
document.getElementById('preview-csv').style.display = 'block';
|
||||
document.getElementById('btn-importar').disabled = false;
|
||||
};
|
||||
|
||||
reader.readAsText(arquivo);
|
||||
}
|
||||
|
||||
/**
|
||||
* Executa a importação do CSV
|
||||
*/
|
||||
function executarImportacao() {
|
||||
const arquivo = document.getElementById('arquivo-csv').files[0];
|
||||
const tipo = document.getElementById('tipo-perfil-importar').value;
|
||||
|
||||
if (!arquivo || !tipo) {
|
||||
alert('⚠️ Selecione o tipo de perfil e o arquivo CSV!');
|
||||
return;
|
||||
}
|
||||
|
||||
// Mostrar log
|
||||
document.getElementById('log-importacao').style.display = 'block';
|
||||
const logDiv = document.getElementById('log-conteudo');
|
||||
|
||||
function log(msg) {
|
||||
const time = new Date().toLocaleTimeString();
|
||||
logDiv.innerHTML += `<div>[${time}] ${msg}</div>`;
|
||||
logDiv.scrollTop = logDiv.scrollHeight;
|
||||
}
|
||||
|
||||
log('🚀 Iniciando importação...');
|
||||
log(`📊 Tipo: ${tipo}`);
|
||||
log(`📁 Arquivo: ${arquivo.name}`);
|
||||
|
||||
const reader = new FileReader();
|
||||
reader.onload = function(e) {
|
||||
try {
|
||||
const csvText = e.target.result;
|
||||
const linhas = csvText.trim().split('\n');
|
||||
|
||||
log(`📄 Arquivo lido: ${linhas.length} linhas`);
|
||||
|
||||
if (linhas.length < 2) {
|
||||
throw new Error('Arquivo CSV deve ter pelo menos cabeçalho + 1 linha de dados');
|
||||
}
|
||||
|
||||
// Processar dados
|
||||
const cabecalho = linhas[0].split(',').map(h => h.trim().replace(/"/g, ''));
|
||||
log(`📋 Cabeçalho: ${cabecalho.join(', ')}`);
|
||||
|
||||
const dados = [];
|
||||
for (let i = 1; i < linhas.length; i++) {
|
||||
const linha = linhas[i].trim();
|
||||
if (!linha) continue;
|
||||
|
||||
const valores = linha.split(',').map(v => v.trim().replace(/"/g, ''));
|
||||
if (valores.length < cabecalho.length) continue;
|
||||
|
||||
const item = {};
|
||||
cabecalho.forEach((col, index) => {
|
||||
let valor = valores[index] || '';
|
||||
|
||||
// Converter números
|
||||
if (col.includes('_mm') || col.includes('_kg') || col.includes('_cm') || col.includes('_m2')) {
|
||||
valor = parseFloat(valor) || 0;
|
||||
}
|
||||
|
||||
item[col] = valor;
|
||||
});
|
||||
|
||||
dados.push(item);
|
||||
}
|
||||
|
||||
log(`✅ Processados: ${dados.length} itens`);
|
||||
|
||||
if (dados.length === 0) {
|
||||
throw new Error('Nenhum dado válido encontrado no CSV');
|
||||
}
|
||||
|
||||
// Atualizar banco de dados interno
|
||||
if (!window.BANCO_DADOS_PERFIS) {
|
||||
window.BANCO_DADOS_PERFIS = {};
|
||||
}
|
||||
|
||||
window.BANCO_DADOS_PERFIS[tipo] = dados;
|
||||
|
||||
// Salvar no localStorage
|
||||
const chaveCache = `acoCalcPro_dados_${tipo}`;
|
||||
localStorage.setItem(chaveCache, JSON.stringify(dados));
|
||||
|
||||
log(`💾 Dados salvos no localStorage: ${chaveCache}`);
|
||||
|
||||
// Atualizar metadata
|
||||
const metadata = {
|
||||
tipo: tipo,
|
||||
arquivo: arquivo.name,
|
||||
dataImportacao: new Date().toISOString(),
|
||||
totalItens: dados.length,
|
||||
colunas: cabecalho
|
||||
};
|
||||
|
||||
localStorage.setItem(`acoCalcPro_metadata_${tipo}`, JSON.stringify(metadata));
|
||||
|
||||
log(`📊 Metadata salvo`);
|
||||
|
||||
// Recarregar dados na tabela se estiver visível
|
||||
if (typeof carregarPerfilUniversal === 'function') {
|
||||
const sucesso = carregarPerfilUniversal(tipo);
|
||||
if (sucesso) {
|
||||
log(`🔄 Tabela atualizada automaticamente`);
|
||||
}
|
||||
}
|
||||
|
||||
log(`🎉 IMPORTAÇÃO CONCLUÍDA COM SUCESSO!`);
|
||||
|
||||
// Notificar usuário
|
||||
setTimeout(() => {
|
||||
alert(`✅ Importação concluída!\n\n📊 ${dados.length} itens de ${tipo} importados\n💾 Dados salvos no BD interno\n🔄 Tabela atualizada`);
|
||||
}, 1000);
|
||||
|
||||
} catch (error) {
|
||||
log(`❌ ERRO: ${error.message}`);
|
||||
alert(`❌ Erro na importação: ${error.message}`);
|
||||
}
|
||||
};
|
||||
|
||||
reader.readAsText(arquivo);
|
||||
}
|
||||
|
||||
/**
|
||||
* Exporta dados atuais para CSV
|
||||
*/
|
||||
function exportarDadosCSV(tipo) {
|
||||
if (!window.BANCO_DADOS_PERFIS || !window.BANCO_DADOS_PERFIS[tipo]) {
|
||||
alert(`❌ Nenhum dado encontrado para ${tipo}`);
|
||||
return;
|
||||
}
|
||||
|
||||
const dados = window.BANCO_DADOS_PERFIS[tipo];
|
||||
if (dados.length === 0) {
|
||||
alert(`❌ Dados vazios para ${tipo}`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Gerar CSV
|
||||
const cabecalho = Object.keys(dados[0]);
|
||||
const linhas = [cabecalho.join(',')];
|
||||
|
||||
dados.forEach(item => {
|
||||
const linha = cabecalho.map(col => {
|
||||
let valor = item[col];
|
||||
if (typeof valor === 'string' && valor.includes(',')) {
|
||||
valor = `"${valor}"`;
|
||||
}
|
||||
return valor;
|
||||
}).join(',');
|
||||
linhas.push(linha);
|
||||
});
|
||||
|
||||
const csvContent = linhas.join('\n');
|
||||
|
||||
// Download
|
||||
const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = `${tipo}_${new Date().toISOString().split('T')[0]}.csv`;
|
||||
a.click();
|
||||
URL.revokeObjectURL(url);
|
||||
|
||||
console.log(`✅ Dados de ${tipo} exportados para CSV`);
|
||||
}
|
||||
|
||||
// Exportar funções
|
||||
window.abrirImportadorCSV = abrirImportadorCSV;
|
||||
window.fecharImportadorCSV = fecharImportadorCSV;
|
||||
window.visualizarCSV = visualizarCSV;
|
||||
window.executarImportacao = executarImportacao;
|
||||
window.exportarDadosCSV = exportarDadosCSV;
|
||||
|
||||
console.log('✅ Importador de CSV carregado');
|
||||
330
js/database/perfis-loader.js
Normal file
330
js/database/perfis-loader.js
Normal file
@@ -0,0 +1,330 @@
|
||||
/**
|
||||
* CARREGADOR DE PERFIS - Usando Data Manager
|
||||
*
|
||||
* Funções específicas para carregar e exibir dados dos perfis
|
||||
* usando o sistema de cache inteligente.
|
||||
*/
|
||||
|
||||
// ========================================
|
||||
// CANTONEIRAS
|
||||
// ========================================
|
||||
|
||||
/**
|
||||
* Carrega dados das cantoneiras usando o Data Manager
|
||||
*/
|
||||
async function carregarCantoneirasV2() {
|
||||
console.log('🔧 carregarCantoneirasV2() - Nova versão com Data Manager');
|
||||
|
||||
try {
|
||||
// Verificar se o elemento existe
|
||||
const tbody = document.getElementById('cantoneiras-tbody');
|
||||
if (!tbody) {
|
||||
console.error('❌ Elemento cantoneiras-tbody não encontrado!');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('✅ Elemento tbody encontrado');
|
||||
|
||||
// Mostrar loading
|
||||
tbody.innerHTML = `
|
||||
<tr>
|
||||
<td colspan="9" style="text-align: center; padding: 20px;">
|
||||
<div style="display: flex; align-items: center; justify-content: center; gap: 10px;">
|
||||
<div class="spinner"></div>
|
||||
📊 Carregando cantoneiras do banco de dados...
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
`;
|
||||
|
||||
// Carregar dados via Data Manager
|
||||
const dados = await window.dataManager.getData('cantoneiras');
|
||||
|
||||
if (!dados || dados.length === 0) {
|
||||
tbody.innerHTML = `
|
||||
<tr>
|
||||
<td colspan="9" style="text-align: center; padding: 20px; color: var(--color-error);">
|
||||
❌ Nenhuma cantoneira encontrada no banco de dados.
|
||||
<br><br>
|
||||
<button class="btn btn-primary" onclick="atualizarDadosCantoneiras()">
|
||||
🔄 Atualizar Dados
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
`;
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`✅ ${dados.length} cantoneiras carregadas do Data Manager`);
|
||||
console.log('🔍 Primeiro item:', dados[0]);
|
||||
|
||||
// Armazenar globalmente para filtros
|
||||
window.cantoneirasData = dados;
|
||||
console.log('💾 Dados armazenados em window.cantoneirasData');
|
||||
|
||||
// Exibir na tabela
|
||||
console.log('🎨 Chamando exibirCantoneirasV2...');
|
||||
console.log('🔍 Tipo de exibirCantoneirasV2:', typeof exibirCantoneirasV2);
|
||||
|
||||
if (typeof exibirCantoneirasV2 === 'function') {
|
||||
exibirCantoneirasV2(dados);
|
||||
} else {
|
||||
console.error('❌ exibirCantoneirasV2 não é uma função!');
|
||||
// Fallback: exibir diretamente
|
||||
console.log('🔄 Usando fallback para exibir dados...');
|
||||
const tbody = document.getElementById('cantoneiras-tbody');
|
||||
if (tbody) {
|
||||
tbody.innerHTML = dados.map(item => `
|
||||
<tr>
|
||||
<td><strong>${item.nome}</strong></td>
|
||||
<td>${item.lado_mm}</td>
|
||||
<td>${item.espessura_mm}</td>
|
||||
<td>${item.peso_kg_m.toFixed(2)}</td>
|
||||
<td>${item.area_cm2.toFixed(2)}</td>
|
||||
<td>${item.momento_inercia_cm4.toFixed(2)}</td>
|
||||
<td>${item.raio_giracao_cm.toFixed(2)}</td>
|
||||
<td><span class="badge">${item.tipo}</span></td>
|
||||
<td><button class="btn btn-sm">Ver</button></td>
|
||||
</tr>
|
||||
`).join('');
|
||||
console.log('✅ Tabela preenchida via fallback!');
|
||||
} else {
|
||||
console.error('❌ Elemento tbody não encontrado no fallback!');
|
||||
}
|
||||
}
|
||||
|
||||
// Atualizar contador
|
||||
const totalEl = document.getElementById('cant-total');
|
||||
if (totalEl) {
|
||||
totalEl.textContent = dados.length;
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ Erro ao carregar cantoneiras:', error);
|
||||
|
||||
const tbody = document.getElementById('cantoneiras-tbody');
|
||||
if (tbody) {
|
||||
tbody.innerHTML = `
|
||||
<tr>
|
||||
<td colspan="9" style="text-align: center; padding: 20px; color: var(--color-error);">
|
||||
❌ Erro ao carregar dados: ${error.message}
|
||||
<br><br>
|
||||
<button class="btn btn-primary" onclick="atualizarDadosCantoneiras()">
|
||||
🔄 Tentar Novamente
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Exibe cantoneiras na tabela (versão otimizada)
|
||||
*/
|
||||
function exibirCantoneirasV2(dados) {
|
||||
console.log(`📋 Exibindo ${dados.length} cantoneiras na tabela`);
|
||||
|
||||
const tbody = document.getElementById('cantoneiras-tbody');
|
||||
if (!tbody) {
|
||||
console.error('❌ Elemento tbody não encontrado');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!dados || dados.length === 0) {
|
||||
tbody.innerHTML = `
|
||||
<tr>
|
||||
<td colspan="9" style="text-align: center; padding: 20px;">
|
||||
Nenhuma cantoneira encontrada
|
||||
</td>
|
||||
</tr>
|
||||
`;
|
||||
return;
|
||||
}
|
||||
|
||||
// Gerar HTML otimizado
|
||||
const html = dados.map(item => {
|
||||
const badgeColor = getBadgeColorV2(item.tipo);
|
||||
return `
|
||||
<tr>
|
||||
<td><strong>${item.nome}</strong></td>
|
||||
<td>${item.lado_mm}</td>
|
||||
<td>${item.espessura_mm}</td>
|
||||
<td>${item.peso_kg_m.toFixed(2)}</td>
|
||||
<td>${item.area_cm2.toFixed(2)}</td>
|
||||
<td>${item.momento_inercia_cm4.toFixed(2)}</td>
|
||||
<td>${item.raio_giracao_cm.toFixed(2)}</td>
|
||||
<td><span class="badge badge-${badgeColor}">${item.tipo}</span></td>
|
||||
<td>
|
||||
<button class="btn btn-sm btn-primary" onclick="verDetalhesCantoneira('${item.id}')">
|
||||
👁️ Ver
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
`;
|
||||
}).join('');
|
||||
|
||||
tbody.innerHTML = html;
|
||||
console.log('✅ Tabela atualizada com sucesso');
|
||||
}
|
||||
|
||||
/**
|
||||
* Filtra cantoneiras usando Data Manager
|
||||
*/
|
||||
function filtrarCantoneirasV2() {
|
||||
if (!window.cantoneirasData) {
|
||||
console.warn('⚠️ Dados não carregados ainda');
|
||||
return;
|
||||
}
|
||||
|
||||
// Obter filtros
|
||||
const tamanho = document.getElementById('cant-tamanho')?.value || '';
|
||||
const pesoMax = parseFloat(document.getElementById('cant-peso-max')?.value) || Infinity;
|
||||
const busca = document.getElementById('cant-busca')?.value || '';
|
||||
|
||||
console.log('🔍 Aplicando filtros:', { tamanho, pesoMax, busca });
|
||||
|
||||
// Aplicar filtros usando Data Manager
|
||||
let filtrados = window.cantoneirasData;
|
||||
|
||||
// Filtro por tipo/tamanho
|
||||
if (tamanho) {
|
||||
filtrados = window.dataManager.filterData(filtrados, { tipo: tamanho });
|
||||
}
|
||||
|
||||
// Filtro por peso máximo
|
||||
if (pesoMax < Infinity) {
|
||||
filtrados = window.dataManager.filterData(filtrados, { peso_kg_m: { max: pesoMax } });
|
||||
}
|
||||
|
||||
// Busca por nome
|
||||
if (busca) {
|
||||
filtrados = window.dataManager.searchData(filtrados, busca, ['nome', 'id']);
|
||||
}
|
||||
|
||||
console.log(`🎯 Filtrados: ${filtrados.length} de ${window.cantoneirasData.length}`);
|
||||
|
||||
// Exibir resultados
|
||||
exibirCantoneirasV2(filtrados);
|
||||
|
||||
// Atualizar contador
|
||||
const totalEl = document.getElementById('cant-total');
|
||||
if (totalEl) {
|
||||
totalEl.textContent = filtrados.length;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Limpa filtros das cantoneiras
|
||||
*/
|
||||
function limparFiltrosCantoneirasV2() {
|
||||
console.log('🧹 Limpando filtros');
|
||||
|
||||
// Limpar campos
|
||||
const tamanhoEl = document.getElementById('cant-tamanho');
|
||||
const pesoEl = document.getElementById('cant-peso-max');
|
||||
const buscaEl = document.getElementById('cant-busca');
|
||||
|
||||
if (tamanhoEl) tamanhoEl.value = '';
|
||||
if (pesoEl) pesoEl.value = '';
|
||||
if (buscaEl) buscaEl.value = '';
|
||||
|
||||
// Exibir todos os dados
|
||||
if (window.cantoneirasData) {
|
||||
exibirCantoneirasV2(window.cantoneirasData);
|
||||
|
||||
const totalEl = document.getElementById('cant-total');
|
||||
if (totalEl) {
|
||||
totalEl.textContent = window.cantoneirasData.length;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Força atualização dos dados das cantoneiras
|
||||
*/
|
||||
async function atualizarDadosCantoneiras() {
|
||||
console.log('🔄 Forçando atualização dos dados das cantoneiras...');
|
||||
|
||||
try {
|
||||
// Limpar cache específico
|
||||
localStorage.removeItem('acoCalcPro_cache_cantoneiras');
|
||||
|
||||
// Recarregar
|
||||
await carregarCantoneirasV2();
|
||||
|
||||
// Notificar sucesso
|
||||
alert('✅ Dados das cantoneiras atualizados com sucesso!');
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ Erro ao atualizar dados:', error);
|
||||
alert('❌ Erro ao atualizar dados: ' + error.message);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retorna cor do badge (versão otimizada)
|
||||
*/
|
||||
function getBadgeColorV2(tipo) {
|
||||
const cores = {
|
||||
'Pequena': 'info',
|
||||
'Média': 'success',
|
||||
'Grande': 'warning',
|
||||
'Muito Grande': 'warning',
|
||||
'Extra-Grande': 'error',
|
||||
'Massiva': 'error'
|
||||
};
|
||||
return cores[tipo] || 'info';
|
||||
}
|
||||
|
||||
/**
|
||||
* Ver detalhes de uma cantoneira específica
|
||||
*/
|
||||
function verDetalhesCantoneira(id) {
|
||||
if (!window.cantoneirasData) {
|
||||
alert('❌ Dados não carregados');
|
||||
return;
|
||||
}
|
||||
|
||||
const item = window.cantoneirasData.find(c => c.id === id);
|
||||
if (!item) {
|
||||
alert('❌ Cantoneira não encontrada');
|
||||
return;
|
||||
}
|
||||
|
||||
alert(`
|
||||
📐 ${item.nome}
|
||||
|
||||
Lado: ${item.lado_mm} mm
|
||||
Espessura: ${item.espessura_mm} mm
|
||||
Peso: ${item.peso_kg_m.toFixed(2)} kg/m
|
||||
Área: ${item.area_cm2.toFixed(2)} cm²
|
||||
Momento de Inércia: ${item.momento_inercia_cm4.toFixed(2)} cm⁴
|
||||
Raio de Giração: ${item.raio_giracao_cm.toFixed(2)} cm
|
||||
Tipo: ${item.tipo}
|
||||
`);
|
||||
}
|
||||
|
||||
// Adicionar CSS para spinner
|
||||
if (!document.getElementById('spinner-styles')) {
|
||||
const style = document.createElement('style');
|
||||
style.id = 'spinner-styles';
|
||||
style.textContent = `
|
||||
.spinner {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border: 2px solid var(--color-border);
|
||||
border-top: 2px solid var(--color-primary);
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
`;
|
||||
document.head.appendChild(style);
|
||||
}
|
||||
|
||||
console.log('✅ Perfis Loader V2 carregado com Data Manager');
|
||||
124
js/main.js
Normal file
124
js/main.js
Normal file
@@ -0,0 +1,124 @@
|
||||
/**
|
||||
* Main Entry Point - ES6 Modules
|
||||
* Initializes the application with modular architecture
|
||||
*/
|
||||
|
||||
// Core imports
|
||||
import { loadPreferences } from './core/storage.js';
|
||||
import { applyUserPreferences } from './ui/theme.js';
|
||||
import { showSection } from './ui/navigation.js';
|
||||
|
||||
// UI imports
|
||||
import './ui/mobile-menu.js';
|
||||
import './ui/csv-manager-ui.js';
|
||||
|
||||
/**
|
||||
* Initialize application
|
||||
*/
|
||||
async function initializeApp() {
|
||||
console.log('🚀 SteelBase v7.5 - Inicializando (Modular)...');
|
||||
|
||||
try {
|
||||
// 1. Load user preferences
|
||||
loadPreferences();
|
||||
|
||||
// 2. Apply preferences (theme, colors, fonts)
|
||||
applyUserPreferences();
|
||||
|
||||
// 3. Load initial section
|
||||
await showSection('cev');
|
||||
|
||||
// 4. Initialize optional functions safely
|
||||
initializeOptionalFeatures();
|
||||
|
||||
// 5. Apply admin config
|
||||
if (typeof window.applyAdminConfig === 'function') {
|
||||
window.applyAdminConfig();
|
||||
}
|
||||
|
||||
// 6. Filter tools by mode
|
||||
if (typeof window.filterToolsByMode === 'function') {
|
||||
window.filterToolsByMode();
|
||||
}
|
||||
|
||||
console.log('✅ Aplicativo inicializado com sucesso (Modular)!');
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ Erro na inicialização:', error);
|
||||
} finally {
|
||||
// Remove loading screen
|
||||
removeLoadingScreen();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize optional features that may not exist
|
||||
*/
|
||||
function initializeOptionalFeatures() {
|
||||
const optionalFunctions = [
|
||||
'mostrarEquivalencias',
|
||||
'gerarChecklistCertificado',
|
||||
'updatePaintFields',
|
||||
'updateWeightFields'
|
||||
];
|
||||
|
||||
optionalFunctions.forEach(funcName => {
|
||||
if (typeof window[funcName] === 'function') {
|
||||
try {
|
||||
window[funcName]();
|
||||
} catch (error) {
|
||||
console.warn(`⚠️ Erro ao inicializar ${funcName}:`, error);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove loading screen with animation
|
||||
*/
|
||||
function removeLoadingScreen() {
|
||||
setTimeout(() => {
|
||||
const loadingScreen = document.getElementById('app-loading');
|
||||
if (loadingScreen) {
|
||||
loadingScreen.style.opacity = '0';
|
||||
loadingScreen.style.transition = 'opacity 0.3s ease';
|
||||
setTimeout(() => loadingScreen.remove(), 300);
|
||||
}
|
||||
}, 500);
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup event listeners
|
||||
*/
|
||||
function setupEventListeners() {
|
||||
// Budget observer
|
||||
const observer = new MutationObserver(() => {
|
||||
if (window.appState && window.appState.currentSection === 'orcamento') {
|
||||
if (typeof window.initializeBudget === 'function') {
|
||||
setTimeout(window.initializeBudget, 100);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Removido: adminModal legado
|
||||
}
|
||||
|
||||
// Initialize when DOM is ready
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
initializeApp();
|
||||
setupEventListeners();
|
||||
});
|
||||
} else {
|
||||
initializeApp();
|
||||
setupEventListeners();
|
||||
}
|
||||
|
||||
// Export for debugging
|
||||
if (typeof window !== 'undefined') {
|
||||
window.__appModules = {
|
||||
version: '7.5.0',
|
||||
modular: true,
|
||||
initialized: true
|
||||
};
|
||||
}
|
||||
349
js/sections/perfis-auto-loader.js
Normal file
349
js/sections/perfis-auto-loader.js
Normal file
@@ -0,0 +1,349 @@
|
||||
/**
|
||||
* CARREGADOR AUTOMÁTICO PARA TODOS OS PERFIS
|
||||
* Gera automaticamente as funções de carregamento para cada tipo de perfil
|
||||
*/
|
||||
|
||||
/**
|
||||
* Gera função de carregamento forçado para um tipo de perfil
|
||||
*/
|
||||
function gerarFuncaoCarregamento(tipo) {
|
||||
const config = window.PERFIS_CONFIG[tipo];
|
||||
if (!config) return null;
|
||||
|
||||
const tipoId = tipo.replace(/-/g, '_');
|
||||
const funcName = `forcarCarregamento${capitalize(tipoId)}`;
|
||||
|
||||
window[funcName] = async function() {
|
||||
console.log(`🚨 CARREGAMENTO FORÇADO INICIADO: ${config.nome}`);
|
||||
|
||||
const tbody = document.getElementById(`${tipoId}-tbody`);
|
||||
if (!tbody) {
|
||||
alert(`❌ Erro: Elemento da tabela não encontrado!`);
|
||||
console.error(`❌ Elemento ${tipoId}-tbody não encontrado`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Mostrar loading
|
||||
tbody.innerHTML = `
|
||||
<tr>
|
||||
<td colspan="${config.colunasTabel.length + 1}" style="text-align: center; padding: 40px;">
|
||||
<div style="font-size: 48px; margin-bottom: 16px;">⏳</div>
|
||||
<div style="font-size: 18px; font-weight: bold;">Carregando dados...</div>
|
||||
<div style="font-size: 14px; color: #666; margin-top: 8px;">Aguarde alguns segundos</div>
|
||||
</td>
|
||||
</tr>
|
||||
`;
|
||||
|
||||
try {
|
||||
// Carregar CSV
|
||||
console.log(`📥 Carregando CSV: ${config.csv}`);
|
||||
const response = await fetch(config.csv);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
||||
}
|
||||
|
||||
const csvText = await response.text();
|
||||
const linhas = csvText.trim().split('\n');
|
||||
|
||||
console.log(`📊 CSV carregado: ${linhas.length} linhas`);
|
||||
|
||||
// Processar dados
|
||||
const dados = [];
|
||||
for (let i = 1; i < linhas.length; i++) {
|
||||
const linha = linhas[i].trim();
|
||||
if (!linha) continue;
|
||||
|
||||
const colunas = linha.split(',');
|
||||
if (colunas.length >= config.colunas.length) {
|
||||
const item = {};
|
||||
config.colunas.forEach((col, index) => {
|
||||
let value = colunas[index].trim();
|
||||
// Converter números
|
||||
if (col.includes('_mm') || col.includes('_kg') || col.includes('_cm') || col.includes('_m2')) {
|
||||
value = parseFloat(value) || 0;
|
||||
}
|
||||
item[col] = value;
|
||||
});
|
||||
dados.push(item);
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`✅ Processados: ${dados.length} itens`);
|
||||
|
||||
if (dados.length === 0) {
|
||||
throw new Error('Nenhum dado encontrado no CSV');
|
||||
}
|
||||
|
||||
// Exibir na tabela
|
||||
tbody.innerHTML = dados.map(item => `
|
||||
<tr>
|
||||
${config.colunasTabel.map(col => {
|
||||
let value = item[col.key];
|
||||
if (col.decimals && typeof value === 'number') {
|
||||
value = value.toFixed(col.decimals);
|
||||
}
|
||||
if (col.badge) {
|
||||
return `<td><span class="badge badge-info">${value}</span></td>`;
|
||||
}
|
||||
return `<td>${col.key === 'nome' ? '<strong>' + value + '</strong>' : value}</td>`;
|
||||
}).join('')}
|
||||
<td><button class="btn btn-sm btn-primary">👁️ Ver</button></td>
|
||||
</tr>
|
||||
`).join('');
|
||||
|
||||
// Atualizar contador
|
||||
const totalEl = document.getElementById(`${tipoId}-total`);
|
||||
if (totalEl) {
|
||||
totalEl.textContent = dados.length;
|
||||
}
|
||||
|
||||
// Armazenar globalmente
|
||||
window[`${tipoId}Data`] = dados;
|
||||
|
||||
console.log(`🎉 CARREGAMENTO CONCLUÍDO: ${config.nome}`);
|
||||
|
||||
// Notificar usuário
|
||||
alert(`✅ ${dados.length} ${config.nome.toLowerCase()} carregados com sucesso!`);
|
||||
|
||||
} catch (error) {
|
||||
console.error(`❌ Erro no carregamento: ${error.message}`);
|
||||
|
||||
tbody.innerHTML = `
|
||||
<tr>
|
||||
<td colspan="${config.colunasTabel.length + 1}" style="text-align: center; padding: 40px;">
|
||||
<div style="font-size: 48px; margin-bottom: 16px;">❌</div>
|
||||
<div style="font-size: 18px; font-weight: bold; color: #ef4444;">Erro ao carregar dados</div>
|
||||
<div style="font-size: 14px; color: #666; margin-top: 8px;">${error.message}</div>
|
||||
<button class="btn btn-primary" onclick="${funcName}()" style="margin-top: 16px;">
|
||||
🔄 Tentar Novamente
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
`;
|
||||
|
||||
alert(`❌ Erro ao carregar dados: ${error.message}`);
|
||||
}
|
||||
};
|
||||
|
||||
console.log(`✅ Função criada: ${funcName}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gera funções de filtro para um tipo de perfil
|
||||
*/
|
||||
function gerarFuncoesFiltro(tipo) {
|
||||
const config = window.PERFIS_CONFIG[tipo];
|
||||
if (!config) return;
|
||||
|
||||
const tipoId = tipo.replace(/-/g, '_');
|
||||
|
||||
// Função de filtrar
|
||||
window[`filtrar${capitalize(tipoId)}`] = function() {
|
||||
const dados = window[`${tipoId}Data`];
|
||||
if (!dados) {
|
||||
console.warn('⚠️ Dados não carregados ainda');
|
||||
return;
|
||||
}
|
||||
|
||||
// Implementar filtros básicos
|
||||
console.log(`🔍 Filtrando ${config.nome}...`);
|
||||
// TODO: Implementar lógica de filtro
|
||||
};
|
||||
|
||||
// Função de limpar filtros
|
||||
window[`limparFiltros${capitalize(tipoId)}`] = function() {
|
||||
console.log(`🧹 Limpando filtros de ${config.nome}`);
|
||||
|
||||
// Limpar campos
|
||||
config.filtros.forEach(filtro => {
|
||||
const el = document.getElementById(`${tipoId}-${filtro.id}`);
|
||||
if (el) el.value = '';
|
||||
});
|
||||
|
||||
// Reexibir todos os dados
|
||||
const dados = window[`${tipoId}Data`];
|
||||
if (dados) {
|
||||
const tbody = document.getElementById(`${tipoId}-tbody`);
|
||||
if (tbody) {
|
||||
tbody.innerHTML = dados.map(item => `
|
||||
<tr>
|
||||
${config.colunasTabel.map(col => {
|
||||
let value = item[col.key];
|
||||
if (col.decimals && typeof value === 'number') {
|
||||
value = value.toFixed(col.decimals);
|
||||
}
|
||||
if (col.badge) {
|
||||
return `<td><span class="badge badge-info">${value}</span></td>`;
|
||||
}
|
||||
return `<td>${col.key === 'nome' ? '<strong>' + value + '</strong>' : value}</td>`;
|
||||
}).join('')}
|
||||
<td><button class="btn btn-sm btn-primary">👁️ Ver</button></td>
|
||||
</tr>
|
||||
`).join('');
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Gera função de conteúdo para um tipo de perfil
|
||||
*/
|
||||
function gerarFuncaoConteudo(tipo) {
|
||||
const config = window.PERFIS_CONFIG[tipo];
|
||||
if (!config) return;
|
||||
|
||||
const funcName = `get${capitalize(tipo.replace(/-/g, '_'))}Content`;
|
||||
|
||||
window[funcName] = function() {
|
||||
console.log(`🔧 ${funcName}() chamada`);
|
||||
return gerarConteudoPerfil(tipo);
|
||||
};
|
||||
|
||||
console.log(`✅ Função criada: ${funcName}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Inicializa todas as funções para todos os perfis
|
||||
*/
|
||||
function inicializarTodosPerfis() {
|
||||
console.log('🚀 Inicializando funções para todos os perfis...');
|
||||
|
||||
Object.keys(window.PERFIS_CONFIG).forEach(tipo => {
|
||||
gerarFuncaoConteudo(tipo);
|
||||
gerarFuncaoCarregamento(tipo);
|
||||
gerarFuncoesFiltro(tipo);
|
||||
gerarFuncaoAtualizarFonte(tipo);
|
||||
});
|
||||
|
||||
console.log('✅ Todas as funções de perfis inicializadas!');
|
||||
}
|
||||
|
||||
/**
|
||||
* Capitaliza primeira letra
|
||||
*/
|
||||
function capitalize(str) {
|
||||
return str.split('_').map(word =>
|
||||
word.charAt(0).toUpperCase() + word.slice(1)
|
||||
).join('');
|
||||
}
|
||||
|
||||
// Inicializar quando o script carregar
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', inicializarTodosPerfis);
|
||||
} else {
|
||||
inicializarTodosPerfis();
|
||||
}
|
||||
|
||||
console.log('✅ Auto-loader de perfis carregado');
|
||||
|
||||
// ========================================
|
||||
// Atualização direta a partir da fonte (CSV)
|
||||
// ========================================
|
||||
|
||||
/**
|
||||
* Mapeia o ID do template (com hífen) para a chave do DataManager (com underscore)
|
||||
*/
|
||||
function mapToDataManagerKey(tipo) {
|
||||
return tipo.replace(/-/g, '_');
|
||||
}
|
||||
|
||||
/**
|
||||
* Gera função de atualização de dados a partir da fonte oficial (DataManager)
|
||||
* Cria window["atualizarFonte<Cap>"] para cada tipo. O botão existe apenas em Tubos RHS.
|
||||
*/
|
||||
function gerarFuncaoAtualizarFonte(tipo) {
|
||||
const config = window.PERFIS_CONFIG[tipo];
|
||||
if (!config) return;
|
||||
const tipoId = tipo.replace(/-/g, '_');
|
||||
const funcName = `atualizarFonte${capitalize(tipoId)}`;
|
||||
const dmKey = mapToDataManagerKey(tipo);
|
||||
|
||||
window[funcName] = async function() {
|
||||
try {
|
||||
const tbody = document.getElementById(`${tipoId}-tbody`);
|
||||
const tableContainer = document.getElementById(`${tipoId}-table-container`);
|
||||
if (!tbody || !tableContainer) {
|
||||
alert('❌ Elementos da tabela não encontrados.');
|
||||
return;
|
||||
}
|
||||
|
||||
// Loader visual
|
||||
tbody.innerHTML = `
|
||||
<tr>
|
||||
<td colspan="${config.colunasTabel.length + 1}" style="text-align:center; padding:32px;">
|
||||
<div style="font-size:42px;">⏳</div>
|
||||
<div style="margin-top:8px; color: var(--color-text-secondary);">Atualizando da fonte: ${config.csv}</div>
|
||||
</td>
|
||||
</tr>
|
||||
`;
|
||||
|
||||
// Atualizar via DataManager para registrar metadados e cache
|
||||
const result = await window.dataManager.updateTypeData(dmKey);
|
||||
const dados = result.data || [];
|
||||
|
||||
if (!dados.length) {
|
||||
throw new Error('Fonte retornou 0 itens');
|
||||
}
|
||||
|
||||
// Preencher tabela com os novos dados
|
||||
tbody.innerHTML = dados.map(item => `
|
||||
<tr>
|
||||
${config.colunasTabel.map(col => {
|
||||
let value = item[col.key];
|
||||
if (col.decimals && typeof value === 'number') {
|
||||
value = value.toFixed(col.decimals);
|
||||
}
|
||||
if (col.badge) {
|
||||
return `<td><span class="badge badge-info">${value}</span></td>`;
|
||||
}
|
||||
return `<td>${col.key === 'nome' ? '<strong>' + value + '</strong>' : value}</td>`;
|
||||
}).join('')}
|
||||
<td><button class="btn btn-sm btn-primary">👁️ Ver</button></td>
|
||||
</tr>
|
||||
`).join('');
|
||||
|
||||
// Atualizar contadores e metadados visuais
|
||||
const totalEl = document.getElementById(`${tipoId}-total`);
|
||||
const fonteEl = document.getElementById(`${tipoId}-fonte`);
|
||||
const lastEl = document.getElementById(`${tipoId}-last`);
|
||||
if (totalEl) totalEl.textContent = dados.length;
|
||||
if (fonteEl) fonteEl.textContent = result.source || config.csv;
|
||||
if (lastEl) lastEl.textContent = new Date(result.lastUpdate || Date.now()).toLocaleString('pt-BR');
|
||||
|
||||
// Atualizar descrição do cabeçalho (quantidade)
|
||||
const headerDesc = document.querySelector('.section-description');
|
||||
if (headerDesc && headerDesc.textContent.includes(config.descricao)) {
|
||||
headerDesc.textContent = `${config.descricao} - ${dados.length} modelos disponíveis`;
|
||||
}
|
||||
|
||||
// Atualizar notas nas outras abas (preços, especificações, fabricantes, aplicações)
|
||||
['perfil-tab-1', 'perfil-tab-2', 'perfil-tab-3', 'perfil-tab-4'].forEach(tabId => {
|
||||
const tab = document.getElementById(tabId);
|
||||
if (tab) {
|
||||
const noteId = `${tipoId}-update-note-${tabId}`;
|
||||
let note = document.getElementById(noteId);
|
||||
if (!note) {
|
||||
note = document.createElement('div');
|
||||
note.id = noteId;
|
||||
note.style.marginTop = '12px';
|
||||
note.style.fontSize = '12px';
|
||||
note.style.color = 'var(--color-text-secondary)';
|
||||
tab.appendChild(note);
|
||||
}
|
||||
note.textContent = `Dados atualizados (${dados.length} itens) • Fonte: ${result.source} • ${new Date(result.lastUpdate).toLocaleString('pt-BR')}`;
|
||||
}
|
||||
});
|
||||
|
||||
// Disponibilizar globalmente
|
||||
window[`${tipoId}Data`] = dados;
|
||||
|
||||
alert(`✅ ${config.nome} atualizado da fonte com ${dados.length} itens.`);
|
||||
} catch (error) {
|
||||
console.error(`❌ Erro ao atualizar da fonte (${tipo}):`, error);
|
||||
alert('❌ Erro ao atualizar da fonte: ' + error.message);
|
||||
}
|
||||
};
|
||||
|
||||
console.log(`✅ Função criada: ${funcName}`);
|
||||
}
|
||||
3653
js/sections/perfis-catalog.js
Normal file
3653
js/sections/perfis-catalog.js
Normal file
File diff suppressed because it is too large
Load Diff
417
js/sections/perfis-templates.js
Normal file
417
js/sections/perfis-templates.js
Normal file
@@ -0,0 +1,417 @@
|
||||
/**
|
||||
* TEMPLATES PARA CATÁLOGO DE PERFIS
|
||||
* Sistema automatizado para gerar conteúdo de todos os perfis
|
||||
*/
|
||||
|
||||
// Configuração de cada tipo de perfil
|
||||
const PERFIS_CONFIG = {
|
||||
'barras-redondas': {
|
||||
nome: 'Barras Redondas',
|
||||
icone: '⚫',
|
||||
descricao: 'Barras de aço redondas laminadas a quente',
|
||||
quantidade: 25,
|
||||
csv: 'BD/perfis/barras_brasil_completo.csv',
|
||||
colunas: ['id', 'nome', 'diametro_mm', 'peso_kg_m', 'area_cm2', 'tipo'],
|
||||
colunasTabel: [
|
||||
{ key: 'nome', label: 'Designação' },
|
||||
{ key: 'diametro_mm', label: 'Diâmetro (mm)' },
|
||||
{ key: 'peso_kg_m', label: 'Peso (kg/m)', decimals: 2 },
|
||||
{ key: 'area_cm2', label: 'Área (cm²)', decimals: 2 },
|
||||
{ key: 'tipo', label: 'Categoria', badge: true }
|
||||
],
|
||||
filtros: [
|
||||
{ id: 'tamanho', label: 'Tamanho', type: 'select', options: [
|
||||
{ value: '', label: 'Todos' },
|
||||
{ value: 'Pequeno', label: 'Pequeno (Ø6-Ø16)' },
|
||||
{ value: 'Médio', label: 'Médio (Ø20-Ø32)' },
|
||||
{ value: 'Grande', label: 'Grande (Ø38-Ø50)' },
|
||||
{ value: 'Extra-Grande', label: 'Extra-Grande (Ø63+)' }
|
||||
]},
|
||||
{ id: 'peso-max', label: 'Peso Máximo (kg/m)', type: 'number', placeholder: 'Ex: 20' },
|
||||
{ id: 'busca', label: 'Buscar por Nome', type: 'text', placeholder: 'Ex: Ø25' }
|
||||
]
|
||||
},
|
||||
|
||||
'tubos-circulares': {
|
||||
nome: 'Tubos Circulares',
|
||||
icone: '⭕',
|
||||
descricao: 'Tubos de aço circulares sem costura e com costura',
|
||||
quantidade: 30,
|
||||
csv: 'BD/perfis/tubos_circulares_brasil_completo.csv',
|
||||
colunas: ['id', 'nome', 'diametro_externo_mm', 'espessura_mm', 'peso_kg_m', 'area_cm2', 'tipo'],
|
||||
colunasTabel: [
|
||||
{ key: 'nome', label: 'Designação' },
|
||||
{ key: 'diametro_externo_mm', label: 'Ø Externo (mm)' },
|
||||
{ key: 'espessura_mm', label: 'Espessura (mm)' },
|
||||
{ key: 'peso_kg_m', label: 'Peso (kg/m)', decimals: 2 },
|
||||
{ key: 'area_cm2', label: 'Área (cm²)', decimals: 2 },
|
||||
{ key: 'tipo', label: 'Categoria', badge: true }
|
||||
],
|
||||
filtros: [
|
||||
{ id: 'tamanho', label: 'Diâmetro', type: 'select', options: [
|
||||
{ value: '', label: 'Todos' },
|
||||
{ value: 'Pequeno', label: 'Pequeno (Ø21-Ø60)' },
|
||||
{ value: 'Médio', label: 'Médio (Ø73-Ø114)' },
|
||||
{ value: 'Grande', label: 'Grande (Ø141-Ø219)' },
|
||||
{ value: 'Extra-Grande', label: 'Extra-Grande (Ø273+)' }
|
||||
]},
|
||||
{ id: 'peso-max', label: 'Peso Máximo (kg/m)', type: 'number', placeholder: 'Ex: 50' },
|
||||
{ id: 'busca', label: 'Buscar por Nome', type: 'text', placeholder: 'Ex: Ø114' }
|
||||
]
|
||||
},
|
||||
|
||||
'perfis-i': {
|
||||
nome: 'Perfis I (IPE)',
|
||||
icone: '🏗️',
|
||||
descricao: 'Perfis I laminados a quente - Série IPE',
|
||||
quantidade: 20,
|
||||
csv: 'BD/perfis/perfis_i_brasil_completo.csv',
|
||||
colunas: ['id', 'nome', 'altura_mm', 'largura_mm', 'espessura_alma_mm', 'espessura_mesa_mm', 'peso_kg_m', 'area_cm2', 'tipo'],
|
||||
colunasTabel: [
|
||||
{ key: 'nome', label: 'Designação' },
|
||||
{ key: 'altura_mm', label: 'Altura (mm)' },
|
||||
{ key: 'largura_mm', label: 'Largura (mm)' },
|
||||
{ key: 'peso_kg_m', label: 'Peso (kg/m)', decimals: 2 },
|
||||
{ key: 'area_cm2', label: 'Área (cm²)', decimals: 2 },
|
||||
{ key: 'tipo', label: 'Categoria', badge: true }
|
||||
],
|
||||
filtros: [
|
||||
{ id: 'tamanho', label: 'Tamanho', type: 'select', options: [
|
||||
{ value: '', label: 'Todos' },
|
||||
{ value: 'Pequeno', label: 'Pequeno (IPE80-IPE160)' },
|
||||
{ value: 'Médio', label: 'Médio (IPE180-IPE270)' },
|
||||
{ value: 'Grande', label: 'Grande (IPE300-IPE400)' },
|
||||
{ value: 'Extra-Grande', label: 'Extra-Grande (IPE450+)' }
|
||||
]},
|
||||
{ id: 'peso-max', label: 'Peso Máximo (kg/m)', type: 'number', placeholder: 'Ex: 100' },
|
||||
{ id: 'busca', label: 'Buscar por Nome', type: 'text', placeholder: 'Ex: IPE200' }
|
||||
]
|
||||
},
|
||||
|
||||
'perfis-w': {
|
||||
nome: 'Perfis W',
|
||||
icone: '🏛️',
|
||||
descricao: 'Perfis W laminados a quente - Série americana',
|
||||
quantidade: 25,
|
||||
csv: 'BD/perfis/perfis_w_brasil_completo.csv',
|
||||
colunas: ['id', 'nome', 'altura_mm', 'largura_mm', 'espessura_alma_mm', 'espessura_mesa_mm', 'peso_kg_m', 'area_cm2', 'tipo'],
|
||||
colunasTabel: [
|
||||
{ key: 'nome', label: 'Designação' },
|
||||
{ key: 'altura_mm', label: 'Altura (mm)' },
|
||||
{ key: 'largura_mm', label: 'Largura (mm)' },
|
||||
{ key: 'peso_kg_m', label: 'Peso (kg/m)', decimals: 2 },
|
||||
{ key: 'area_cm2', label: 'Área (cm²)', decimals: 2 },
|
||||
{ key: 'tipo', label: 'Categoria', badge: true }
|
||||
],
|
||||
filtros: [
|
||||
{ id: 'tamanho', label: 'Tamanho', type: 'select', options: [
|
||||
{ value: '', label: 'Todos' },
|
||||
{ value: 'Pequeno', label: 'Pequeno (W150-W250)' },
|
||||
{ value: 'Médio', label: 'Médio (W310-W410)' },
|
||||
{ value: 'Grande', label: 'Grande (W460-W610)' },
|
||||
{ value: 'Extra-Grande', label: 'Extra-Grande (W690+)' }
|
||||
]},
|
||||
{ id: 'peso-max', label: 'Peso Máximo (kg/m)', type: 'number', placeholder: 'Ex: 150' },
|
||||
{ id: 'busca', label: 'Buscar por Nome', type: 'text', placeholder: 'Ex: W310' }
|
||||
]
|
||||
},
|
||||
|
||||
'tubos-rhs': {
|
||||
nome: 'Tubos RHS',
|
||||
icone: '▭',
|
||||
descricao: 'Tubos retangulares e quadrados estruturais',
|
||||
quantidade: 35,
|
||||
csv: 'BD/perfis/tubos_rhs_brasil_completo.csv',
|
||||
colunas: ['id', 'nome', 'largura_mm', 'altura_mm', 'espessura_mm', 'peso_kg_m', 'area_cm2', 'tipo'],
|
||||
colunasTabel: [
|
||||
{ key: 'nome', label: 'Designação' },
|
||||
{ key: 'largura_mm', label: 'Largura (mm)' },
|
||||
{ key: 'altura_mm', label: 'Altura (mm)' },
|
||||
{ key: 'espessura_mm', label: 'Espessura (mm)' },
|
||||
{ key: 'peso_kg_m', label: 'Peso (kg/m)', decimals: 2 },
|
||||
{ key: 'area_cm2', label: 'Área (cm²)', decimals: 2 },
|
||||
{ key: 'tipo', label: 'Categoria', badge: true }
|
||||
],
|
||||
filtros: [
|
||||
{ id: 'tamanho', label: 'Tamanho', type: 'select', options: [
|
||||
{ value: '', label: 'Todos' },
|
||||
{ value: 'Pequeno', label: 'Pequeno (até 50x50)' },
|
||||
{ value: 'Médio', label: 'Médio (60x40 a 100x100)' },
|
||||
{ value: 'Grande', label: 'Grande (120x80 a 200x100)' },
|
||||
{ value: 'Extra-Grande', label: 'Extra-Grande (200x200+)' }
|
||||
]},
|
||||
{ id: 'peso-max', label: 'Peso Máximo (kg/m)', type: 'number', placeholder: 'Ex: 80' },
|
||||
{ id: 'busca', label: 'Buscar por Nome', type: 'text', placeholder: 'Ex: 100x100' }
|
||||
]
|
||||
},
|
||||
|
||||
'chapas': {
|
||||
nome: 'Chapas',
|
||||
icone: '📄',
|
||||
descricao: 'Chapas de aço laminadas a quente e a frio',
|
||||
quantidade: 15,
|
||||
csv: 'BD/perfis/chapas_brasil_completo.csv',
|
||||
colunas: ['id', 'nome', 'espessura_mm', 'peso_kg_m2', 'tipo'],
|
||||
colunasTabel: [
|
||||
{ key: 'nome', label: 'Designação' },
|
||||
{ key: 'espessura_mm', label: 'Espessura (mm)' },
|
||||
{ key: 'peso_kg_m2', label: 'Peso (kg/m²)', decimals: 2 },
|
||||
{ key: 'tipo', label: 'Categoria', badge: true }
|
||||
],
|
||||
filtros: [
|
||||
{ id: 'tamanho', label: 'Espessura', type: 'select', options: [
|
||||
{ value: '', label: 'Todas' },
|
||||
{ value: 'Fina', label: 'Fina (até 6mm)' },
|
||||
{ value: 'Média', label: 'Média (8-16mm)' },
|
||||
{ value: 'Grossa', label: 'Grossa (19-32mm)' },
|
||||
{ value: 'Extra-Grossa', label: 'Extra-Grossa (38mm+)' }
|
||||
]},
|
||||
{ id: 'peso-max', label: 'Peso Máximo (kg/m²)', type: 'number', placeholder: 'Ex: 100' },
|
||||
{ id: 'busca', label: 'Buscar por Nome', type: 'text', placeholder: 'Ex: 12.5mm' }
|
||||
]
|
||||
},
|
||||
|
||||
'perfis-hp': {
|
||||
nome: 'Perfis HP',
|
||||
icone: '🏗️',
|
||||
descricao: 'Perfis HP para estacas e pilares',
|
||||
quantidade: 12,
|
||||
csv: 'BD/perfis/perfis_hp_brasil_completo.csv',
|
||||
colunas: ['id', 'nome', 'altura_mm', 'largura_mm', 'espessura_alma_mm', 'espessura_mesa_mm', 'peso_kg_m', 'area_cm2', 'tipo'],
|
||||
colunasTabel: [
|
||||
{ key: 'nome', label: 'Designação' },
|
||||
{ key: 'altura_mm', label: 'Altura (mm)' },
|
||||
{ key: 'largura_mm', label: 'Largura (mm)' },
|
||||
{ key: 'peso_kg_m', label: 'Peso (kg/m)', decimals: 2 },
|
||||
{ key: 'area_cm2', label: 'Área (cm²)', decimals: 2 },
|
||||
{ key: 'tipo', label: 'Categoria', badge: true }
|
||||
],
|
||||
filtros: [
|
||||
{ id: 'tamanho', label: 'Tamanho', type: 'select', options: [
|
||||
{ value: '', label: 'Todos' },
|
||||
{ value: 'Pequeno', label: 'Pequeno (HP200-HP250)' },
|
||||
{ value: 'Médio', label: 'Médio (HP310-HP360)' },
|
||||
{ value: 'Grande', label: 'Grande (HP400+)' }
|
||||
]},
|
||||
{ id: 'peso-max', label: 'Peso Máximo (kg/m)', type: 'number', placeholder: 'Ex: 150' },
|
||||
{ id: 'busca', label: 'Buscar por Nome', type: 'text', placeholder: 'Ex: HP310' }
|
||||
]
|
||||
},
|
||||
|
||||
'barras-roscadas': {
|
||||
nome: 'Barras Roscadas',
|
||||
icone: '🔩',
|
||||
descricao: 'Barras roscadas de aço para fixação',
|
||||
quantidade: 18,
|
||||
csv: 'BD/perfis/barras_roscadas_brasil_completo.csv',
|
||||
colunas: ['id', 'nome', 'diametro_mm', 'passo_mm', 'peso_kg_m', 'area_cm2', 'tipo'],
|
||||
colunasTabel: [
|
||||
{ key: 'nome', label: 'Designação' },
|
||||
{ key: 'diametro_mm', label: 'Diâmetro (mm)' },
|
||||
{ key: 'passo_mm', label: 'Passo (mm)' },
|
||||
{ key: 'peso_kg_m', label: 'Peso (kg/m)', decimals: 2 },
|
||||
{ key: 'area_cm2', label: 'Área (cm²)', decimals: 2 },
|
||||
{ key: 'tipo', label: 'Categoria', badge: true }
|
||||
],
|
||||
filtros: [
|
||||
{ id: 'tamanho', label: 'Diâmetro', type: 'select', options: [
|
||||
{ value: '', label: 'Todos' },
|
||||
{ value: 'Pequeno', label: 'Pequeno (M6-M12)' },
|
||||
{ value: 'Médio', label: 'Médio (M16-M24)' },
|
||||
{ value: 'Grande', label: 'Grande (M30-M36)' },
|
||||
{ value: 'Extra-Grande', label: 'Extra-Grande (M42+)' }
|
||||
]},
|
||||
{ id: 'peso-max', label: 'Peso Máximo (kg/m)', type: 'number', placeholder: 'Ex: 10' },
|
||||
{ id: 'busca', label: 'Buscar por Nome', type: 'text', placeholder: 'Ex: M20' }
|
||||
]
|
||||
},
|
||||
|
||||
'barras-chatas': {
|
||||
nome: 'Barras Chatas',
|
||||
icone: '▬',
|
||||
descricao: 'Barras chatas de aço laminadas a quente',
|
||||
quantidade: 22,
|
||||
csv: 'BD/perfis/barras_chatas_brasil_completo.csv',
|
||||
colunas: ['id', 'nome', 'largura_mm', 'espessura_mm', 'peso_kg_m', 'area_cm2', 'tipo'],
|
||||
colunasTabel: [
|
||||
{ key: 'nome', label: 'Designação' },
|
||||
{ key: 'largura_mm', label: 'Largura (mm)' },
|
||||
{ key: 'espessura_mm', label: 'Espessura (mm)' },
|
||||
{ key: 'peso_kg_m', label: 'Peso (kg/m)', decimals: 2 },
|
||||
{ key: 'area_cm2', label: 'Área (cm²)', decimals: 2 },
|
||||
{ key: 'tipo', label: 'Categoria', badge: true }
|
||||
],
|
||||
filtros: [
|
||||
{ id: 'tamanho', label: 'Tamanho', type: 'select', options: [
|
||||
{ value: '', label: 'Todas' },
|
||||
{ value: 'Pequena', label: 'Pequena (até 25mm)' },
|
||||
{ value: 'Média', label: 'Média (32-50mm)' },
|
||||
{ value: 'Grande', label: 'Grande (63-100mm)' },
|
||||
{ value: 'Extra-Grande', label: 'Extra-Grande (125mm+)' }
|
||||
]},
|
||||
{ id: 'peso-max', label: 'Peso Máximo (kg/m)', type: 'number', placeholder: 'Ex: 30' },
|
||||
{ id: 'busca', label: 'Buscar por Nome', type: 'text', placeholder: 'Ex: 50x6' }
|
||||
]
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Gera o HTML completo para um tipo de perfil
|
||||
*/
|
||||
function gerarConteudoPerfil(tipo) {
|
||||
const config = PERFIS_CONFIG[tipo];
|
||||
if (!config) {
|
||||
console.error(`Configuração não encontrada para: ${tipo}`);
|
||||
return '<p>Erro: Tipo de perfil não configurado</p>';
|
||||
}
|
||||
|
||||
const tipoId = tipo.replace(/-/g, '_');
|
||||
|
||||
return `
|
||||
<div class="section-header">
|
||||
<div class="section-title">${config.icone} ${config.nome} - Catálogo Completo Brasil</div>
|
||||
<div class="section-description">${config.descricao} - ${config.quantidade} modelos disponíveis</div>
|
||||
</div>
|
||||
|
||||
<!-- Tabs de Navegação -->
|
||||
<div class="tabs-container">
|
||||
<div class="tabs-nav">
|
||||
<button class="tab-btn active" onclick="switchPerfilTab(0)">📊 Tabela Técnica</button>
|
||||
<button class="tab-btn" onclick="switchPerfilTab(1)">📋 Especificações</button>
|
||||
<button class="tab-btn" onclick="switchPerfilTab(2)">🏭 Fabricantes</button>
|
||||
<button class="tab-btn" onclick="switchPerfilTab(3)">💰 Preços 2025</button>
|
||||
<button class="tab-btn" onclick="switchPerfilTab(4)">🔧 Aplicações</button>
|
||||
</div>
|
||||
|
||||
<!-- TAB 1: TABELA TÉCNICA -->
|
||||
<div class="tab-content active" id="perfil-tab-0">
|
||||
<div class="card">
|
||||
<div class="card-title">🔍 Filtros de Busca</div>
|
||||
<div class="form-grid">
|
||||
${config.filtros.map((filtro, index) => `
|
||||
<div class="form-group">
|
||||
<label class="form-label">${filtro.label}</label>
|
||||
${filtro.type === 'select' ? `
|
||||
<select class="form-control" id="${tipoId}-${filtro.id}" onchange="filtrar${capitalize(tipoId)}()">
|
||||
${filtro.options.map(opt => `<option value="${opt.value}">${opt.label}</option>`).join('')}
|
||||
</select>
|
||||
` : `
|
||||
<input type="${filtro.type}" class="form-control" id="${tipoId}-${filtro.id}"
|
||||
placeholder="${filtro.placeholder}"
|
||||
${filtro.type === 'number' ? 'onchange' : 'oninput'}="filtrar${capitalize(tipoId)}()">
|
||||
`}
|
||||
</div>
|
||||
`).join('')}
|
||||
</div>
|
||||
<button class="btn btn-secondary" onclick="limparFiltros${capitalize(tipoId)}()">🔄 Limpar Filtros</button>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-title">
|
||||
📊 Tabela de ${config.nome} (${config.quantidade} modelos)
|
||||
<button class="btn btn-primary" id="btn-carregar-${tipoId}"
|
||||
onclick="forcarCarregamento${capitalize(tipoId)}()"
|
||||
style="float: right; margin-top: -4px;">
|
||||
🔄 Carregar Dados
|
||||
</button>
|
||||
${tipo === 'tubos-rhs' ? `
|
||||
<button class="btn btn-secondary" id="btn-atualizar-${tipoId}"
|
||||
onclick="atualizarFonte${capitalize(tipoId)}()"
|
||||
style="float: right; margin-right: 8px; margin-top: -4px;">
|
||||
🗄️ Atualizar da Fonte
|
||||
</button>
|
||||
` : ''}
|
||||
</div>
|
||||
<div id="${tipoId}-table-container">
|
||||
<div class="table-container">
|
||||
<table id="${tipoId}-table">
|
||||
<thead>
|
||||
<tr>
|
||||
${config.colunasTabel.map(col => `<th>${col.label}</th>`).join('')}
|
||||
<th>Ações</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="${tipoId}-tbody">
|
||||
<!-- Será preenchido via JavaScript -->
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<div style="margin-top: 16px; padding: 12px; background: var(--color-bg-2); border-radius: 8px;">
|
||||
<strong>📌 Total:</strong> <span id="${tipoId}-total">${config.quantidade}</span> modelos encontrados
|
||||
${tipo === 'tubos-rhs' ? `
|
||||
<div style="margin-top:8px; font-size:12px; color: var(--color-text-secondary);">
|
||||
Fonte: <span id="${tipoId}-fonte">${config.csv}</span> |
|
||||
Última atualização: <span id="${tipoId}-last">—</span>
|
||||
</div>
|
||||
` : ''}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- TAB 2: ESPECIFICAÇÕES -->
|
||||
<div class="tab-content" id="perfil-tab-1">
|
||||
<div class="card" style="background: var(--color-bg-1);">
|
||||
<div class="card-title">📋 Especificações Técnicas Completas</div>
|
||||
<h3 style="color: var(--color-primary); margin-top: 20px;">🔧 Descrição Geral</h3>
|
||||
<p><strong>${config.descricao}</strong></p>
|
||||
<p>Perfis estruturais amplamente utilizados em construção civil, naval e industrial.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- TAB 3: FABRICANTES -->
|
||||
<div class="tab-content" id="perfil-tab-2">
|
||||
<div class="card" style="background: var(--color-bg-1);">
|
||||
<div class="card-title">🏭 Fabricantes e Distribuição no Brasil</div>
|
||||
<p>Principais fabricantes nacionais disponíveis.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- TAB 4: PREÇOS -->
|
||||
<div class="tab-content" id="perfil-tab-3">
|
||||
<div class="card" style="background: var(--color-bg-1);">
|
||||
<div class="card-title">💰 Preços e Fatores de Custo (2025)</div>
|
||||
<p>Informações de preços atualizadas para ${new Date().getFullYear()}.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- TAB 5: APLICAÇÕES -->
|
||||
<div class="tab-content" id="perfil-tab-4">
|
||||
<div class="card" style="background: var(--color-bg-1);">
|
||||
<div class="card-title">🔧 Aplicações Principais e Recomendações</div>
|
||||
<p>Aplicações típicas e recomendações de uso.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Capitaliza primeira letra
|
||||
*/
|
||||
function capitalize(str) {
|
||||
return str.split('_').map(word =>
|
||||
word.charAt(0).toUpperCase() + word.slice(1)
|
||||
).join('');
|
||||
}
|
||||
|
||||
// Exportar configurações e funções
|
||||
window.PERFIS_CONFIG = PERFIS_CONFIG;
|
||||
window.gerarConteudoPerfil = gerarConteudoPerfil;
|
||||
|
||||
console.log('✅ Templates de perfis carregados');
|
||||
|
||||
// Fallback: registrar switchPerfilTab global se não existir
|
||||
if (typeof window.switchPerfilTab !== 'function') {
|
||||
window.switchPerfilTab = function(index) {
|
||||
try {
|
||||
const btns = document.querySelectorAll('.tabs-nav .tab-btn');
|
||||
const tabs = document.querySelectorAll('.tab-content');
|
||||
btns.forEach((btn, i) => btn.classList.toggle('active', i === index));
|
||||
tabs.forEach((tab, i) => tab.classList.toggle('active', i === index));
|
||||
} catch (err) {
|
||||
console.warn('⚠️ Fallback switchPerfilTab falhou:', err);
|
||||
}
|
||||
};
|
||||
console.log('✅ Fallback switchPerfilTab registrado no templates');
|
||||
}
|
||||
69
js/tests/test-backup-log.js
Normal file
69
js/tests/test-backup-log.js
Normal file
@@ -0,0 +1,69 @@
|
||||
/**
|
||||
* Teste completo de Backup e Log
|
||||
* Executar no console: runBackupAndLogTest()
|
||||
*/
|
||||
(function(){
|
||||
async function run(){
|
||||
const results = [];
|
||||
const add = (ok, msg) => { results.push({ ok, msg }); console.log(`${ok ? '✅' : '❌'} ${msg}`); };
|
||||
try {
|
||||
if (!window.backupManager || !window.dataManager) {
|
||||
add(false, 'Managers não encontrados (backupManager/dataManager)');
|
||||
return { results };
|
||||
}
|
||||
|
||||
console.group('🔧 Teste: Backup e Log');
|
||||
|
||||
// Estado inicial
|
||||
const beforeBackups = window.backupManager.getBackups();
|
||||
console.log('Backups existentes:', beforeBackups.length);
|
||||
|
||||
// 1) Criar backup manual
|
||||
const newBackup = await window.backupManager.createBackup('Backup manual - Teste');
|
||||
add(typeof newBackup.timestamp === 'number', 'timestamp numérico no backup criado');
|
||||
add(new Date(newBackup.createdAt).toString() !== 'Invalid Date', 'data válida no backup criado');
|
||||
add(typeof newBackup.sizeBytes === 'number' && newBackup.sizeBytes > 0, 'sizeBytes válido no backup criado');
|
||||
add(typeof newBackup.size === 'string' && /B|KB|MB|GB/.test(newBackup.size), 'size formatado no backup criado');
|
||||
|
||||
// 2) Listagem sem "Invalid Date" ou "undefined"
|
||||
const list = window.backupManager.getBackups();
|
||||
const invalids = list.filter(b => {
|
||||
const d = Number(b.createdAt ?? b.timestamp);
|
||||
return isNaN(d) || new Date(d).toString() === 'Invalid Date' || b.size == null || b.sizeBytes == null;
|
||||
});
|
||||
add(invalids.length === 0, 'lista de backups sem "Invalid Date"/"undefined"');
|
||||
|
||||
// 3) Restauração (escuta evento)
|
||||
let restoredEvent = false;
|
||||
const handler = () => { restoredEvent = true; };
|
||||
document.addEventListener('backupRestored', handler, { once: true });
|
||||
await window.backupManager.restoreBackup(newBackup.id);
|
||||
add(restoredEvent === true, 'evento de restauração disparado');
|
||||
|
||||
// 4) Remoção do backup criado
|
||||
window.backupManager.removeBackup(newBackup.id);
|
||||
const afterRemoval = window.backupManager.getBackups();
|
||||
add(!afterRemoval.find(b => b.id === newBackup.id), 'backup de teste removido com sucesso');
|
||||
|
||||
// 5) Atualização de logs (metadata)
|
||||
const metaBefore = window.dataManager.getMetadata();
|
||||
await window.dataManager.updateAllData();
|
||||
const metaAfter = window.dataManager.getMetadata();
|
||||
add(metaAfter && metaBefore && metaAfter.lastUpdate > metaBefore.lastUpdate, 'log: lastUpdate atualizado');
|
||||
add(metaAfter && (metaAfter.successCount + metaAfter.errorCount === metaAfter.totalTypes), 'log: contagem consistente');
|
||||
|
||||
console.groupEnd();
|
||||
|
||||
return {
|
||||
results,
|
||||
backups: window.backupManager.getBackups(),
|
||||
metadata: window.dataManager.getMetadata()
|
||||
};
|
||||
} catch (err) {
|
||||
add(false, `Erro no teste: ${err.message}`);
|
||||
return { results, error: err };
|
||||
}
|
||||
}
|
||||
|
||||
window.runBackupAndLogTest = run;
|
||||
})();
|
||||
185
js/tests/test-persistencia-simples.js
Normal file
185
js/tests/test-persistencia-simples.js
Normal file
@@ -0,0 +1,185 @@
|
||||
/**
|
||||
* Teste Simples de Persistência - Executável no navegador
|
||||
* Testa funcionalidades básicas do AdminConfigManager
|
||||
*/
|
||||
|
||||
// Função principal de teste
|
||||
async function testarPersistenciaSimples() {
|
||||
console.log('🧪 Iniciando teste simples de persistência...');
|
||||
|
||||
try {
|
||||
// Testar AdminConfigManager
|
||||
if (typeof AdminConfigManager === 'undefined') {
|
||||
throw new Error('AdminConfigManager não está disponível');
|
||||
}
|
||||
|
||||
console.log('✅ AdminConfigManager disponível');
|
||||
|
||||
// Criar instância
|
||||
const manager = new AdminConfigManager();
|
||||
console.log('✅ Instância criada');
|
||||
|
||||
// Testar configuração padrão
|
||||
const config = manager.getConfig();
|
||||
console.log('✅ Configuração padrão obtida:', config);
|
||||
|
||||
// Testar salvamento
|
||||
manager.updateConfig('app.name', 'Aço Calc Pro - Teste');
|
||||
manager.updateConfig('theme.name', 'dark');
|
||||
console.log('✅ Configurações atualizadas');
|
||||
|
||||
// Testar recuperação
|
||||
const configAtualizada = manager.getConfig();
|
||||
console.log('✅ Configuração recuperada:', configAtualizada);
|
||||
|
||||
// Verificar se as mudanças foram aplicadas
|
||||
const nomeAtualizado = configAtualizada.app.name === 'Aço Calc Pro - Teste';
|
||||
const temaAtualizado = configAtualizada.theme.name === 'dark';
|
||||
|
||||
console.log(`✅ Nome atualizado: ${nomeAtualizado}`);
|
||||
console.log(`✅ Tema atualizado: ${temaAtualizado}`);
|
||||
|
||||
// Testar localStorage
|
||||
const localStorageKey = manager.configKey;
|
||||
const localStorageData = localStorage.getItem(localStorageKey);
|
||||
console.log('✅ Dados no localStorage:', localStorageData ? 'Presentes' : 'Ausentes');
|
||||
|
||||
if (localStorageData) {
|
||||
const parsedData = JSON.parse(localStorageData);
|
||||
console.log('✅ Versão dos dados:', parsedData.version);
|
||||
}
|
||||
|
||||
// Testar ToastManager se disponível
|
||||
if (typeof ToastManager !== 'undefined') {
|
||||
window.toastManager = new ToastManager();
|
||||
console.log('✅ ToastManager inicializado');
|
||||
|
||||
if (nomeAtualizado && temaAtualizado) {
|
||||
window.toastManager.success('🧪 Teste de persistência: SUCESSO!');
|
||||
} else {
|
||||
window.toastManager.warning('🧪 Teste de persistência: Parcial');
|
||||
}
|
||||
}
|
||||
|
||||
// Testar BackupManager se disponível
|
||||
if (typeof BackupManager !== 'undefined') {
|
||||
const backupManager = new BackupManager();
|
||||
console.log('✅ BackupManager disponível');
|
||||
|
||||
// Criar backup de teste
|
||||
const backup = await backupManager.createBackup();
|
||||
console.log('✅ Backup criado:', backup.id);
|
||||
|
||||
// Listar backups
|
||||
const backups = backupManager.getBackups();
|
||||
console.log('✅ Backups disponíveis:', backups.length);
|
||||
}
|
||||
|
||||
console.log('🎉 Teste simples concluído com sucesso!');
|
||||
|
||||
// Retornar resultado para uso externo
|
||||
return {
|
||||
success: nomeAtualizado && temaAtualizado,
|
||||
details: {
|
||||
nomeAtualizado,
|
||||
temaAtualizado,
|
||||
localStoragePresente: !!localStorageData
|
||||
}
|
||||
};
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ Erro no teste:', error);
|
||||
|
||||
if (typeof ToastManager !== 'undefined' && window.toastManager) {
|
||||
window.toastManager.error('❌ Erro no teste: ' + error.message);
|
||||
}
|
||||
|
||||
return {
|
||||
success: false,
|
||||
error: error.message
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Função para testar persistência após reload
|
||||
function testarReload() {
|
||||
console.log('🔄 Preparando teste de reload...');
|
||||
|
||||
if (typeof AdminConfigManager !== 'undefined') {
|
||||
const manager = new AdminConfigManager();
|
||||
|
||||
// Salvar configuração de teste
|
||||
manager.updateConfig('app.name', 'Aço Calc Pro - Teste Reload');
|
||||
manager.updateConfig('test.timestamp', new Date().toISOString());
|
||||
|
||||
console.log('✅ Configurações de teste salvas');
|
||||
console.log('🔄 Recarregando em 3 segundos...');
|
||||
|
||||
if (window.toastManager) {
|
||||
window.toastManager.info('🔄 Recarregando página para testar persistência...');
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
location.reload();
|
||||
}, 3000);
|
||||
|
||||
} else {
|
||||
console.log('⚠️ Verificando persistência após reload...');
|
||||
|
||||
// Verificar se as configurações persistiram
|
||||
const manager = new AdminConfigManager();
|
||||
const config = manager.getConfig();
|
||||
|
||||
if (config.app.name === 'Aço Calc Pro - Teste Reload') {
|
||||
console.log('✅ Persistência após reload: SUCESSO!');
|
||||
if (window.toastManager) {
|
||||
window.toastManager.success('✅ Configurações persistiram após reload!');
|
||||
}
|
||||
} else {
|
||||
console.log('❌ Persistência após reload: FALHA');
|
||||
console.log('Config atual:', config.app.name);
|
||||
if (window.toastManager) {
|
||||
window.toastManager.error('❌ Configurações não persistiram após reload');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Função para limpar testes
|
||||
function limparTestes() {
|
||||
console.log('🧹 Limpando testes...');
|
||||
|
||||
if (typeof AdminConfigManager !== 'undefined') {
|
||||
const manager = new AdminConfigManager();
|
||||
manager.resetConfig();
|
||||
console.log('✅ Configurações resetadas');
|
||||
}
|
||||
|
||||
if (typeof BackupManager !== 'undefined') {
|
||||
const backupManager = new BackupManager();
|
||||
backupManager.clearBackups();
|
||||
console.log('✅ Backups limpos');
|
||||
}
|
||||
|
||||
if (window.toastManager) {
|
||||
window.toastManager.success('🧹 Testes limpos com sucesso!');
|
||||
}
|
||||
|
||||
console.log('✅ Testes limpos');
|
||||
}
|
||||
|
||||
// Disponibilizar funções globalmente
|
||||
window.testarPersistenciaSimples = testarPersistenciaSimples;
|
||||
window.testarReload = testarReload;
|
||||
window.limparTestes = limparTestes;
|
||||
|
||||
// Auto-executar teste quando a página carregar
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
setTimeout(testarPersistenciaSimples, 2000);
|
||||
});
|
||||
} else {
|
||||
setTimeout(testarPersistenciaSimples, 2000);
|
||||
}
|
||||
|
||||
console.log('🧪 Sistema de teste simples de persistência carregado');
|
||||
310
js/tests/test-persistencia.js
Normal file
310
js/tests/test-persistencia.js
Normal file
@@ -0,0 +1,310 @@
|
||||
/**
|
||||
* Script de Teste para Sistema de Persistência
|
||||
* Testa as funcionalidades do AdminConfigManager e BackupManager
|
||||
*/
|
||||
|
||||
class PersistenceTest {
|
||||
constructor() {
|
||||
this.tests = [];
|
||||
this.results = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Executa todos os testes de persistência
|
||||
*/
|
||||
async runAllTests() {
|
||||
console.log('🧪 Iniciando testes de persistência...');
|
||||
|
||||
// Aguardar carregamento dos módulos
|
||||
await this.waitForModules();
|
||||
|
||||
// Executar testes
|
||||
await this.testConfigPersistence();
|
||||
await this.testBackupSystem();
|
||||
await this.testThemePersistence();
|
||||
await this.testResetFunctionality();
|
||||
|
||||
// Mostrar resultados
|
||||
this.showResults();
|
||||
|
||||
console.log('✅ Testes de persistência concluídos!');
|
||||
}
|
||||
|
||||
/**
|
||||
* Aguarda os módulos necessários estarem carregados
|
||||
*/
|
||||
async waitForModules() {
|
||||
console.log('⏳ Aguardando módulos...');
|
||||
|
||||
let attempts = 0;
|
||||
const maxAttempts = 50; // 5 segundos
|
||||
|
||||
while (attempts < maxAttempts) {
|
||||
// Verificar instâncias globais inicializadas pelos módulos
|
||||
if (window.adminConfigManager && window.backupManager && window.toastManager) {
|
||||
console.log('✅ Módulos (instâncias) carregados');
|
||||
return;
|
||||
}
|
||||
|
||||
// Como fallback, se as classes existirem mas instâncias ainda não, tentar inicializar
|
||||
if (!window.adminConfigManager && typeof AdminConfigManager !== 'undefined') {
|
||||
try { window.adminConfigManager = new AdminConfigManager(); } catch(e) {}
|
||||
}
|
||||
if (!window.backupManager && typeof BackupManager !== 'undefined') {
|
||||
try { window.backupManager = new BackupManager(); } catch(e) {}
|
||||
}
|
||||
if (!window.toastManager && typeof ToastManager !== 'undefined') {
|
||||
try { window.toastManager = new ToastManager(); } catch(e) {}
|
||||
}
|
||||
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
attempts++;
|
||||
}
|
||||
|
||||
throw new Error('❌ Módulos não carregados após 5 segundos');
|
||||
}
|
||||
|
||||
/**
|
||||
* Testa persistência de configurações
|
||||
*/
|
||||
async testConfigPersistence() {
|
||||
console.log('📝 Testando persistência de configurações...');
|
||||
|
||||
try {
|
||||
// Criar instância do config manager
|
||||
const configManager = new AdminConfigManager();
|
||||
|
||||
// Testar configuração inicial
|
||||
const initialConfig = configManager.getConfig();
|
||||
this.addResult('Config inicial', !!initialConfig, 'Configuração inicial carregada');
|
||||
|
||||
// Testar salvamento de configuração
|
||||
configManager.updateConfig('app.name', 'Aço Calc Pro - Teste');
|
||||
configManager.updateConfig('app.version', '2.0.0');
|
||||
configManager.updateConfig('theme.name', 'light');
|
||||
|
||||
// Recarregar e verificar se persistiu
|
||||
const configManager2 = new AdminConfigManager();
|
||||
const config2 = configManager2.getConfig();
|
||||
|
||||
this.addResult('Salvamento app.name', config2.app.name === 'Aço Calc Pro - Teste', 'Nome da aplicação persistido');
|
||||
this.addResult('Salvamento app.version', config2.app.version === '2.0.0', 'Versão persistida');
|
||||
this.addResult('Salvamento theme.name', config2.theme.name === 'light', 'Tema persistido');
|
||||
|
||||
console.log('✅ Testes de persistência de configurações concluídos');
|
||||
|
||||
} catch (error) {
|
||||
this.addResult('Config Persistence', false, `Erro: ${error.message}`);
|
||||
console.error('❌ Erro nos testes de configuração:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Testa sistema de backup
|
||||
*/
|
||||
async testBackupSystem() {
|
||||
console.log('💾 Testando sistema de backup...');
|
||||
|
||||
try {
|
||||
const backupManager = new BackupManager();
|
||||
|
||||
// Testar criação de backup
|
||||
const backup = await backupManager.createBackup();
|
||||
this.addResult('Criação backup', !!backup.id, 'Backup criado com ID');
|
||||
|
||||
// Testar listagem de backups
|
||||
const backups = backupManager.getBackups();
|
||||
this.addResult('Listagem backups', backups.length > 0, `${backups.length} backup(s) encontrado(s)`);
|
||||
|
||||
// Testar recuperação de backup específico (via lista)
|
||||
const specificBackup = backups.find(b => b.timestamp === backup.timestamp);
|
||||
this.addResult('Recuperação backup', !!specificBackup, 'Backup recuperado por timestamp');
|
||||
|
||||
// Testar remoção de backup (usar timestamp)
|
||||
backupManager.removeBackup(backup.timestamp);
|
||||
const backupsAfterRemove = backupManager.getBackups();
|
||||
this.addResult('Remoção backup', backupsAfterRemove.length === backups.length - 1, 'Backup removido com sucesso');
|
||||
|
||||
console.log('✅ Testes de backup concluídos');
|
||||
|
||||
} catch (error) {
|
||||
this.addResult('Backup System', false, `Erro: ${error.message}`);
|
||||
console.error('❌ Erro nos testes de backup:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Testa persistência de tema
|
||||
*/
|
||||
async testThemePersistence() {
|
||||
console.log('🎨 Testando persistência de tema...');
|
||||
|
||||
try {
|
||||
const configManager = new AdminConfigManager();
|
||||
|
||||
// Testar diferentes temas
|
||||
const themes = ['dark', 'light', 'auto'];
|
||||
|
||||
for (const theme of themes) {
|
||||
configManager.updateConfig('theme.name', theme);
|
||||
|
||||
// Recarregar e verificar
|
||||
const configManager2 = new AdminConfigManager();
|
||||
const config2 = configManager2.getConfig();
|
||||
|
||||
this.addResult(`Tema ${theme}`, config2.theme.name === theme, `Tema ${theme} persistido`);
|
||||
}
|
||||
|
||||
console.log('✅ Testes de tema concluídos');
|
||||
|
||||
} catch (error) {
|
||||
this.addResult('Theme Persistence', false, `Erro: ${error.message}`);
|
||||
console.error('❌ Erro nos testes de tema:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Testa funcionalidade de reset
|
||||
*/
|
||||
async testResetFunctionality() {
|
||||
console.log('🔄 Testando funcionalidade de reset...');
|
||||
|
||||
try {
|
||||
const configManager = new AdminConfigManager();
|
||||
|
||||
// Modificar configurações
|
||||
configManager.updateConfig('app.name', 'Nome Modificado');
|
||||
configManager.updateConfig('app.version', '9.9.9');
|
||||
|
||||
// Resetar
|
||||
configManager.resetConfig();
|
||||
|
||||
// Verificar se voltou ao padrão
|
||||
const config = configManager.getConfig();
|
||||
// Estrutura padrão usa chaves top-level: appName e version
|
||||
const isDefaultName = config.appName === 'AÇO CALC PRO';
|
||||
const isDefaultVersion = config.version === '1.0.0';
|
||||
|
||||
this.addResult('Reset app.name', isDefaultName, 'Nome resetado para padrão');
|
||||
this.addResult('Reset app.version', isDefaultVersion, 'Versão resetada para padrão');
|
||||
|
||||
console.log('✅ Testes de reset concluídos');
|
||||
|
||||
} catch (error) {
|
||||
this.addResult('Reset Functionality', false, `Erro: ${error.message}`);
|
||||
console.error('❌ Erro nos testes de reset:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adiciona resultado de teste
|
||||
*/
|
||||
addResult(testName, success, message) {
|
||||
this.results.push({
|
||||
name: testName,
|
||||
success: success,
|
||||
message: message,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
|
||||
const status = success ? '✅' : '❌';
|
||||
console.log(`${status} ${testName}: ${message}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Mostra resultados dos testes
|
||||
*/
|
||||
showResults() {
|
||||
const totalTests = this.results.length;
|
||||
const passedTests = this.results.filter(r => r.success).length;
|
||||
const failedTests = totalTests - passedTests;
|
||||
|
||||
console.log('\n📊 RESUMO DOS TESTES:');
|
||||
console.log(`Total de testes: ${totalTests}`);
|
||||
console.log(`✅ Passou: ${passedTests}`);
|
||||
console.log(`❌ Falhou: ${failedTests}`);
|
||||
console.log(`Taxa de sucesso: ${((passedTests / totalTests) * 100).toFixed(1)}%`);
|
||||
|
||||
// Mostrar falhas em detalhes
|
||||
if (failedTests > 0) {
|
||||
console.log('\n📋 FALHAS DETALHADAS:');
|
||||
this.results.filter(r => !r.success).forEach(result => {
|
||||
console.log(`❌ ${result.name}: ${result.message}`);
|
||||
});
|
||||
}
|
||||
|
||||
// Testar se o ToastManager está disponível para mostrar resultado
|
||||
if (window.toastManager) {
|
||||
const successRate = (passedTests / totalTests) * 100;
|
||||
if (successRate === 100) {
|
||||
window.toastManager.success(`🧪 Todos os testes de persistência passaram! (${passedTests}/${totalTests})`);
|
||||
} else if (successRate >= 80) {
|
||||
window.toastManager.warning(`🧪 ${passedTests}/${totalTests} testes passaram (${successRate.toFixed(1)}%)`);
|
||||
} else {
|
||||
window.toastManager.error(`🧪 Apenas ${passedTests}/${totalTests} testes passaram (${successRate.toFixed(1)}%)`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Função para testar persistência após reload
|
||||
function testarPersistenciaAposReload() {
|
||||
console.log('🔄 Testando persistência após reload da página...');
|
||||
|
||||
// Salvar configuração de teste
|
||||
if (window.adminConfigManager) {
|
||||
window.adminConfigManager.updateConfig('app.name', 'Aço Calc Pro - Teste Reload');
|
||||
window.adminConfigManager.updateConfig('theme.name', 'light');
|
||||
|
||||
console.log('✅ Configurações de teste salvas');
|
||||
console.log('🔄 Recarregando página em 2 segundos...');
|
||||
|
||||
setTimeout(() => {
|
||||
location.reload();
|
||||
}, 2000);
|
||||
} else {
|
||||
// Verificar se as configurações persistiram após reload
|
||||
if (window.adminConfigManager) {
|
||||
const config = window.adminConfigManager.getConfig();
|
||||
|
||||
if (config.app.name === 'Aço Calc Pro - Teste Reload') {
|
||||
console.log('✅ Persistência após reload: SUCESSO!');
|
||||
if (window.toastManager) {
|
||||
window.toastManager.success('✅ Configurações persistiram após reload!');
|
||||
}
|
||||
} else {
|
||||
console.log('❌ Persistência após reload: FALHA');
|
||||
if (window.toastManager) {
|
||||
window.toastManager.error('❌ Configurações não persistiram após reload');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Auto-executar testes quando os módulos estiverem prontos
|
||||
if (typeof window !== 'undefined') {
|
||||
window.addEventListener('load', async () => {
|
||||
console.log('🧪 Sistema de testes de persistência carregado');
|
||||
|
||||
// Aguardar um pouco para garantir que tudo está carregado
|
||||
setTimeout(async () => {
|
||||
try {
|
||||
const tester = new PersistenceTest();
|
||||
await tester.runAllTests();
|
||||
} catch (error) {
|
||||
console.error('❌ Erro ao executar testes:', error);
|
||||
}
|
||||
}, 1000);
|
||||
});
|
||||
}
|
||||
|
||||
// Disponibilizar funções globalmente para testes manuais
|
||||
window.testarPersistencia = () => {
|
||||
const tester = new PersistenceTest();
|
||||
tester.runAllTests();
|
||||
};
|
||||
|
||||
window.testarPersistenciaAposReload = testarPersistenciaAposReload;
|
||||
|
||||
console.log('🧪 Sistema de testes de persistência inicializado');
|
||||
81
js/tests/teste-manual-disponibilidade.js
Normal file
81
js/tests/teste-manual-disponibilidade.js
Normal file
@@ -0,0 +1,81 @@
|
||||
/**
|
||||
* Teste Manual de Disponibilidade de Módulos
|
||||
* Verifica se os módulos de persistência estão disponíveis globalmente
|
||||
*/
|
||||
|
||||
console.log('🔍 Teste Manual de Disponibilidade Iniciado');
|
||||
|
||||
// Verificar disponibilidade dos módulos
|
||||
function verificarDisponibilidade() {
|
||||
const resultados = {
|
||||
AdminConfigManager: typeof AdminConfigManager !== 'undefined',
|
||||
BackupManager: typeof BackupManager !== 'undefined',
|
||||
ToastManager: typeof ToastManager !== 'undefined',
|
||||
adminConfigManager: typeof window.adminConfigManager !== 'undefined',
|
||||
backupManager: typeof window.backupManager !== 'undefined',
|
||||
toastManager: typeof window.toastManager !== 'undefined'
|
||||
};
|
||||
|
||||
console.log('📊 Disponibilidade de Módulos:', resultados);
|
||||
|
||||
// Testar criação de instâncias
|
||||
if (resultados.AdminConfigManager) {
|
||||
try {
|
||||
const tempConfig = new AdminConfigManager();
|
||||
console.log('✅ AdminConfigManager pode ser instanciado');
|
||||
console.log('📋 Config padrão:', tempConfig.getConfig());
|
||||
} catch (error) {
|
||||
console.error('❌ Erro ao instanciar AdminConfigManager:', error);
|
||||
}
|
||||
}
|
||||
|
||||
if (resultados.BackupManager) {
|
||||
try {
|
||||
const tempBackup = new BackupManager();
|
||||
console.log('✅ BackupManager pode ser instanciado');
|
||||
} catch (error) {
|
||||
console.error('❌ Erro ao instanciar BackupManager:', error);
|
||||
}
|
||||
}
|
||||
|
||||
if (resultados.ToastManager) {
|
||||
try {
|
||||
const tempToast = new ToastManager();
|
||||
console.log('✅ ToastManager pode ser instanciado');
|
||||
} catch (error) {
|
||||
console.error('❌ Erro ao instanciar ToastManager:', error);
|
||||
}
|
||||
}
|
||||
|
||||
return resultados;
|
||||
}
|
||||
|
||||
// Executar após 2 segundos para garantir carregamento
|
||||
setTimeout(() => {
|
||||
console.log('⏱️ Executando teste de disponibilidade após 2s...');
|
||||
const resultados = verificarDisponibilidade();
|
||||
|
||||
// Testar persistência básica
|
||||
if (resultados.AdminConfigManager) {
|
||||
try {
|
||||
const configManager = new AdminConfigManager();
|
||||
const config = configManager.getConfig();
|
||||
config.appName = 'Teste Manual';
|
||||
configManager.saveConfig(config);
|
||||
|
||||
const configSalva = configManager.getConfig();
|
||||
console.log('✅ Teste de persistência básico:', configSalva.appName === 'Teste Manual' ? 'SUCESSO' : 'FALHA');
|
||||
|
||||
// Resetar
|
||||
configManager.resetConfig();
|
||||
console.log('🔄 Config resetada para padrão');
|
||||
} catch (error) {
|
||||
console.error('❌ Erro no teste de persistência:', error);
|
||||
}
|
||||
}
|
||||
|
||||
console.log('✅ Teste Manual de Disponibilidade Concluído');
|
||||
}, 2000);
|
||||
|
||||
// Tornar função global para testes manuais
|
||||
window.verificarDisponibilidade = verificarDisponibilidade;
|
||||
176
js/tests/verificador-persistencia.js
Normal file
176
js/tests/verificador-persistencia.js
Normal file
@@ -0,0 +1,176 @@
|
||||
/**
|
||||
* Verificador de Status do Sistema de Persistência
|
||||
* Verifica se os módulos foram carregados corretamente
|
||||
*/
|
||||
|
||||
(function verificarStatusPersistencia() {
|
||||
console.log('🔍 Verificando status do sistema de persistência...');
|
||||
|
||||
// Verificar após um delay para garantir que tudo foi carregado
|
||||
setTimeout(() => {
|
||||
const status = {
|
||||
AdminConfigManager: typeof AdminConfigManager !== 'undefined',
|
||||
BackupManager: typeof BackupManager !== 'undefined',
|
||||
ToastManager: typeof ToastManager !== 'undefined',
|
||||
adminConfigManager: window.adminConfigManager !== null,
|
||||
backupManager: window.backupManager !== null,
|
||||
toastManager: window.toastManager !== null
|
||||
};
|
||||
|
||||
console.log('📊 Status do Sistema de Persistência:');
|
||||
console.table(status);
|
||||
|
||||
// Verificar localStorage
|
||||
const localStorageKeys = Object.keys(localStorage).filter(key =>
|
||||
key.includes('admin') || key.includes('backup') || key.includes('config')
|
||||
);
|
||||
|
||||
console.log('📁 Chaves no localStorage:', localStorageKeys);
|
||||
|
||||
// Tentar criar instâncias se não existirem
|
||||
if (typeof AdminConfigManager !== 'undefined' && !window.adminConfigManager) {
|
||||
try {
|
||||
window.adminConfigManager = new AdminConfigManager();
|
||||
console.log('✅ AdminConfigManager inicializado manualmente');
|
||||
} catch (error) {
|
||||
console.error('❌ Erro ao inicializar AdminConfigManager:', error);
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof BackupManager !== 'undefined' && !window.backupManager) {
|
||||
try {
|
||||
window.backupManager = new BackupManager();
|
||||
console.log('✅ BackupManager inicializado manualmente');
|
||||
} catch (error) {
|
||||
console.error('❌ Erro ao inicializar BackupManager:', error);
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof ToastManager !== 'undefined' && !window.toastManager) {
|
||||
try {
|
||||
window.toastManager = new ToastManager();
|
||||
console.log('✅ ToastManager inicializado manualmente');
|
||||
} catch (error) {
|
||||
console.error('❌ Erro ao inicializar ToastManager:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Testar funcionalidade básica
|
||||
if (window.adminConfigManager) {
|
||||
try {
|
||||
const config = window.adminConfigManager.getConfig();
|
||||
console.log('✅ Configuração atual:', config);
|
||||
|
||||
// Testar salvamento
|
||||
window.adminConfigManager.updateConfig('test.verification', new Date().toISOString());
|
||||
console.log('✅ Teste de salvamento realizado');
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ Erro ao testar AdminConfigManager:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Mostrar resumo
|
||||
const totalModulos = Object.keys(status).length;
|
||||
const modulosOk = Object.values(status).filter(v => v).length;
|
||||
const percentual = (modulosOk / totalModulos * 100).toFixed(1);
|
||||
|
||||
console.log(`📈 Sistema de Persistência: ${modulosOk}/${totalModulos} módulos OK (${percentual}%)`);
|
||||
|
||||
if (window.toastManager) {
|
||||
if (percentual === '100.0') {
|
||||
window.toastManager.success('✅ Sistema de persistência totalmente operacional!');
|
||||
} else if (percentual >= '66.7') {
|
||||
window.toastManager.warning(`⚠️ Sistema de persistência parcial (${percentual}%)`);
|
||||
} else {
|
||||
window.toastManager.error(`❌ Sistema de persistência com problemas (${percentual}%)`);
|
||||
}
|
||||
}
|
||||
|
||||
}, 3000); // Aguardar 3 segundos para garantir carregamento
|
||||
|
||||
// Também verificar após 10 segundos (backup)
|
||||
setTimeout(() => {
|
||||
console.log('🔍 Verificação de backup do sistema de persistência:');
|
||||
|
||||
if (window.adminConfigManager) {
|
||||
try {
|
||||
const config = window.adminConfigManager.getConfig();
|
||||
console.log('✅ Config após 10s:', {
|
||||
appName: config.app.name,
|
||||
theme: config.theme.name,
|
||||
version: config.version
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('❌ AdminConfigManager ainda com problemas:', error);
|
||||
}
|
||||
} else {
|
||||
console.error('❌ AdminConfigManager não disponível após 10s');
|
||||
}
|
||||
}, 10000);
|
||||
|
||||
})();
|
||||
|
||||
// Função para debug detalhado
|
||||
window.debugPersistencia = function() {
|
||||
console.group('🔍 Debug Detalhado do Sistema de Persistência');
|
||||
|
||||
// Verificar cada módulo
|
||||
console.log('1. Verificando AdminConfigManager:');
|
||||
console.log(' - Tipo:', typeof AdminConfigManager);
|
||||
console.log(' - Disponível:', typeof AdminConfigManager !== 'undefined');
|
||||
|
||||
if (typeof AdminConfigManager !== 'undefined') {
|
||||
try {
|
||||
const tempManager = new AdminConfigManager();
|
||||
console.log(' - Instância criada com sucesso');
|
||||
console.log(' - Config padrão:', tempManager.getConfig());
|
||||
|
||||
// Testar localStorage
|
||||
const key = tempManager.configKey;
|
||||
const data = localStorage.getItem(key);
|
||||
console.log(' - localStorage key:', key);
|
||||
console.log(' - Dados no localStorage:', data ? 'Presentes' : 'Ausentes');
|
||||
|
||||
} catch (error) {
|
||||
console.error(' - Erro ao criar instância:', error);
|
||||
}
|
||||
}
|
||||
|
||||
console.log('2. Verificando BackupManager:');
|
||||
console.log(' - Tipo:', typeof BackupManager);
|
||||
console.log(' - Disponível:', typeof BackupManager !== 'undefined');
|
||||
|
||||
console.log('3. Verificando ToastManager:');
|
||||
console.log(' - Tipo:', typeof ToastManager);
|
||||
console.log(' - Disponível:', typeof ToastManager !== 'undefined');
|
||||
|
||||
console.log('4. Verificando variáveis globais:');
|
||||
console.log(' - window.adminConfigManager:', window.adminConfigManager);
|
||||
console.log(' - window.backupManager:', window.backupManager);
|
||||
console.log(' - window.toastManager:', window.toastManager);
|
||||
|
||||
console.log('5. Verificando localStorage:');
|
||||
const relevantKeys = Object.keys(localStorage).filter(key =>
|
||||
key.includes('admin') || key.includes('backup') || key.includes('config')
|
||||
);
|
||||
console.log(' - Chaves relevantes:', relevantKeys);
|
||||
relevantKeys.forEach(key => {
|
||||
try {
|
||||
const data = JSON.parse(localStorage.getItem(key));
|
||||
console.log(` - ${key}:`, {
|
||||
type: typeof data,
|
||||
hasVersion: !!data.version,
|
||||
hasAppConfig: !!(data.app || data.application),
|
||||
hasTheme: !!(data.theme || data.themes),
|
||||
size: JSON.stringify(data).length
|
||||
});
|
||||
} catch (e) {
|
||||
console.log(` - ${key}: (dados inválidos)`);
|
||||
}
|
||||
});
|
||||
|
||||
console.groupEnd();
|
||||
};
|
||||
|
||||
console.log('🔍 Sistema de verificação de persistência carregado');
|
||||
402
js/ui/csv-manager-ui.js
Normal file
402
js/ui/csv-manager-ui.js
Normal file
@@ -0,0 +1,402 @@
|
||||
/**
|
||||
* CSV Manager UI
|
||||
* User interface for CRUD operations on CSV files
|
||||
*/
|
||||
|
||||
import {
|
||||
loadCSV,
|
||||
toCSV,
|
||||
downloadCSV,
|
||||
getAvailableCSVFiles,
|
||||
validateCSVData
|
||||
} from '../utils/csv-manager.js';
|
||||
|
||||
// Current state
|
||||
let currentCSVData = [];
|
||||
let currentFilename = '';
|
||||
let currentFileId = '';
|
||||
let editingIndex = -1;
|
||||
|
||||
/**
|
||||
* Open CSV Manager Modal
|
||||
*/
|
||||
export function openCSVManager() {
|
||||
const modal = document.getElementById('csvManagerModal');
|
||||
const select = document.getElementById('csvFileSelect');
|
||||
|
||||
// Populate file selector
|
||||
const files = getAvailableCSVFiles();
|
||||
select.innerHTML = '<option value="">-- Selecione um arquivo --</option>';
|
||||
files.forEach(file => {
|
||||
const option = document.createElement('option');
|
||||
option.value = file.id;
|
||||
option.textContent = `${file.icon} ${file.name} - ${file.description}`;
|
||||
select.appendChild(option);
|
||||
});
|
||||
|
||||
modal.classList.add('active');
|
||||
}
|
||||
|
||||
/**
|
||||
* Close CSV Manager Modal
|
||||
*/
|
||||
export function closeCSVManager() {
|
||||
const modal = document.getElementById('csvManagerModal');
|
||||
modal.classList.remove('active');
|
||||
|
||||
// Reset state
|
||||
currentCSVData = [];
|
||||
currentFilename = '';
|
||||
currentFileId = '';
|
||||
|
||||
// Reset UI
|
||||
document.getElementById('csvFileSelect').value = '';
|
||||
document.getElementById('csvContent').innerHTML = `
|
||||
<div style="text-align: center; padding: 60px 20px; color: var(--color-text-secondary);">
|
||||
<div style="font-size: 64px; margin-bottom: 16px;">📁</div>
|
||||
<p style="font-size: 18px; margin-bottom: 8px;">Selecione um arquivo CSV para começar</p>
|
||||
<p style="font-size: 14px;">Você poderá visualizar, editar, adicionar e remover registros</p>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Hide buttons
|
||||
document.getElementById('btnAddRecord').style.display = 'none';
|
||||
document.getElementById('btnDownload').style.display = 'none';
|
||||
}
|
||||
|
||||
/**
|
||||
* Load selected CSV file
|
||||
*/
|
||||
export async function loadSelectedCSV() {
|
||||
const select = document.getElementById('csvFileSelect');
|
||||
const fileId = select.value;
|
||||
|
||||
if (!fileId) {
|
||||
closeCSVManager();
|
||||
openCSVManager();
|
||||
return;
|
||||
}
|
||||
|
||||
const files = getAvailableCSVFiles();
|
||||
const file = files.find(f => f.id === fileId);
|
||||
|
||||
if (!file) return;
|
||||
|
||||
// Show loading
|
||||
document.getElementById('csvContent').innerHTML = `
|
||||
<div style="text-align: center; padding: 60px 20px;">
|
||||
<div style="font-size: 48px; margin-bottom: 16px;">⏳</div>
|
||||
<p style="font-size: 18px;">Carregando ${file.name}...</p>
|
||||
</div>
|
||||
`;
|
||||
|
||||
try {
|
||||
// Load CSV
|
||||
const data = await loadCSV(file.filename);
|
||||
|
||||
// Update state
|
||||
currentCSVData = data;
|
||||
currentFilename = file.filename;
|
||||
currentFileId = fileId;
|
||||
|
||||
// Render table
|
||||
renderCSVTable(data, file);
|
||||
|
||||
// Show buttons
|
||||
document.getElementById('btnAddRecord').style.display = 'inline-block';
|
||||
document.getElementById('btnDownload').style.display = 'inline-block';
|
||||
|
||||
} catch (error) {
|
||||
document.getElementById('csvContent').innerHTML = `
|
||||
<div style="text-align: center; padding: 60px 20px;">
|
||||
<div style="font-size: 48px; margin-bottom: 16px; color: var(--color-error);">❌</div>
|
||||
<p style="font-size: 18px; color: var(--color-error); margin-bottom: 8px;">Erro ao carregar arquivo</p>
|
||||
<p style="font-size: 14px; color: var(--color-text-secondary);">${error.message}</p>
|
||||
<button class="btn btn-primary" onclick="location.reload()" style="margin-top: 20px;">Recarregar Página</button>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Render CSV data as table
|
||||
* @param {Array<object>} data - CSV data
|
||||
* @param {object} file - File metadata
|
||||
*/
|
||||
function renderCSVTable(data, file) {
|
||||
if (data.length === 0) {
|
||||
document.getElementById('csvContent').innerHTML = `
|
||||
<div style="text-align: center; padding: 60px 20px;">
|
||||
<div style="font-size: 48px; margin-bottom: 16px;">📭</div>
|
||||
<p style="font-size: 18px; margin-bottom: 8px;">Arquivo vazio</p>
|
||||
<p style="font-size: 14px; color: var(--color-text-secondary);">Adicione o primeiro registro</p>
|
||||
</div>
|
||||
`;
|
||||
return;
|
||||
}
|
||||
|
||||
const headers = Object.keys(data[0]);
|
||||
|
||||
let html = `
|
||||
<div style="margin-bottom: 20px; padding: 16px; background: var(--color-bg-1); border-radius: 8px;">
|
||||
<div style="display: flex; align-items: center; gap: 12px; margin-bottom: 8px;">
|
||||
<span style="font-size: 32px;">${file.icon}</span>
|
||||
<div>
|
||||
<h3 style="margin: 0; font-size: 18px;">${file.name}</h3>
|
||||
<p style="margin: 0; font-size: 13px; color: var(--color-text-secondary);">${file.description}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div style="display: flex; gap: 20px; margin-top: 12px; font-size: 13px; color: var(--color-text-secondary);">
|
||||
<span>📄 <strong>${data.length}</strong> registros</span>
|
||||
<span>📊 <strong>${headers.length}</strong> colunas</span>
|
||||
<span>💾 <strong>${currentFilename}</strong></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="table-wrapper" style="overflow-x: auto;">
|
||||
<table style="width: 100%; border-collapse: collapse; font-size: 13px;">
|
||||
<thead>
|
||||
<tr style="background: var(--color-primary); color: white;">
|
||||
<th style="padding: 12px; text-align: left; position: sticky; left: 0; background: var(--color-primary);">#</th>
|
||||
${headers.map(h => `<th style="padding: 12px; text-align: left; white-space: nowrap;">${h}</th>`).join('')}
|
||||
<th style="padding: 12px; text-align: center; width: 120px;">Ações</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
${data.map((row, index) => `
|
||||
<tr style="border-bottom: 1px solid var(--color-border); ${index % 2 === 0 ? 'background: var(--color-surface);' : ''}">
|
||||
<td style="padding: 12px; font-weight: bold; position: sticky; left: 0; background: ${index % 2 === 0 ? 'var(--color-surface)' : 'var(--color-background)'};">${index + 1}</td>
|
||||
${headers.map(h => `<td style="padding: 12px; white-space: nowrap;">${row[h] || '-'}</td>`).join('')}
|
||||
<td style="padding: 12px; text-align: center;">
|
||||
<button class="btn-icon-small" onclick="editRecord(${index})" title="Editar" style="background: var(--color-primary); color: white; border: none; padding: 6px 12px; border-radius: 4px; cursor: pointer; margin-right: 4px;">✏️</button>
|
||||
<button class="btn-icon-small" onclick="deleteRecord(${index})" title="Deletar" style="background: var(--color-error); color: white; border: none; padding: 6px 12px; border-radius: 4px; cursor: pointer;">🗑️</button>
|
||||
</td>
|
||||
</tr>
|
||||
`).join('')}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
`;
|
||||
|
||||
document.getElementById('csvContent').innerHTML = html;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add new record
|
||||
*/
|
||||
export function addNewRecord() {
|
||||
if (currentCSVData.length === 0) {
|
||||
alert('⚠️ Não é possível adicionar registro em arquivo vazio. Faça upload de um arquivo com estrutura.');
|
||||
return;
|
||||
}
|
||||
|
||||
editingIndex = -1;
|
||||
const headers = Object.keys(currentCSVData[0]);
|
||||
showRecordModal('➕ Adicionar Novo Registro', headers, {});
|
||||
}
|
||||
|
||||
/**
|
||||
* Edit existing record
|
||||
* @param {number} index - Record index
|
||||
*/
|
||||
export function editRecord(index) {
|
||||
editingIndex = index;
|
||||
const record = currentCSVData[index];
|
||||
const headers = Object.keys(record);
|
||||
showRecordModal(`✏️ Editar Registro #${index + 1}`, headers, record);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete record
|
||||
* @param {number} index - Record index
|
||||
*/
|
||||
export function deleteRecord(index) {
|
||||
const record = currentCSVData[index];
|
||||
const recordName = record.nome || record.id || `Registro #${index + 1}`;
|
||||
|
||||
if (!confirm(`🗑️ Tem certeza que deseja deletar:\n\n"${recordName}"?\n\nEsta ação não pode ser desfeita.`)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Remove from array
|
||||
currentCSVData.splice(index, 1);
|
||||
|
||||
// Re-render table
|
||||
const files = getAvailableCSVFiles();
|
||||
const file = files.find(f => f.id === currentFileId);
|
||||
renderCSVTable(currentCSVData, file);
|
||||
|
||||
// Show success message
|
||||
showToast('✅ Registro deletado com sucesso!', 'success');
|
||||
}
|
||||
|
||||
/**
|
||||
* Show record editor modal
|
||||
* @param {string} title - Modal title
|
||||
* @param {Array<string>} headers - Field names
|
||||
* @param {object} data - Current data
|
||||
*/
|
||||
function showRecordModal(title, headers, data) {
|
||||
document.getElementById('recordModalTitle').textContent = title;
|
||||
|
||||
let html = '<div style="display: flex; flex-direction: column; gap: 16px;">';
|
||||
|
||||
headers.forEach(header => {
|
||||
const value = data[header] || '';
|
||||
html += `
|
||||
<div class="form-group" style="margin-bottom: 0;">
|
||||
<label class="form-label">${header}</label>
|
||||
<input
|
||||
type="text"
|
||||
class="form-control"
|
||||
id="field_${header}"
|
||||
value="${value}"
|
||||
placeholder="Digite ${header}"
|
||||
${header === 'id' ? 'required' : ''}
|
||||
>
|
||||
${header === 'id' ? '<small style="color: var(--color-text-secondary);">Campo obrigatório e único</small>' : ''}
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
|
||||
html += '</div>';
|
||||
|
||||
document.getElementById('recordModalBody').innerHTML = html;
|
||||
document.getElementById('csvRecordModal').classList.add('active');
|
||||
}
|
||||
|
||||
/**
|
||||
* Close record editor modal
|
||||
*/
|
||||
export function closeRecordModal() {
|
||||
document.getElementById('csvRecordModal').classList.remove('active');
|
||||
editingIndex = -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Save record (add or edit)
|
||||
*/
|
||||
export function saveRecord() {
|
||||
const headers = Object.keys(currentCSVData[0] || {});
|
||||
const newRecord = {};
|
||||
|
||||
// Collect form data
|
||||
headers.forEach(header => {
|
||||
const input = document.getElementById(`field_${header}`);
|
||||
if (input) {
|
||||
newRecord[header] = input.value.trim();
|
||||
}
|
||||
});
|
||||
|
||||
// Validate required fields
|
||||
if (!newRecord.id) {
|
||||
alert('⚠️ O campo "id" é obrigatório!');
|
||||
return;
|
||||
}
|
||||
|
||||
// Check for duplicate ID (only when adding new)
|
||||
if (editingIndex === -1) {
|
||||
const duplicate = currentCSVData.find(r => r.id === newRecord.id);
|
||||
if (duplicate) {
|
||||
alert(`⚠️ Já existe um registro com o ID "${newRecord.id}"!\n\nEscolha um ID único.`);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Add or update
|
||||
if (editingIndex === -1) {
|
||||
// Add new
|
||||
currentCSVData.push(newRecord);
|
||||
showToast('✅ Registro adicionado com sucesso!', 'success');
|
||||
} else {
|
||||
// Update existing
|
||||
currentCSVData[editingIndex] = newRecord;
|
||||
showToast('✅ Registro atualizado com sucesso!', 'success');
|
||||
}
|
||||
|
||||
// Re-render table
|
||||
const files = getAvailableCSVFiles();
|
||||
const file = files.find(f => f.id === currentFileId);
|
||||
renderCSVTable(currentCSVData, file);
|
||||
|
||||
// Close modal
|
||||
closeRecordModal();
|
||||
}
|
||||
|
||||
/**
|
||||
* Download current CSV
|
||||
*/
|
||||
export function downloadCurrentCSV() {
|
||||
if (currentCSVData.length === 0) {
|
||||
alert('⚠️ Não há dados para download!');
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate data
|
||||
const validation = validateCSVData(currentCSVData);
|
||||
if (!validation.valid) {
|
||||
const proceed = confirm(`⚠️ Dados contêm erros:\n\n${validation.errors.join('\n')}\n\nDeseja fazer download mesmo assim?`);
|
||||
if (!proceed) return;
|
||||
}
|
||||
|
||||
// Convert to CSV
|
||||
const csvText = toCSV(currentCSVData);
|
||||
|
||||
// Download
|
||||
downloadCSV(currentFilename, csvText);
|
||||
|
||||
showToast('💾 CSV baixado com sucesso!', 'success');
|
||||
}
|
||||
|
||||
/**
|
||||
* Show toast notification
|
||||
* @param {string} message - Message text
|
||||
* @param {string} type - Type (success, error, warning)
|
||||
*/
|
||||
function showToast(message, type = 'success') {
|
||||
const toast = document.createElement('div');
|
||||
toast.className = `toast toast-${type}`;
|
||||
toast.textContent = message;
|
||||
toast.style.cssText = `
|
||||
position: fixed;
|
||||
bottom: 24px;
|
||||
right: 24px;
|
||||
padding: 16px 24px;
|
||||
background: ${type === 'success' ? 'var(--color-success)' : 'var(--color-error)'};
|
||||
color: white;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
|
||||
z-index: 10000;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
opacity: 0;
|
||||
transform: translateY(20px);
|
||||
transition: all 0.3s ease;
|
||||
`;
|
||||
|
||||
document.body.appendChild(toast);
|
||||
|
||||
setTimeout(() => {
|
||||
toast.style.opacity = '1';
|
||||
toast.style.transform = 'translateY(0)';
|
||||
}, 10);
|
||||
|
||||
setTimeout(() => {
|
||||
toast.style.opacity = '0';
|
||||
toast.style.transform = 'translateY(20px)';
|
||||
setTimeout(() => toast.remove(), 300);
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
// Export to global scope for onclick handlers
|
||||
if (typeof window !== 'undefined') {
|
||||
window.openCSVManager = openCSVManager;
|
||||
window.closeCSVManager = closeCSVManager;
|
||||
window.loadSelectedCSV = loadSelectedCSV;
|
||||
window.addNewRecord = addNewRecord;
|
||||
window.editRecord = editRecord;
|
||||
window.deleteRecord = deleteRecord;
|
||||
window.closeRecordModal = closeRecordModal;
|
||||
window.saveRecord = saveRecord;
|
||||
window.downloadCurrentCSV = downloadCurrentCSV;
|
||||
}
|
||||
127
js/ui/mobile-menu.js
Normal file
127
js/ui/mobile-menu.js
Normal file
@@ -0,0 +1,127 @@
|
||||
/**
|
||||
* Mobile Menu - Hamburger Navigation
|
||||
* Handles mobile sidebar drawer with smooth animations
|
||||
*/
|
||||
|
||||
export function initMobileMenu() {
|
||||
// Only initialize on mobile/tablet
|
||||
if (window.innerWidth >= 1024) return;
|
||||
|
||||
// Create hamburger button
|
||||
const hamburger = createHamburgerButton();
|
||||
|
||||
// Place hamburger inside header/topbar actions
|
||||
const headerActions = document.querySelector('.header-actions');
|
||||
if (headerActions) {
|
||||
headerActions.appendChild(hamburger);
|
||||
} else {
|
||||
// Fallback: add to body if header not found
|
||||
document.body.appendChild(hamburger);
|
||||
}
|
||||
|
||||
// Get sidebar
|
||||
const sidebar = document.querySelector('.sidebar');
|
||||
if (!sidebar) return;
|
||||
// Accessibility roles
|
||||
sidebar.setAttribute('role', 'navigation');
|
||||
sidebar.setAttribute('aria-label', 'Menu principal');
|
||||
|
||||
// Toggle menu function
|
||||
const toggleMenu = () => {
|
||||
const isOpen = hamburger.classList.contains('open');
|
||||
|
||||
hamburger.classList.toggle('open');
|
||||
sidebar.classList.toggle('open');
|
||||
|
||||
// Accessibility
|
||||
hamburger.setAttribute('aria-expanded', String(!isOpen));
|
||||
sidebar.setAttribute('aria-hidden', String(isOpen));
|
||||
};
|
||||
|
||||
// Event listeners
|
||||
hamburger.addEventListener('click', toggleMenu);
|
||||
// Close menu when clicking sidebar item
|
||||
document.querySelectorAll('.sidebar-item').forEach(item => {
|
||||
// Make items focusable for keyboard navigation
|
||||
if (!item.hasAttribute('tabindex')) item.setAttribute('tabindex', '0');
|
||||
item.addEventListener('click', () => {
|
||||
if (window.innerWidth < 1024 && hamburger.classList.contains('open')) {
|
||||
// Slide left then close
|
||||
sidebar.classList.add('closing-left');
|
||||
setTimeout(() => {
|
||||
sidebar.classList.remove('closing-left');
|
||||
toggleMenu();
|
||||
}, 250);
|
||||
}
|
||||
});
|
||||
// Keyboard activation closes menu
|
||||
item.addEventListener('keydown', (e) => {
|
||||
if ((e.key === 'Enter' || e.key === ' ') && window.innerWidth < 1024 && hamburger.classList.contains('open')) {
|
||||
e.preventDefault();
|
||||
sidebar.classList.add('closing-left');
|
||||
setTimeout(() => {
|
||||
sidebar.classList.remove('closing-left');
|
||||
toggleMenu();
|
||||
}, 250);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Handle resize - close menu and cleanup on desktop
|
||||
let resizeTimer;
|
||||
window.addEventListener('resize', () => {
|
||||
clearTimeout(resizeTimer);
|
||||
resizeTimer = setTimeout(() => {
|
||||
if (window.innerWidth >= 1024) {
|
||||
// Desktop - cleanup mobile menu
|
||||
if (hamburger.classList.contains('open')) {
|
||||
toggleMenu();
|
||||
}
|
||||
hamburger.style.display = 'none';
|
||||
} else {
|
||||
// Mobile - show menu button
|
||||
hamburger.style.display = 'flex';
|
||||
}
|
||||
}, 250);
|
||||
});
|
||||
|
||||
// Keyboard navigation and focus management
|
||||
document.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Escape' && hamburger.classList.contains('open')) {
|
||||
toggleMenu();
|
||||
hamburger.focus();
|
||||
}
|
||||
});
|
||||
|
||||
// Manage focus when opening
|
||||
hamburger.addEventListener('click', () => {
|
||||
if (hamburger.classList.contains('open')) {
|
||||
const firstItem = sidebar.querySelector('.sidebar-item');
|
||||
if (firstItem) firstItem.focus?.();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function createHamburgerButton() {
|
||||
const button = document.createElement('button');
|
||||
button.className = 'hamburger';
|
||||
button.setAttribute('aria-label', 'Menu de navegação');
|
||||
button.setAttribute('aria-expanded', 'false');
|
||||
button.innerHTML = `
|
||||
<div class="hamburger-icon">
|
||||
<span></span>
|
||||
<span></span>
|
||||
<span></span>
|
||||
</div>
|
||||
`;
|
||||
return button;
|
||||
}
|
||||
|
||||
// Auto-initialize if loaded as module
|
||||
if (typeof window !== 'undefined') {
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', initMobileMenu);
|
||||
} else {
|
||||
initMobileMenu();
|
||||
}
|
||||
}
|
||||
184
js/ui/navigation.js
Normal file
184
js/ui/navigation.js
Normal file
@@ -0,0 +1,184 @@
|
||||
/**
|
||||
* Navigation System
|
||||
* Handles section switching and sidebar management
|
||||
*/
|
||||
|
||||
import { appState, updateState } from '../core/state.js';
|
||||
import { loadSection, showLoading, showError } from './section-loader.js';
|
||||
|
||||
/**
|
||||
* Show a section
|
||||
* @param {string} sectionId - Section identifier
|
||||
*/
|
||||
export async function showSection(sectionId) {
|
||||
console.log(`🔄 Navegando para: ${sectionId}`);
|
||||
|
||||
// Update state
|
||||
updateState('currentSection', sectionId);
|
||||
|
||||
// Update sidebar active state
|
||||
updateSidebarActive(sectionId);
|
||||
|
||||
// Show loading
|
||||
showLoading();
|
||||
|
||||
try {
|
||||
// Load section content
|
||||
const content = await loadSection(sectionId);
|
||||
|
||||
// Inject content
|
||||
const mainContent = document.getElementById('main-content');
|
||||
if (mainContent) {
|
||||
mainContent.innerHTML = content;
|
||||
}
|
||||
|
||||
// Add help button after content loads
|
||||
setTimeout(() => {
|
||||
if (typeof window.addHelpButton === 'function') {
|
||||
// For sections with tabs, show help for first tab
|
||||
if (sectionId === 'preaquecimento') {
|
||||
window.addHelpButton('preaquecimento');
|
||||
} else if (sectionId === 'parafusos') {
|
||||
window.addHelpButton('parafusos-cisalhamento');
|
||||
} else {
|
||||
window.addHelpButton(sectionId);
|
||||
}
|
||||
}
|
||||
}, 100);
|
||||
|
||||
// Scroll to top
|
||||
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||||
|
||||
console.log(`✅ Seção ${sectionId} exibida`);
|
||||
|
||||
} catch (error) {
|
||||
console.error(`❌ Erro ao exibir seção ${sectionId}:`, error);
|
||||
showError('Erro ao carregar seção. Tente novamente.');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update sidebar active state
|
||||
* @param {string} sectionId - Active section ID
|
||||
*/
|
||||
function updateSidebarActive(sectionId) {
|
||||
document.querySelectorAll('.sidebar-item').forEach(item => {
|
||||
const isActive = item.dataset.section === sectionId;
|
||||
item.classList.toggle('active', isActive);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Switch sidebar tab
|
||||
* @param {number} tabIndex - Tab index
|
||||
*/
|
||||
export function switchSidebarTab(tabIndex) {
|
||||
console.log(`🔄 Mudando para aba: ${tabIndex}`);
|
||||
|
||||
// Update state
|
||||
updateState('currentSidebarTab', tabIndex);
|
||||
|
||||
// Update tab buttons
|
||||
document.querySelectorAll('.sidebar-tab').forEach((tab, i) => {
|
||||
tab.classList.toggle('active', i === tabIndex);
|
||||
});
|
||||
|
||||
// Update tab content
|
||||
document.querySelectorAll('.sidebar-content').forEach((content, i) => {
|
||||
content.classList.toggle('active', i === tabIndex);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Switch tab within a section
|
||||
* @param {number} tabIndex - Tab index
|
||||
*/
|
||||
export function switchTab(tabIndex) {
|
||||
document.querySelectorAll('.tab-btn').forEach((btn, i) => {
|
||||
btn.classList.toggle('active', i === tabIndex);
|
||||
});
|
||||
document.querySelectorAll('.tab-content').forEach((content, i) => {
|
||||
content.classList.toggle('active', i === tabIndex);
|
||||
});
|
||||
|
||||
// Update help button for the active tab (for parafusos section)
|
||||
const tabIds = ['parafusos-cisalhamento', 'parafusos-esmagamento', 'parafusos-bloco', 'layout', 'parafuso-vs-solda'];
|
||||
if (tabIds[tabIndex] && typeof window.addHelpButton === 'function') {
|
||||
window.addHelpButton(tabIds[tabIndex]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Switch welding tab
|
||||
* @param {number} index - Tab index
|
||||
*/
|
||||
export function switchWeldTab(index) {
|
||||
document.querySelectorAll('.tabs-nav .tab-btn').forEach((btn, i) => {
|
||||
if (btn.textContent.includes('Pré-Aquecimento') || btn.textContent.includes('Filete') ||
|
||||
btn.textContent.includes('Energia') || btn.textContent.includes('Consumo') ||
|
||||
btn.textContent.includes('Sequência') || btn.textContent.includes('Padrões')) {
|
||||
btn.classList.toggle('active', i === index);
|
||||
}
|
||||
});
|
||||
|
||||
for (let i = 0; i < 6; i++) {
|
||||
const tab = document.getElementById(`weld-tab-${i}`);
|
||||
if (tab) {
|
||||
tab.classList.toggle('active', i === index);
|
||||
}
|
||||
}
|
||||
|
||||
// Update help button for the active tab
|
||||
const tabIds = ['preaquecimento', 'filete', 'energia', 'consumo-eletrodo', 'sequencia-soldagem', 'padroes-soldagem'];
|
||||
if (typeof window.addHelpButton === 'function') {
|
||||
window.addHelpButton(tabIds[index]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Navigate to a specific tool/tab (used by global search)
|
||||
* @param {string} toolId - Tool identifier
|
||||
* @param {number|null} tabIndex - Optional tab index
|
||||
*/
|
||||
export async function navegarParaFerramenta(toolId, tabIndex) {
|
||||
// Close search modal if open
|
||||
if (typeof window.closeGlobalSearchModal === 'function') {
|
||||
window.closeGlobalSearchModal();
|
||||
}
|
||||
|
||||
// Determine which section to show
|
||||
let sectionId = toolId;
|
||||
|
||||
// Handle tabs within sections
|
||||
if (toolId.startsWith('parafusos-')) {
|
||||
sectionId = 'parafusos';
|
||||
} else if (['filete', 'energia', 'consumo-eletrodo', 'sequencia-soldagem', 'padroes-soldagem'].includes(toolId)) {
|
||||
sectionId = 'preaquecimento';
|
||||
}
|
||||
|
||||
// Show the section
|
||||
await showSection(sectionId);
|
||||
|
||||
// Switch to specific tab if needed
|
||||
setTimeout(() => {
|
||||
if (tabIndex !== null && tabIndex !== undefined) {
|
||||
if (sectionId === 'parafusos') {
|
||||
switchTab(tabIndex);
|
||||
} else if (sectionId === 'preaquecimento') {
|
||||
switchWeldTab(tabIndex);
|
||||
}
|
||||
}
|
||||
|
||||
// Scroll to top
|
||||
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||||
}, 200);
|
||||
}
|
||||
|
||||
// Export to global scope for compatibility
|
||||
if (typeof window !== 'undefined') {
|
||||
window.showSection = showSection;
|
||||
window.switchSidebarTab = switchSidebarTab;
|
||||
window.switchTab = switchTab;
|
||||
window.switchWeldTab = switchWeldTab;
|
||||
window.navegarParaFerramenta = navegarParaFerramenta;
|
||||
}
|
||||
238
js/ui/section-loader.js
Normal file
238
js/ui/section-loader.js
Normal file
@@ -0,0 +1,238 @@
|
||||
/**
|
||||
* Section Loader - Dynamic content loading with lazy loading
|
||||
* Loads sections on-demand to improve initial load time
|
||||
*/
|
||||
|
||||
// Cache for loaded sections
|
||||
const sectionCache = new Map();
|
||||
|
||||
// Loading state
|
||||
let isLoading = false;
|
||||
|
||||
/**
|
||||
* Load section content dynamically
|
||||
* @param {string} sectionId - Section identifier
|
||||
* @returns {Promise<string>} Section HTML content
|
||||
*/
|
||||
export async function loadSection(sectionId) {
|
||||
// Check cache first
|
||||
if (sectionCache.has(sectionId)) {
|
||||
console.log(`📦 Carregando ${sectionId} do cache`);
|
||||
return sectionCache.get(sectionId);
|
||||
}
|
||||
|
||||
// Prevent concurrent loads
|
||||
if (isLoading) {
|
||||
console.warn('⚠️ Já está carregando uma seção');
|
||||
return '';
|
||||
}
|
||||
|
||||
isLoading = true;
|
||||
|
||||
try {
|
||||
console.log(`⏳ Carregando seção: ${sectionId}`);
|
||||
|
||||
// Map section to content function
|
||||
const content = await getSectionContent(sectionId);
|
||||
|
||||
// Cache the content
|
||||
sectionCache.set(sectionId, content);
|
||||
|
||||
console.log(`✅ Seção ${sectionId} carregada`);
|
||||
return content;
|
||||
|
||||
} catch (error) {
|
||||
console.error(`❌ Erro ao carregar seção ${sectionId}:`, error);
|
||||
return `<div class="error-message">Erro ao carregar seção. Tente novamente.</div>`;
|
||||
} finally {
|
||||
isLoading = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get section content from global functions
|
||||
* This maintains compatibility with existing code
|
||||
* @param {string} sectionId - Section identifier
|
||||
* @returns {Promise<string>} Section HTML
|
||||
*/
|
||||
async function getSectionContent(sectionId) {
|
||||
// Map of section IDs to their content functions
|
||||
const sectionMap = {
|
||||
// MATERIAIS - Aços Estruturais
|
||||
'cev': 'getCEVContent',
|
||||
'seletor': 'getSeletorContent',
|
||||
'equivalencias': 'getEquivalenciasContent',
|
||||
'comparativo': 'getComparativoContent',
|
||||
'assistente-inteligente': 'getAssistenteInteligenteContent',
|
||||
|
||||
// MATERIAIS - Consumíveis de Soldagem
|
||||
'eletrodos': 'getEletrodosContent',
|
||||
'arames': 'getAramesContent',
|
||||
'fluxos': 'getFluxosContent',
|
||||
'gases': 'getGasesContent',
|
||||
|
||||
// MATERIAIS - Fixadores
|
||||
'parafusos-catalogo': 'getParafusosCatalogoContent',
|
||||
'porcas': 'getPorcasContent',
|
||||
'arruelas': 'getArruelasContent',
|
||||
'chumbadores': 'getChumbadoresContent',
|
||||
|
||||
// MATERIAIS - Tintas e Revestimentos
|
||||
'tintas-catalogo': 'getTintasCatalogoContent',
|
||||
'sistemas-pintura': 'getSistemasPinturaContent',
|
||||
'abrasivos': 'getAbrasivosContent',
|
||||
'granalha': 'getGranalhaContent',
|
||||
|
||||
// MATERIAIS - Elementos Complementares
|
||||
'telhas': 'getTelhasContent',
|
||||
'paineis': 'getPaineisContent',
|
||||
'steel-deck': 'getSteelDeckContent',
|
||||
'perfis-formados': 'getPerfisFormadosContent',
|
||||
|
||||
// MATERIAIS - Catálogo de Perfis (novos)
|
||||
'cantoneiras': 'getCantoneirasContent',
|
||||
'barras-redondas': 'getBarrasRedondasContent',
|
||||
'tubos-circulares': 'getTubosCircularesContent',
|
||||
'perfis-i': 'getPerfisIContent',
|
||||
'perfis-w': 'getPerfisWContent',
|
||||
'tubos-rhs': 'getTubosRHSContent',
|
||||
'chapas': 'getChapasContent',
|
||||
'perfis-hp': 'getPerfisHPContent',
|
||||
'barras-roscadas': 'getBarrasRoscadasContent',
|
||||
'barras-chatas': 'getBarrasChatasContent',
|
||||
|
||||
// CONEXÕES
|
||||
'parafusos': 'getParafusosContent',
|
||||
'layout': 'getLayoutContent',
|
||||
'parafuso-vs-solda': 'getParafusoVsSoldaContent',
|
||||
|
||||
// SOLDAGEM
|
||||
'preaquecimento': 'getPreaquecimentoContent',
|
||||
'solda-filete': 'getSoldaFileteContent',
|
||||
'energia-soldagem': 'getEnergiaSoldagemContent',
|
||||
'consumo-eletrodos': 'getConsumoEletrodosContent',
|
||||
|
||||
// ENSAIOS
|
||||
'dureza': 'getDurezaContent',
|
||||
'charpy': 'getCharpyContent',
|
||||
'certificado': 'getCertificadoContent',
|
||||
'ultrassom': 'getUltrassomContent',
|
||||
|
||||
// PINTURA
|
||||
'area-pintura': 'getAreaPinturaContent',
|
||||
'consumo-tinta': 'getConsumoTintaContent',
|
||||
'galvanizacao': 'getGalvanizacaoContent',
|
||||
'custo-pintura': 'getCustoPinturaContent',
|
||||
'secagem': 'getSecagemContent',
|
||||
'inspecao-pintura': 'getInspecaoPinturaContent',
|
||||
|
||||
// ORÇAMENTO
|
||||
'orcamento': 'getOrcamentoContent',
|
||||
'peso-rigging': 'getPesoRiggingContent',
|
||||
'referencia': 'getReferenciaContent'
|
||||
};
|
||||
|
||||
const functionName = sectionMap[sectionId];
|
||||
|
||||
if (!functionName) {
|
||||
throw new Error(`Seção não encontrada: ${sectionId}`);
|
||||
}
|
||||
|
||||
// Check if function exists in global scope
|
||||
if (typeof window[functionName] !== 'function') {
|
||||
throw new Error(`Função não encontrada: ${functionName}`);
|
||||
}
|
||||
|
||||
// Call the function and return content
|
||||
return window[functionName]();
|
||||
}
|
||||
|
||||
/**
|
||||
* Preload sections for faster navigation
|
||||
* @param {string[]} sectionIds - Array of section IDs to preload
|
||||
*/
|
||||
export async function preloadSections(sectionIds) {
|
||||
console.log('🔄 Pré-carregando seções:', sectionIds);
|
||||
|
||||
const promises = sectionIds.map(id => loadSection(id));
|
||||
|
||||
try {
|
||||
await Promise.all(promises);
|
||||
console.log('✅ Seções pré-carregadas');
|
||||
} catch (error) {
|
||||
console.warn('⚠️ Erro ao pré-carregar seções:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear section cache
|
||||
* @param {string} sectionId - Optional section ID to clear, or all if not provided
|
||||
*/
|
||||
export function clearCache(sectionId = null) {
|
||||
if (sectionId) {
|
||||
sectionCache.delete(sectionId);
|
||||
console.log(`🗑️ Cache limpo: ${sectionId}`);
|
||||
} else {
|
||||
sectionCache.clear();
|
||||
console.log('🗑️ Todo cache limpo');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get cache statistics
|
||||
* @returns {object} Cache stats
|
||||
*/
|
||||
export function getCacheStats() {
|
||||
return {
|
||||
size: sectionCache.size,
|
||||
sections: Array.from(sectionCache.keys()),
|
||||
memoryEstimate: estimateCacheSize()
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Estimate cache size in bytes
|
||||
* @returns {number} Estimated size
|
||||
*/
|
||||
function estimateCacheSize() {
|
||||
let size = 0;
|
||||
for (const content of sectionCache.values()) {
|
||||
size += new Blob([content]).size;
|
||||
}
|
||||
return size;
|
||||
}
|
||||
|
||||
/**
|
||||
* Show loading indicator
|
||||
*/
|
||||
export function showLoading() {
|
||||
const content = document.getElementById('main-content');
|
||||
if (content) {
|
||||
content.innerHTML = `
|
||||
<div style="display: flex; align-items: center; justify-content: center; min-height: 400px; flex-direction: column; gap: 16px;">
|
||||
<div style="font-size: 48px;">⏳</div>
|
||||
<div style="font-size: 18px; color: var(--color-text-secondary);">Carregando...</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Show error message
|
||||
* @param {string} message - Error message
|
||||
*/
|
||||
export function showError(message) {
|
||||
const content = document.getElementById('main-content');
|
||||
if (content) {
|
||||
content.innerHTML = `
|
||||
<div class="card" style="background: var(--color-bg-4); border-color: var(--color-error);">
|
||||
<div style="display: flex; align-items: center; gap: 12px; margin-bottom: 12px;">
|
||||
<span style="font-size: 32px;">❌</span>
|
||||
<h3 style="margin: 0; color: var(--color-error);">Erro</h3>
|
||||
</div>
|
||||
<p>${message}</p>
|
||||
<button class="btn btn-primary" onclick="location.reload()">Recarregar Página</button>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
129
js/ui/theme.js
Normal file
129
js/ui/theme.js
Normal file
@@ -0,0 +1,129 @@
|
||||
/**
|
||||
* Theme Management
|
||||
* Handles dark/light mode and visual customization
|
||||
*/
|
||||
|
||||
import { appState, userPreferences, updateState, updatePreference } from '../core/state.js';
|
||||
import { savePreferences } from '../core/storage.js';
|
||||
|
||||
/**
|
||||
* Toggle between dark and light theme
|
||||
*/
|
||||
export function toggleTheme() {
|
||||
const newTheme = appState.currentTheme === 'dark' ? 'light' : 'dark';
|
||||
updateState('currentTheme', newTheme);
|
||||
updatePreference('theme', newTheme);
|
||||
savePreferences();
|
||||
applyTheme();
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply current theme to document
|
||||
*/
|
||||
export function applyTheme() {
|
||||
const theme = appState.currentTheme;
|
||||
|
||||
// Apply theme attributes
|
||||
document.documentElement.setAttribute('data-theme', theme);
|
||||
document.documentElement.setAttribute('data-color-scheme', theme);
|
||||
|
||||
// Update theme toggle button
|
||||
const btn = document.getElementById('theme-toggle');
|
||||
if (btn) {
|
||||
btn.textContent = theme === 'dark' ? '☀️ Claro' : '🌙 Escuro';
|
||||
btn.classList.toggle('light', theme === 'light');
|
||||
}
|
||||
|
||||
console.log(`🎨 Tema aplicado: ${theme}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply all user preferences (theme, colors, fonts)
|
||||
*/
|
||||
export function applyUserPreferences() {
|
||||
// Apply theme
|
||||
updateState('currentTheme', userPreferences.theme);
|
||||
applyTheme();
|
||||
|
||||
// Apply color scheme variant
|
||||
document.documentElement.setAttribute('data-color-scheme-variant', userPreferences.colorScheme);
|
||||
|
||||
// Apply font size
|
||||
document.documentElement.setAttribute('data-font-size', userPreferences.fontSize);
|
||||
|
||||
// Apply font family
|
||||
document.documentElement.setAttribute('data-font-family', userPreferences.fontFamily);
|
||||
|
||||
console.log('🎨 Preferências aplicadas:', userPreferences);
|
||||
}
|
||||
|
||||
/**
|
||||
* Preview color scheme (used in admin panel)
|
||||
* @param {string} scheme - Color scheme name
|
||||
*/
|
||||
export function previewColorScheme(scheme) {
|
||||
updatePreference('colorScheme', scheme);
|
||||
applyUserPreferences();
|
||||
savePreferences();
|
||||
}
|
||||
|
||||
/**
|
||||
* Preview font size (used in admin panel)
|
||||
* @param {string} size - Font size name
|
||||
*/
|
||||
export function previewFontSize(size) {
|
||||
updatePreference('fontSize', size);
|
||||
applyUserPreferences();
|
||||
savePreferences();
|
||||
}
|
||||
|
||||
/**
|
||||
* Preview font family (used in admin panel)
|
||||
* @param {string} family - Font family name
|
||||
*/
|
||||
export function previewFontFamily(family) {
|
||||
updatePreference('fontFamily', family);
|
||||
applyUserPreferences();
|
||||
savePreferences();
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle expert mode
|
||||
*/
|
||||
export function toggleExpertMode() {
|
||||
const newMode = !appState.expertMode;
|
||||
updateState('expertMode', newMode);
|
||||
|
||||
document.documentElement.classList.toggle('expert-mode', newMode);
|
||||
|
||||
// Update header toggle if it exists (legacy)
|
||||
const headerBtn = document.getElementById('expert-toggle');
|
||||
if (headerBtn) {
|
||||
headerBtn.textContent = newMode ? '🔬 Expert' : '🎯 Simples';
|
||||
headerBtn.classList.toggle('active', newMode);
|
||||
}
|
||||
// Update Admin panel toggle if present
|
||||
const adminBtn = document.getElementById('adminExpertToggle');
|
||||
if (adminBtn) {
|
||||
adminBtn.classList.toggle('active', newMode);
|
||||
adminBtn.textContent = newMode ? '🔬 Expert Ativo' : '🎯 Alternar Expert';
|
||||
}
|
||||
|
||||
// Filter tools by mode
|
||||
if (typeof window.filterToolsByMode === 'function') {
|
||||
window.filterToolsByMode();
|
||||
}
|
||||
|
||||
console.log(`🎯 Modo expert: ${newMode ? 'ativado' : 'desativado'}`);
|
||||
}
|
||||
|
||||
// Export to global scope for compatibility
|
||||
if (typeof window !== 'undefined') {
|
||||
window.toggleTheme = toggleTheme;
|
||||
window.applyTheme = applyTheme;
|
||||
window.applyUserPreferences = applyUserPreferences;
|
||||
window.previewColorScheme = previewColorScheme;
|
||||
window.previewFontSize = previewFontSize;
|
||||
window.previewFontFamily = previewFontFamily;
|
||||
window.toggleExpertMode = toggleExpertMode;
|
||||
}
|
||||
571
js/ui/toast-manager.js
Normal file
571
js/ui/toast-manager.js
Normal file
@@ -0,0 +1,571 @@
|
||||
/**
|
||||
* ToastManager - Sistema de Notificações Visuais
|
||||
* Responsável por exibir mensagens de feedback ao usuário de forma elegante e não-intrusiva
|
||||
*/
|
||||
class ToastManager {
|
||||
constructor() {
|
||||
this.container = null;
|
||||
this.toasts = new Map();
|
||||
this.config = {
|
||||
position: 'top-right',
|
||||
maxToasts: 5,
|
||||
duration: {
|
||||
success: 3000,
|
||||
error: 5000,
|
||||
warning: 4000,
|
||||
info: 3000
|
||||
}
|
||||
};
|
||||
|
||||
this.init();
|
||||
}
|
||||
|
||||
/**
|
||||
* Inicializa o ToastManager criando o container
|
||||
*/
|
||||
init() {
|
||||
this.createContainer();
|
||||
console.log('🍞 ToastManager inicializado');
|
||||
}
|
||||
|
||||
/**
|
||||
* Cria o container de toasts no DOM
|
||||
*/
|
||||
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}`;
|
||||
|
||||
// Adicionar estilos CSS
|
||||
this.addStyles();
|
||||
|
||||
// Adicionar ao body
|
||||
document.body.appendChild(this.container);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adiciona estilos CSS para os toasts
|
||||
*/
|
||||
addStyles() {
|
||||
// Verificar se os estilos já existem
|
||||
if (document.querySelector('#toast-styles')) {
|
||||
return;
|
||||
}
|
||||
|
||||
const styles = document.createElement('style');
|
||||
styles.id = 'toast-styles';
|
||||
styles.textContent = `
|
||||
.toast-container {
|
||||
position: fixed;
|
||||
z-index: 10000;
|
||||
pointer-events: none;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
max-width: 400px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.toast-top-right {
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
}
|
||||
|
||||
.toast-top-left {
|
||||
top: 20px;
|
||||
left: 20px;
|
||||
}
|
||||
|
||||
.toast-bottom-right {
|
||||
bottom: 20px;
|
||||
right: 20px;
|
||||
}
|
||||
|
||||
.toast-bottom-left {
|
||||
bottom: 20px;
|
||||
left: 20px;
|
||||
}
|
||||
|
||||
.toast-top-center {
|
||||
top: 20px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
|
||||
.toast-bottom-center {
|
||||
bottom: 20px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
|
||||
.toast {
|
||||
background: var(--color-bg-2);
|
||||
color: var(--color-text);
|
||||
border-radius: 8px;
|
||||
padding: 16px 20px;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||
border-left: 4px solid var(--color-primary);
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 12px;
|
||||
animation: toast-in 0.3s ease-out;
|
||||
pointer-events: auto;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.toast-success {
|
||||
border-left-color: #10b981;
|
||||
background: linear-gradient(135deg, #10b98120 0%, transparent 100%);
|
||||
}
|
||||
|
||||
.toast-error {
|
||||
border-left-color: #ef4444;
|
||||
background: linear-gradient(135deg, #ef444420 0%, transparent 100%);
|
||||
}
|
||||
|
||||
.toast-warning {
|
||||
border-left-color: #f59e0b;
|
||||
background: linear-gradient(135deg, #f59e0b20 0%, transparent 100%);
|
||||
}
|
||||
|
||||
.toast-info {
|
||||
border-left-color: #3b82f6;
|
||||
background: linear-gradient(135deg, #3b82f620 0%, transparent 100%);
|
||||
}
|
||||
|
||||
.toast-icon {
|
||||
flex-shrink: 0;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.toast-content {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.toast-title {
|
||||
font-weight: 600;
|
||||
margin-bottom: 4px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.toast-message {
|
||||
font-size: 13px;
|
||||
line-height: 1.4;
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.toast-close {
|
||||
flex-shrink: 0;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: var(--color-text);
|
||||
cursor: pointer;
|
||||
opacity: 0.6;
|
||||
transition: opacity 0.2s;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 16px;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.toast-close:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.toast-progress {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
height: 2px;
|
||||
background: var(--color-primary);
|
||||
opacity: 0.3;
|
||||
animation: toast-progress linear forwards;
|
||||
}
|
||||
|
||||
.toast-success .toast-progress {
|
||||
background: #10b981;
|
||||
}
|
||||
|
||||
.toast-error .toast-progress {
|
||||
background: #ef4444;
|
||||
}
|
||||
|
||||
.toast-warning .toast-progress {
|
||||
background: #f59e0b;
|
||||
}
|
||||
|
||||
.toast-info .toast-progress {
|
||||
background: #3b82f6;
|
||||
}
|
||||
|
||||
.toast.removing {
|
||||
animation: toast-out 0.3s ease-in forwards;
|
||||
}
|
||||
|
||||
@keyframes toast-in {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateX(100%);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateX(0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes toast-out {
|
||||
from {
|
||||
opacity: 1;
|
||||
transform: translateX(0);
|
||||
}
|
||||
to {
|
||||
opacity: 0;
|
||||
transform: translateX(100%);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes toast-progress {
|
||||
from {
|
||||
width: 100%;
|
||||
}
|
||||
to {
|
||||
width: 0%;
|
||||
}
|
||||
}
|
||||
|
||||
/* Responsividade */
|
||||
@media (max-width: 480px) {
|
||||
.toast-container {
|
||||
left: 10px !important;
|
||||
right: 10px !important;
|
||||
max-width: none;
|
||||
}
|
||||
|
||||
.toast {
|
||||
padding: 14px 16px;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
document.head.appendChild(styles);
|
||||
}
|
||||
|
||||
/**
|
||||
* Mostra um toast genérico
|
||||
* @param {string} message - Mensagem a exibir
|
||||
* @param {string} type - Tipo do toast (success, error, warning, info)
|
||||
* @param {number} duration - Duração em milissegundos
|
||||
* @param {Object} options - Opções adicionais
|
||||
*/
|
||||
show(message, type = 'info', duration = null, options = {}) {
|
||||
// Limitar número de toasts simultâneos
|
||||
if (this.toasts.size >= this.config.maxToasts) {
|
||||
// Remover o toast mais antigo
|
||||
const oldestToast = this.toasts.values().next().value;
|
||||
if (oldestToast) {
|
||||
this.removeToast(oldestToast.id);
|
||||
}
|
||||
}
|
||||
|
||||
const toastId = `toast-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
||||
const toastDuration = duration || this.config.duration[type] || 3000;
|
||||
|
||||
const toast = this.createToastElement(toastId, message, type, options);
|
||||
this.container.appendChild(toast);
|
||||
|
||||
const toastData = {
|
||||
id: toastId,
|
||||
element: toast,
|
||||
type: type,
|
||||
message: message,
|
||||
startTime: Date.now(),
|
||||
duration: toastDuration
|
||||
};
|
||||
|
||||
this.toasts.set(toastId, toastData);
|
||||
|
||||
// Configurar remoção automática
|
||||
if (toastDuration > 0) {
|
||||
setTimeout(() => {
|
||||
this.removeToast(toastId);
|
||||
}, toastDuration);
|
||||
|
||||
// Configurar barra de progresso
|
||||
const progressBar = toast.querySelector('.toast-progress');
|
||||
if (progressBar) {
|
||||
progressBar.style.animationDuration = `${toastDuration}ms`;
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`🍞 Toast ${type}: ${message}`);
|
||||
|
||||
return toastId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Cria o elemento HTML do toast
|
||||
* @param {string} id - ID do toast
|
||||
* @param {string} message - Mensagem
|
||||
* @param {string} type - Tipo do toast
|
||||
* @param {Object} options - Opções adicionais
|
||||
* @returns {HTMLElement} Elemento do toast
|
||||
*/
|
||||
createToastElement(id, message, type, options = {}) {
|
||||
const toast = document.createElement('div');
|
||||
toast.className = `toast toast-${type}`;
|
||||
toast.id = id;
|
||||
|
||||
// Ícone baseado no tipo
|
||||
const icons = {
|
||||
success: '✅',
|
||||
error: '❌',
|
||||
warning: '⚠️',
|
||||
info: 'ℹ️'
|
||||
};
|
||||
|
||||
const icon = options.icon || icons[type] || '💬';
|
||||
const title = options.title || this.getTitleByType(type);
|
||||
|
||||
toast.innerHTML = `
|
||||
<div class="toast-icon">${icon}</div>
|
||||
<div class="toast-content">
|
||||
${title ? `<div class="toast-title">${title}</div>` : ''}
|
||||
<div class="toast-message">${this.escapeHtml(message)}</div>
|
||||
</div>
|
||||
<button class="toast-close" onclick="window.toastManager.removeToast('${id}')">✕</button>
|
||||
<div class="toast-progress"></div>
|
||||
`;
|
||||
|
||||
// Adicionar eventos de hover
|
||||
toast.addEventListener('mouseenter', () => {
|
||||
const progressBar = toast.querySelector('.toast-progress');
|
||||
if (progressBar) {
|
||||
progressBar.style.animationPlayState = 'paused';
|
||||
}
|
||||
});
|
||||
|
||||
toast.addEventListener('mouseleave', () => {
|
||||
const progressBar = toast.querySelector('.toast-progress');
|
||||
if (progressBar) {
|
||||
progressBar.style.animationPlayState = 'running';
|
||||
}
|
||||
});
|
||||
|
||||
// Permitir clique para fechar (exceto no botão de fechar)
|
||||
toast.addEventListener('click', (e) => {
|
||||
if (!e.target.classList.contains('toast-close')) {
|
||||
this.removeToast(id);
|
||||
}
|
||||
});
|
||||
|
||||
return toast;
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtém título baseado no tipo
|
||||
* @param {string} type - Tipo do toast
|
||||
* @returns {string} Título
|
||||
*/
|
||||
getTitleByType(type) {
|
||||
const titles = {
|
||||
success: 'Sucesso!',
|
||||
error: 'Erro!',
|
||||
warning: 'Atenção!',
|
||||
info: 'Informação'
|
||||
};
|
||||
|
||||
return titles[type] || '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove um toast específico
|
||||
* @param {string} toastId - ID do toast a remover
|
||||
*/
|
||||
removeToast(toastId) {
|
||||
const toastData = this.toasts.get(toastId);
|
||||
if (!toastData) {
|
||||
return;
|
||||
}
|
||||
|
||||
const toast = toastData.element;
|
||||
toast.classList.add('removing');
|
||||
|
||||
// Remover após animação
|
||||
setTimeout(() => {
|
||||
if (toast.parentNode) {
|
||||
toast.parentNode.removeChild(toast);
|
||||
}
|
||||
this.toasts.delete(toastId);
|
||||
}, 300);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove todos os toasts ativos
|
||||
*/
|
||||
removeAllToasts() {
|
||||
const toastIds = Array.from(this.toasts.keys());
|
||||
toastIds.forEach(id => this.removeToast(id));
|
||||
}
|
||||
|
||||
/**
|
||||
* Escapa HTML para prevenir XSS
|
||||
* @param {string} text - Texto a escapar
|
||||
* @returns {string} Texto escapado
|
||||
*/
|
||||
escapeHtml(text) {
|
||||
const div = document.createElement('div');
|
||||
div.textContent = text;
|
||||
return div.innerHTML;
|
||||
}
|
||||
|
||||
// Métodos de conveniência
|
||||
|
||||
/**
|
||||
* Mostra um toast de sucesso
|
||||
* @param {string} message - Mensagem
|
||||
* @param {number} duration - Duração em milissegundos
|
||||
* @param {Object} options - Opções adicionais
|
||||
*/
|
||||
success(message, duration = null, options = {}) {
|
||||
return this.show(message, 'success', duration, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Mostra um toast de erro
|
||||
* @param {string} message - Mensagem
|
||||
* @param {number} duration - Duração em milissegundos
|
||||
* @param {Object} options - Opções adicionais
|
||||
*/
|
||||
error(message, duration = null, options = {}) {
|
||||
return this.show(message, 'error', duration, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Mostra um toast de aviso
|
||||
* @param {string} message - Mensagem
|
||||
* @param {number} duration - Duração em milissegundos
|
||||
* @param {Object} options - Opções adicionais
|
||||
*/
|
||||
warning(message, duration = null, options = {}) {
|
||||
return this.show(message, 'warning', duration, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Mostra um toast de informação
|
||||
* @param {string} message - Mensagem
|
||||
* @param {number} duration - Duração em milissegundos
|
||||
* @param {Object} options - Opções adicionais
|
||||
*/
|
||||
info(message, duration = null, options = {}) {
|
||||
return this.show(message, 'info', duration, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Mostra um toast de loading (persistente)
|
||||
* @param {string} message - Mensagem
|
||||
* @param {Object} options - Opções adicionais
|
||||
*/
|
||||
loading(message = 'Carregando...', options = {}) {
|
||||
const toastId = this.show(message, 'info', 0, {
|
||||
icon: '⏳',
|
||||
title: 'Aguarde...',
|
||||
...options
|
||||
});
|
||||
|
||||
// Remover barra de progresso para loading persistente
|
||||
const toast = this.toasts.get(toastId);
|
||||
if (toast) {
|
||||
const progressBar = toast.element.querySelector('.toast-progress');
|
||||
if (progressBar) {
|
||||
progressBar.remove();
|
||||
}
|
||||
}
|
||||
|
||||
return toastId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Atualiza o conteúdo de um toast existente
|
||||
* @param {string} toastId - ID do toast
|
||||
* @param {string} message - Nova mensagem
|
||||
* @param {string} type - Novo tipo (opcional)
|
||||
*/
|
||||
updateToast(toastId, message, type = null) {
|
||||
const toastData = this.toasts.get(toastId);
|
||||
if (!toastData) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const toast = toastData.element;
|
||||
const messageElement = toast.querySelector('.toast-message');
|
||||
|
||||
if (messageElement) {
|
||||
messageElement.textContent = message;
|
||||
}
|
||||
|
||||
if (type && type !== toastData.type) {
|
||||
toast.className = toast.className.replace(`toast-${toastData.type}`, `toast-${type}`);
|
||||
toastData.type = type;
|
||||
|
||||
// Atualizar ícone
|
||||
const iconElement = toast.querySelector('.toast-icon');
|
||||
if (iconElement) {
|
||||
const icons = {
|
||||
success: '✅',
|
||||
error: '❌',
|
||||
warning: '⚠️',
|
||||
info: 'ℹ️'
|
||||
};
|
||||
iconElement.textContent = icons[type] || '💬';
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configura o ToastManager
|
||||
* @param {Object} config - Configurações
|
||||
*/
|
||||
setConfig(config) {
|
||||
this.config = { ...this.config, ...config };
|
||||
|
||||
// Recriar container se a posição mudou
|
||||
if (config.position) {
|
||||
this.createContainer();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtém estatísticas dos toasts
|
||||
* @returns {Object} Estatísticas
|
||||
*/
|
||||
getStats() {
|
||||
return {
|
||||
activeToasts: this.toasts.size,
|
||||
maxToasts: this.config.maxToasts,
|
||||
position: this.config.position
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Criar instância global
|
||||
window.toastManager = new ToastManager();
|
||||
|
||||
console.log('🍞 ToastManager inicializado com sucesso');
|
||||
230
js/utils/csv-manager.js
Normal file
230
js/utils/csv-manager.js
Normal file
@@ -0,0 +1,230 @@
|
||||
/**
|
||||
* CSV Manager - CRUD operations for CSV files
|
||||
* Handles reading, parsing, editing, and saving CSV data
|
||||
*/
|
||||
|
||||
/**
|
||||
* Parse CSV text to array of objects
|
||||
* @param {string} csvText - CSV content
|
||||
* @returns {Array<object>} Parsed data
|
||||
*/
|
||||
export function parseCSV(csvText) {
|
||||
const lines = csvText.trim().split('\n');
|
||||
if (lines.length === 0) return [];
|
||||
|
||||
// Get headers
|
||||
const headers = lines[0].split(',').map(h => h.trim());
|
||||
|
||||
// Parse rows
|
||||
const data = [];
|
||||
for (let i = 1; i < lines.length; i++) {
|
||||
const values = lines[i].split(',').map(v => v.trim());
|
||||
const row = {};
|
||||
headers.forEach((header, index) => {
|
||||
row[header] = values[index] || '';
|
||||
});
|
||||
data.push(row);
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert array of objects to CSV text
|
||||
* @param {Array<object>} data - Data array
|
||||
* @returns {string} CSV text
|
||||
*/
|
||||
export function toCSV(data) {
|
||||
if (data.length === 0) return '';
|
||||
|
||||
// Get headers from first object
|
||||
const headers = Object.keys(data[0]);
|
||||
|
||||
// Create CSV lines
|
||||
const lines = [headers.join(',')];
|
||||
|
||||
data.forEach(row => {
|
||||
const values = headers.map(header => {
|
||||
const value = row[header] || '';
|
||||
// Escape commas and quotes
|
||||
if (value.includes(',') || value.includes('"')) {
|
||||
return `"${value.replace(/"/g, '""')}"`;
|
||||
}
|
||||
return value;
|
||||
});
|
||||
lines.push(values.join(','));
|
||||
});
|
||||
|
||||
return lines.join('\n');
|
||||
}
|
||||
|
||||
/**
|
||||
* Load CSV file
|
||||
* @param {string} filename - CSV filename
|
||||
* @returns {Promise<Array<object>>} Parsed data
|
||||
*/
|
||||
export async function loadCSV(filename) {
|
||||
try {
|
||||
const response = await fetch(`BD/${filename}`);
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
const text = await response.text();
|
||||
return parseCSV(text);
|
||||
} catch (error) {
|
||||
console.error(`Erro ao carregar ${filename}:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Download CSV file
|
||||
* @param {string} filename - Filename
|
||||
* @param {string} csvText - CSV content
|
||||
*/
|
||||
export function downloadCSV(filename, csvText) {
|
||||
const blob = new Blob([csvText], { type: 'text/csv;charset=utf-8;' });
|
||||
const link = document.createElement('a');
|
||||
const url = URL.createObjectURL(blob);
|
||||
|
||||
link.setAttribute('href', url);
|
||||
link.setAttribute('download', filename);
|
||||
link.style.visibility = 'hidden';
|
||||
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get available CSV files
|
||||
* @returns {Array<object>} List of CSV files with metadata
|
||||
*/
|
||||
export function getAvailableCSVFiles() {
|
||||
return [
|
||||
{
|
||||
id: 'perfis_w',
|
||||
name: 'Perfis W',
|
||||
filename: 'perfis_w.csv',
|
||||
description: 'Perfis de aço tipo W (vigas)',
|
||||
icon: '🏗️'
|
||||
},
|
||||
{
|
||||
id: 'perfis_i',
|
||||
name: 'Perfis I',
|
||||
filename: 'perfis_i.csv',
|
||||
description: 'Perfis de aço tipo I',
|
||||
icon: '🏗️'
|
||||
},
|
||||
{
|
||||
id: 'cantoneiras',
|
||||
name: 'Cantoneiras',
|
||||
filename: 'cantoneiras.csv',
|
||||
description: 'Cantoneiras de aço',
|
||||
icon: '📐'
|
||||
},
|
||||
{
|
||||
id: 'tubos_circulares',
|
||||
name: 'Tubos Circulares',
|
||||
filename: 'tubos_circulares.csv',
|
||||
description: 'Tubos de seção circular',
|
||||
icon: '⭕'
|
||||
},
|
||||
{
|
||||
id: 'tubos_rhs',
|
||||
name: 'Tubos RHS',
|
||||
filename: 'tubos_rhs.csv',
|
||||
description: 'Tubos retangulares/quadrados',
|
||||
icon: '⬜'
|
||||
},
|
||||
{
|
||||
id: 'chapas',
|
||||
name: 'Chapas',
|
||||
filename: 'chapas.csv',
|
||||
description: 'Chapas de aço',
|
||||
icon: '📄'
|
||||
},
|
||||
{
|
||||
id: 'barras',
|
||||
name: 'Barras',
|
||||
filename: 'barras.csv',
|
||||
description: 'Barras redondas',
|
||||
icon: '➖'
|
||||
},
|
||||
{
|
||||
id: 'eletrodos',
|
||||
name: 'Eletrodos',
|
||||
filename: 'eletrodos.csv',
|
||||
description: 'Eletrodos de soldagem',
|
||||
icon: '⚡'
|
||||
},
|
||||
{
|
||||
id: 'parafusos',
|
||||
name: 'Parafusos',
|
||||
filename: 'parafusos.csv',
|
||||
description: 'Parafusos estruturais',
|
||||
icon: '🔩'
|
||||
},
|
||||
{
|
||||
id: 'tintas',
|
||||
name: 'Tintas',
|
||||
filename: 'tintas.csv',
|
||||
description: 'Tintas e revestimentos',
|
||||
icon: '🎨'
|
||||
},
|
||||
{
|
||||
id: 'acos_soldagem',
|
||||
name: 'Aços - Soldagem',
|
||||
filename: 'Tabela_Acos_Soldagem_Consumiveis.csv',
|
||||
description: 'Relação aços e consumíveis',
|
||||
icon: '🔥'
|
||||
},
|
||||
{
|
||||
id: 'acos_pintura',
|
||||
name: 'Aços - Pintura',
|
||||
filename: 'Tabela_Acos_Pintura_Tintas.csv',
|
||||
description: 'Relação aços e tintas',
|
||||
icon: '🎨'
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate CSV data
|
||||
* @param {Array<object>} data - Data to validate
|
||||
* @returns {object} Validation result
|
||||
*/
|
||||
export function validateCSVData(data) {
|
||||
const errors = [];
|
||||
|
||||
if (!Array.isArray(data) || data.length === 0) {
|
||||
errors.push('Dados vazios ou inválidos');
|
||||
return { valid: false, errors };
|
||||
}
|
||||
|
||||
// Check if all rows have same keys
|
||||
const firstKeys = Object.keys(data[0]).sort();
|
||||
for (let i = 1; i < data.length; i++) {
|
||||
const keys = Object.keys(data[i]).sort();
|
||||
if (JSON.stringify(keys) !== JSON.stringify(firstKeys)) {
|
||||
errors.push(`Linha ${i + 1}: Colunas inconsistentes`);
|
||||
}
|
||||
}
|
||||
|
||||
// Check for empty required fields (id, nome)
|
||||
data.forEach((row, index) => {
|
||||
if (!row.id || row.id.trim() === '') {
|
||||
errors.push(`Linha ${index + 2}: Campo 'id' vazio`);
|
||||
}
|
||||
if (!row.nome || row.nome.trim() === '') {
|
||||
errors.push(`Linha ${index + 2}: Campo 'nome' vazio`);
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
valid: errors.length === 0,
|
||||
errors
|
||||
};
|
||||
}
|
||||
126
js/utils/formatters.js
Normal file
126
js/utils/formatters.js
Normal file
@@ -0,0 +1,126 @@
|
||||
/**
|
||||
* Formatters - Number and text formatting utilities
|
||||
*/
|
||||
|
||||
/**
|
||||
* Format number with decimal places
|
||||
* @param {number} value - Number to format
|
||||
* @param {number} decimals - Number of decimal places
|
||||
* @returns {string} Formatted number
|
||||
*/
|
||||
export function formatNumber(value, decimals = 2) {
|
||||
if (value === null || value === undefined || isNaN(value)) {
|
||||
return '0.00';
|
||||
}
|
||||
return parseFloat(value).toFixed(decimals);
|
||||
}
|
||||
|
||||
/**
|
||||
* Format number as percentage
|
||||
* @param {number} value - Number to format (0-1 or 0-100)
|
||||
* @param {number} decimals - Number of decimal places
|
||||
* @param {boolean} isDecimal - If true, value is 0-1, else 0-100
|
||||
* @returns {string} Formatted percentage
|
||||
*/
|
||||
export function formatPercentage(value, decimals = 1, isDecimal = true) {
|
||||
if (value === null || value === undefined || isNaN(value)) {
|
||||
return '0%';
|
||||
}
|
||||
const percent = isDecimal ? value * 100 : value;
|
||||
return `${percent.toFixed(decimals)}%`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Format number with thousands separator
|
||||
* @param {number} value - Number to format
|
||||
* @param {string} separator - Thousands separator (default: '.')
|
||||
* @returns {string} Formatted number
|
||||
*/
|
||||
export function formatThousands(value, separator = '.') {
|
||||
if (value === null || value === undefined || isNaN(value)) {
|
||||
return '0';
|
||||
}
|
||||
return value.toString().replace(/\B(?=(\d{3})+(?!\d))/g, separator);
|
||||
}
|
||||
|
||||
/**
|
||||
* Format currency (BRL)
|
||||
* @param {number} value - Value to format
|
||||
* @param {boolean} showSymbol - Show R$ symbol
|
||||
* @returns {string} Formatted currency
|
||||
*/
|
||||
export function formatCurrency(value, showSymbol = true) {
|
||||
if (value === null || value === undefined || isNaN(value)) {
|
||||
return showSymbol ? 'R$ 0,00' : '0,00';
|
||||
}
|
||||
const formatted = value.toFixed(2).replace('.', ',').replace(/\B(?=(\d{3})+(?!\d))/g, '.');
|
||||
return showSymbol ? `R$ ${formatted}` : formatted;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse formatted number back to float
|
||||
* @param {string} value - Formatted number string
|
||||
* @returns {number} Parsed number
|
||||
*/
|
||||
export function parseFormattedNumber(value) {
|
||||
if (!value) return 0;
|
||||
// Remove thousands separators and replace comma with dot
|
||||
return parseFloat(value.toString().replace(/\./g, '').replace(',', '.')) || 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Truncate text with ellipsis
|
||||
* @param {string} text - Text to truncate
|
||||
* @param {number} maxLength - Maximum length
|
||||
* @returns {string} Truncated text
|
||||
*/
|
||||
export function truncateText(text, maxLength = 50) {
|
||||
if (!text || text.length <= maxLength) return text;
|
||||
return text.substring(0, maxLength - 3) + '...';
|
||||
}
|
||||
|
||||
/**
|
||||
* Capitalize first letter
|
||||
* @param {string} text - Text to capitalize
|
||||
* @returns {string} Capitalized text
|
||||
*/
|
||||
export function capitalize(text) {
|
||||
if (!text) return '';
|
||||
return text.charAt(0).toUpperCase() + text.slice(1).toLowerCase();
|
||||
}
|
||||
|
||||
/**
|
||||
* Format date to Brazilian format
|
||||
* @param {Date|string} date - Date to format
|
||||
* @returns {string} Formatted date (DD/MM/YYYY)
|
||||
*/
|
||||
export function formatDate(date) {
|
||||
if (!date) return '';
|
||||
const d = new Date(date);
|
||||
if (isNaN(d.getTime())) return '';
|
||||
|
||||
const day = String(d.getDate()).padStart(2, '0');
|
||||
const month = String(d.getMonth() + 1).padStart(2, '0');
|
||||
const year = d.getFullYear();
|
||||
|
||||
return `${day}/${month}/${year}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Format date and time to Brazilian format
|
||||
* @param {Date|string} date - Date to format
|
||||
* @returns {string} Formatted date and time (DD/MM/YYYY HH:MM)
|
||||
*/
|
||||
export function formatDateTime(date) {
|
||||
if (!date) return '';
|
||||
const d = new Date(date);
|
||||
if (isNaN(d.getTime())) return '';
|
||||
|
||||
const day = String(d.getDate()).padStart(2, '0');
|
||||
const month = String(d.getMonth() + 1).padStart(2, '0');
|
||||
const year = d.getFullYear();
|
||||
const hours = String(d.getHours()).padStart(2, '0');
|
||||
const minutes = String(d.getMinutes()).padStart(2, '0');
|
||||
|
||||
return `${day}/${month}/${year} ${hours}:${minutes}`;
|
||||
}
|
||||
272
js/utils/material-relationships.js
Normal file
272
js/utils/material-relationships.js
Normal file
@@ -0,0 +1,272 @@
|
||||
/**
|
||||
* Material Relationships - Cross-reference system
|
||||
* Links steel grades with welding consumables and painting systems
|
||||
*/
|
||||
|
||||
import { loadCSV } from './csv-manager.js';
|
||||
|
||||
// Cache for relationship data
|
||||
let weldingRelations = [];
|
||||
let paintingRelations = [];
|
||||
let isLoaded = false;
|
||||
|
||||
/**
|
||||
* Load all relationship data
|
||||
* @returns {Promise<boolean>} Success status
|
||||
*/
|
||||
export async function loadRelationships() {
|
||||
if (isLoaded) return true;
|
||||
|
||||
try {
|
||||
console.log('📊 Carregando relacionamentos...');
|
||||
|
||||
// Load both CSV files
|
||||
[weldingRelations, paintingRelations] = await Promise.all([
|
||||
loadCSV('Tabela_Acos_Soldagem_Consumiveis.csv'),
|
||||
loadCSV('Tabela_Acos_Pintura_Tintas.csv')
|
||||
]);
|
||||
|
||||
isLoaded = true;
|
||||
console.log(`✅ Relacionamentos carregados: ${weldingRelations.length} soldagem, ${paintingRelations.length} pintura`);
|
||||
return true;
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ Erro ao carregar relacionamentos:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get welding recommendations for a steel grade
|
||||
* @param {string} steelGrade - Steel grade (e.g., "ASTM A36")
|
||||
* @returns {Array<object>} Welding recommendations
|
||||
*/
|
||||
export function getWeldingRecommendations(steelGrade) {
|
||||
if (!isLoaded) {
|
||||
console.warn('⚠️ Relacionamentos não carregados');
|
||||
return [];
|
||||
}
|
||||
|
||||
// Normalize steel grade for comparison
|
||||
const normalized = steelGrade.toUpperCase().trim();
|
||||
|
||||
return weldingRelations.filter(rel => {
|
||||
const relSteel = rel.Aço?.toUpperCase().trim() || '';
|
||||
return relSteel.includes(normalized) || normalized.includes(relSteel);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get painting recommendations for a steel grade and environment
|
||||
* @param {string} steelGrade - Steel grade
|
||||
* @param {string} environment - Corrosive environment (optional)
|
||||
* @returns {Array<object>} Painting recommendations
|
||||
*/
|
||||
export function getPaintingRecommendations(steelGrade, environment = null) {
|
||||
if (!isLoaded) {
|
||||
console.warn('⚠️ Relacionamentos não carregados');
|
||||
return [];
|
||||
}
|
||||
|
||||
const normalized = steelGrade.toUpperCase().trim();
|
||||
|
||||
let results = paintingRelations.filter(rel => {
|
||||
const relSteel = rel.Aço?.toUpperCase().trim() || '';
|
||||
return relSteel.includes(normalized) || normalized.includes(relSteel);
|
||||
});
|
||||
|
||||
// Filter by environment if specified
|
||||
if (environment) {
|
||||
const envNorm = environment.toUpperCase();
|
||||
results = results.filter(rel => {
|
||||
const relEnv = rel.Ambiente_Corrosivo?.toUpperCase() || '';
|
||||
return relEnv.includes(envNorm);
|
||||
});
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get complete recommendation (steel + welding + painting)
|
||||
* @param {string} steelGrade - Steel grade
|
||||
* @param {string} environment - Corrosive environment
|
||||
* @returns {object} Complete recommendation
|
||||
*/
|
||||
export function getCompleteRecommendation(steelGrade, environment = 'C3') {
|
||||
const welding = getWeldingRecommendations(steelGrade);
|
||||
const painting = getPaintingRecommendations(steelGrade, environment);
|
||||
|
||||
return {
|
||||
steel: steelGrade,
|
||||
environment,
|
||||
welding: welding[0] || null,
|
||||
painting: painting[0] || null,
|
||||
hasWelding: welding.length > 0,
|
||||
hasPainting: painting.length > 0,
|
||||
isComplete: welding.length > 0 && painting.length > 0
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all available steel grades
|
||||
* @returns {Array<string>} Unique steel grades
|
||||
*/
|
||||
export function getAvailableSteelGrades() {
|
||||
if (!isLoaded) return [];
|
||||
|
||||
const grades = new Set();
|
||||
|
||||
weldingRelations.forEach(rel => {
|
||||
if (rel.Aço) grades.add(rel.Aço);
|
||||
});
|
||||
|
||||
return Array.from(grades).sort();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get available environments for a steel grade
|
||||
* @param {string} steelGrade - Steel grade
|
||||
* @returns {Array<string>} Available environments
|
||||
*/
|
||||
export function getAvailableEnvironments(steelGrade) {
|
||||
if (!isLoaded) return [];
|
||||
|
||||
const painting = getPaintingRecommendations(steelGrade);
|
||||
const environments = new Set();
|
||||
|
||||
painting.forEach(rel => {
|
||||
if (rel.Ambiente_Corrosivo) {
|
||||
environments.add(rel.Ambiente_Corrosivo);
|
||||
}
|
||||
});
|
||||
|
||||
return Array.from(environments).sort();
|
||||
}
|
||||
|
||||
/**
|
||||
* Format welding recommendation as HTML
|
||||
* @param {object} welding - Welding data
|
||||
* @returns {string} HTML
|
||||
*/
|
||||
export function formatWeldingRecommendation(welding) {
|
||||
if (!welding) {
|
||||
return '<p style="color: var(--color-text-secondary);">Nenhuma recomendação de soldagem disponível</p>';
|
||||
}
|
||||
|
||||
return `
|
||||
<div class="recommendation-card" style="background: var(--color-bg-1); padding: 16px; border-radius: 8px; margin-bottom: 16px;">
|
||||
<h4 style="margin: 0 0 12px 0; color: var(--color-primary);">🔥 Soldagem Recomendada</h4>
|
||||
|
||||
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 12px; margin-bottom: 12px;">
|
||||
<div>
|
||||
<strong>Processo 1:</strong> ${welding.Processo_Soldagem_1 || '-'}<br>
|
||||
<small>Eletrodo: ${welding.Eletrodo_1 || '-'}</small>
|
||||
</div>
|
||||
<div>
|
||||
<strong>Processo 2:</strong> ${welding.Processo_Soldagem_2 || '-'}<br>
|
||||
<small>Arame: ${welding.Arame_2 || '-'}</small><br>
|
||||
<small>Gás: ${welding.Gás_Proteção || '-'}</small>
|
||||
</div>
|
||||
<div>
|
||||
<strong>Processo 3:</strong> ${welding.Processo_Soldagem_3 || '-'}<br>
|
||||
<small>Arame/Fluxo: ${welding.Arame_Fluxo_3 || '-'}</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 12px; padding-top: 12px; border-top: 1px solid var(--color-border);">
|
||||
<div>
|
||||
<strong>CEV:</strong> ${welding.CEV || '-'}
|
||||
</div>
|
||||
<div>
|
||||
<strong>Pré-aquecimento:</strong> ${welding.Pré_Aquecimento || '-'}
|
||||
</div>
|
||||
<div>
|
||||
<strong>Norma:</strong> ${welding.Norma_Soldagem || '-'}
|
||||
</div>
|
||||
<div>
|
||||
<strong>Ensaios:</strong> ${welding.Ensaios_NDT || '-'}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
${welding.Observações ? `
|
||||
<div style="margin-top: 12px; padding: 8px; background: var(--color-bg-2); border-radius: 4px; font-size: 13px;">
|
||||
💡 <strong>Observações:</strong> ${welding.Observações}
|
||||
</div>
|
||||
` : ''}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Format painting recommendation as HTML
|
||||
* @param {object} painting - Painting data
|
||||
* @returns {string} HTML
|
||||
*/
|
||||
export function formatPaintingRecommendation(painting) {
|
||||
if (!painting) {
|
||||
return '<p style="color: var(--color-text-secondary);">Nenhuma recomendação de pintura disponível</p>';
|
||||
}
|
||||
|
||||
return `
|
||||
<div class="recommendation-card" style="background: var(--color-bg-3); padding: 16px; border-radius: 8px; margin-bottom: 16px;">
|
||||
<h4 style="margin: 0 0 12px 0; color: var(--color-primary);">🎨 Pintura Recomendada</h4>
|
||||
|
||||
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 12px; margin-bottom: 12px;">
|
||||
<div>
|
||||
<strong>Ambiente:</strong> ${painting.Ambiente_Corrosivo || '-'}<br>
|
||||
<small>Vida útil: ${painting.Vida_Útil_Esperada || '-'}</small>
|
||||
</div>
|
||||
<div>
|
||||
<strong>Preparação:</strong> ${painting.Preparação_Superfície || '-'}<br>
|
||||
<small>Rugosidade: ${painting.Perfil_Rugosidade || '-'}</small>
|
||||
</div>
|
||||
<div>
|
||||
<strong>DFT Total:</strong> ${painting.DFT_Total || '-'}<br>
|
||||
<small>Custo: ${painting.Custo_Relativo_m2 || '-'}/m²</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="margin-bottom: 12px;">
|
||||
<strong>Sistema de Pintura (3 camadas):</strong>
|
||||
<div style="display: grid; grid-template-columns: repeat(3, 1fr); gap: 8px; margin-top: 8px;">
|
||||
<div style="padding: 8px; background: var(--color-surface); border-radius: 4px; text-align: center;">
|
||||
<div style="font-size: 11px; color: var(--color-text-secondary);">PRIMER</div>
|
||||
<div style="font-weight: bold; margin: 4px 0;">${painting.Primer || '-'}</div>
|
||||
<div style="font-size: 12px;">${painting.Tipo_Tinta_Primer || '-'}</div>
|
||||
</div>
|
||||
<div style="padding: 8px; background: var(--color-surface); border-radius: 4px; text-align: center;">
|
||||
<div style="font-size: 11px; color: var(--color-text-secondary);">INTERMEDIÁRIA</div>
|
||||
<div style="font-weight: bold; margin: 4px 0;">${painting.Intermediária || '-'}</div>
|
||||
<div style="font-size: 12px;">${painting.Tipo_Tinta_Intermediária || '-'}</div>
|
||||
</div>
|
||||
<div style="padding: 8px; background: var(--color-surface); border-radius: 4px; text-align: center;">
|
||||
<div style="font-size: 11px; color: var(--color-text-secondary);">ACABAMENTO</div>
|
||||
<div style="font-weight: bold; margin: 4px 0;">${painting.Acabamento || '-'}</div>
|
||||
<div style="font-size: 12px;">${painting.Tipo_Tinta_Acabamento || '-'}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 12px; padding-top: 12px; border-top: 1px solid var(--color-border);">
|
||||
<div>
|
||||
<strong>Norma:</strong> ${painting.Norma_Pintura || '-'}
|
||||
</div>
|
||||
<div>
|
||||
<strong>Ensaios:</strong> ${painting.Ensaios_Pintura || '-'}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
${painting.Observações_Pintura ? `
|
||||
<div style="margin-top: 12px; padding: 8px; background: var(--color-bg-2); border-radius: 4px; font-size: 13px;">
|
||||
💡 <strong>Observações:</strong> ${painting.Observações_Pintura}
|
||||
</div>
|
||||
` : ''}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
// Auto-load on import
|
||||
if (typeof window !== 'undefined') {
|
||||
loadRelationships();
|
||||
}
|
||||
195
js/utils/validators.js
Normal file
195
js/utils/validators.js
Normal file
@@ -0,0 +1,195 @@
|
||||
/**
|
||||
* Validators - Input validation utilities
|
||||
*/
|
||||
|
||||
/**
|
||||
* Validate if value is a number
|
||||
* @param {any} value - Value to validate
|
||||
* @returns {boolean} True if valid number
|
||||
*/
|
||||
export function isValidNumber(value) {
|
||||
return !isNaN(parseFloat(value)) && isFinite(value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate if value is within range
|
||||
* @param {number} value - Value to validate
|
||||
* @param {number} min - Minimum value
|
||||
* @param {number} max - Maximum value
|
||||
* @returns {boolean} True if within range
|
||||
*/
|
||||
export function isInRange(value, min, max) {
|
||||
if (!isValidNumber(value)) return false;
|
||||
const num = parseFloat(value);
|
||||
return num >= min && num <= max;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate if value is positive
|
||||
* @param {number} value - Value to validate
|
||||
* @returns {boolean} True if positive
|
||||
*/
|
||||
export function isPositive(value) {
|
||||
return isValidNumber(value) && parseFloat(value) > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate if value is non-negative
|
||||
* @param {number} value - Value to validate
|
||||
* @returns {boolean} True if non-negative
|
||||
*/
|
||||
export function isNonNegative(value) {
|
||||
return isValidNumber(value) && parseFloat(value) >= 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate email format
|
||||
* @param {string} email - Email to validate
|
||||
* @returns {boolean} True if valid email
|
||||
*/
|
||||
export function isValidEmail(email) {
|
||||
const regex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||
return regex.test(email);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate if string is not empty
|
||||
* @param {string} value - String to validate
|
||||
* @returns {boolean} True if not empty
|
||||
*/
|
||||
export function isNotEmpty(value) {
|
||||
return value !== null && value !== undefined && value.toString().trim().length > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate CEV value (typical range)
|
||||
* @param {number} cev - CEV value
|
||||
* @returns {object} Validation result with message
|
||||
*/
|
||||
export function validateCEV(cev) {
|
||||
if (!isValidNumber(cev)) {
|
||||
return { valid: false, message: 'CEV deve ser um número válido' };
|
||||
}
|
||||
|
||||
const value = parseFloat(cev);
|
||||
|
||||
if (value < 0) {
|
||||
return { valid: false, message: 'CEV não pode ser negativo' };
|
||||
}
|
||||
|
||||
if (value > 1.0) {
|
||||
return { valid: false, message: 'CEV muito alto (>1.0). Verifique os valores.' };
|
||||
}
|
||||
|
||||
if (value > 0.65) {
|
||||
return { valid: true, message: 'Atenção: CEV muito alto (>0.65). Soldabilidade difícil.' };
|
||||
}
|
||||
|
||||
return { valid: true, message: '' };
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate temperature value
|
||||
* @param {number} temp - Temperature in Celsius
|
||||
* @param {number} min - Minimum temperature
|
||||
* @param {number} max - Maximum temperature
|
||||
* @returns {object} Validation result
|
||||
*/
|
||||
export function validateTemperature(temp, min = -50, max = 500) {
|
||||
if (!isValidNumber(temp)) {
|
||||
return { valid: false, message: 'Temperatura deve ser um número válido' };
|
||||
}
|
||||
|
||||
const value = parseFloat(temp);
|
||||
|
||||
if (value < min) {
|
||||
return { valid: false, message: `Temperatura muito baixa (mínimo: ${min}°C)` };
|
||||
}
|
||||
|
||||
if (value > max) {
|
||||
return { valid: false, message: `Temperatura muito alta (máximo: ${max}°C)` };
|
||||
}
|
||||
|
||||
return { valid: true, message: '' };
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate percentage value
|
||||
* @param {number} value - Percentage value
|
||||
* @param {boolean} isDecimal - If true, expects 0-1, else 0-100
|
||||
* @returns {object} Validation result
|
||||
*/
|
||||
export function validatePercentage(value, isDecimal = false) {
|
||||
if (!isValidNumber(value)) {
|
||||
return { valid: false, message: 'Porcentagem deve ser um número válido' };
|
||||
}
|
||||
|
||||
const num = parseFloat(value);
|
||||
const max = isDecimal ? 1 : 100;
|
||||
|
||||
if (num < 0) {
|
||||
return { valid: false, message: 'Porcentagem não pode ser negativa' };
|
||||
}
|
||||
|
||||
if (num > max) {
|
||||
return { valid: false, message: `Porcentagem não pode exceder ${max}${isDecimal ? '' : '%'}` };
|
||||
}
|
||||
|
||||
return { valid: true, message: '' };
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate form inputs
|
||||
* @param {object} inputs - Object with input values
|
||||
* @param {object} rules - Validation rules
|
||||
* @returns {object} Validation result with errors
|
||||
*/
|
||||
export function validateForm(inputs, rules) {
|
||||
const errors = {};
|
||||
let isValid = true;
|
||||
|
||||
for (const [field, value] of Object.entries(inputs)) {
|
||||
const rule = rules[field];
|
||||
if (!rule) continue;
|
||||
|
||||
// Required check
|
||||
if (rule.required && !isNotEmpty(value)) {
|
||||
errors[field] = 'Campo obrigatório';
|
||||
isValid = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Type check
|
||||
if (rule.type === 'number' && !isValidNumber(value)) {
|
||||
errors[field] = 'Deve ser um número válido';
|
||||
isValid = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Range check
|
||||
if (rule.min !== undefined || rule.max !== undefined) {
|
||||
const num = parseFloat(value);
|
||||
if (rule.min !== undefined && num < rule.min) {
|
||||
errors[field] = `Valor mínimo: ${rule.min}`;
|
||||
isValid = false;
|
||||
continue;
|
||||
}
|
||||
if (rule.max !== undefined && num > rule.max) {
|
||||
errors[field] = `Valor máximo: ${rule.max}`;
|
||||
isValid = false;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// Custom validator
|
||||
if (rule.validator) {
|
||||
const result = rule.validator(value);
|
||||
if (!result.valid) {
|
||||
errors[field] = result.message;
|
||||
isValid = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { isValid, errors };
|
||||
}
|
||||
Reference in New Issue
Block a user