Initial commit SteelBase - Oficiais e Funcionando
This commit is contained in:
230
js/utils/csv-manager.js
Normal file
230
js/utils/csv-manager.js
Normal file
@@ -0,0 +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
|
||||
};
|
||||
}
|
||||
126
js/utils/formatters.js
Normal file
126
js/utils/formatters.js
Normal file
@@ -0,0 +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}`;
|
||||
}
|
||||
272
js/utils/material-relationships.js
Normal file
272
js/utils/material-relationships.js
Normal file
@@ -0,0 +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();
|
||||
}
|
||||
195
js/utils/validators.js
Normal file
195
js/utils/validators.js
Normal file
@@ -0,0 +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 };
|
||||
}
|
||||
Reference in New Issue
Block a user