Files
SteelBase/public/js/database/data-manager.js

742 lines
29 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 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');