docs: implement Antigravity global rules

This commit is contained in:
2026-04-03 21:11:04 +00:00
parent 57ba9d1c5f
commit e8972e7107
83 changed files with 31408 additions and 31386 deletions

View File

@@ -1,209 +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');
/**
* 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');

View File

@@ -1,91 +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];
}
/**
* 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];
}

View File

@@ -1,86 +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;
}
}
/**
* 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;
}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,324 +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));
/**
* 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));

View File

@@ -1,177 +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');
/**
* 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');

File diff suppressed because it is too large Load Diff

View File

@@ -1,342 +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');
/**
* 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');

View File

@@ -1,330 +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');
/**
* 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');

View File

@@ -1,124 +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
};
}
/**
* 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
};
}

View File

@@ -1,349 +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}`);
}
/**
* 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}`);
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,402 +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;
}
/**
* 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;
}

View File

@@ -1,184 +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;
}
/**
* 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;
}

View File

@@ -1,238 +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>
`;
}
}
/**
* 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>
`;
}
}

View File

@@ -1,129 +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;
}
/**
* 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;
}

View File

@@ -1,230 +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
};
}
/**
* 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
};
}

View File

@@ -1,126 +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}`;
}
/**
* 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}`;
}

View File

@@ -1,272 +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();
}
/**
* 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();
}

View File

@@ -1,195 +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 };
}
/**
* 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 };
}