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

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

View File

@@ -0,0 +1,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');