742 lines
29 KiB
JavaScript
742 lines
29 KiB
JavaScript
/**
|
||
* 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');
|