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,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 };
}