/** * 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');