241 lines
8.5 KiB
TypeScript
241 lines
8.5 KiB
TypeScript
import { ReportData, Standard, ComplianceItem } from '../types';
|
|
|
|
/**
|
|
* Extracts a numeric value from a string like "0.22%", "415 MPa", etc.
|
|
*/
|
|
function extractNumber(val: string): number | null {
|
|
if (!val) return null;
|
|
const cleanVal = val.replace(/,/g, '.');
|
|
const match = cleanVal.match(/-?\d+(\.\d+)?/);
|
|
return match ? parseFloat(match[0]) : null;
|
|
}
|
|
|
|
/**
|
|
* Parses the product string to try and find a thickness in mm.
|
|
* E.g., "CHAPA 50MM", "PLATE 25.4 MM"
|
|
*/
|
|
function parseThickness(productDesc: string): number {
|
|
if (!productDesc) return 0;
|
|
|
|
const cleanDesc = productDesc.replace(/,/g, '.');
|
|
|
|
const matchDimensions = cleanDesc.match(/(\d+(?:\.\d+)?)\s*(?:x|X|\*)\s*(\d+(?:\.\d+)?)\s*(?:x|X|\*)\s*(\d+(?:\.\d+)?)/);
|
|
if (matchDimensions) {
|
|
const dim1 = parseFloat(matchDimensions[1]);
|
|
const dim2 = parseFloat(matchDimensions[2]);
|
|
const dim3 = parseFloat(matchDimensions[3]);
|
|
// The thickness is almost always the smallest dimension of a plate
|
|
return Math.min(dim1, dim2, dim3);
|
|
}
|
|
|
|
const match = cleanDesc.match(/(\d+(\.\d+)?)\s*(mm)/i);
|
|
if (match) return parseFloat(match[1]);
|
|
return 0; // Default or unknown
|
|
}
|
|
|
|
function normalizeElementName(element: string): string {
|
|
const map: Record<string, string> = {
|
|
'c': 'carbono',
|
|
'carbon': 'carbono',
|
|
'carbono': 'carbono',
|
|
'mn': 'manganes',
|
|
'manganese': 'manganes',
|
|
'manganes': 'manganes',
|
|
'si': 'silicio',
|
|
'silicon': 'silicio',
|
|
'silicio': 'silicio',
|
|
'p': 'fosforo',
|
|
'phosphorus': 'fosforo',
|
|
'fosforo': 'fosforo',
|
|
's': 'enxofre',
|
|
'sulfur': 'enxofre',
|
|
'enxofre': 'enxofre',
|
|
'cu': 'cobre',
|
|
'copper': 'cobre',
|
|
'cobre': 'cobre',
|
|
'ni': 'niquel',
|
|
'nickel': 'niquel',
|
|
'niquel': 'niquel',
|
|
'cr': 'cromo',
|
|
'chromium': 'cromo',
|
|
'cromo': 'cromo',
|
|
'mo': 'molibdenio',
|
|
'molybdenum': 'molibdenio',
|
|
'molibdenio': 'molibdenio',
|
|
'v': 'vanadio',
|
|
'vanadium': 'vanadio',
|
|
'vanadio': 'vanadio',
|
|
'nb': 'niobio',
|
|
'columbium': 'niobio',
|
|
'niobium': 'niobio',
|
|
'niobio': 'niobio',
|
|
'ti': 'titanio',
|
|
'titanium': 'titanio',
|
|
'titanio': 'titanio',
|
|
'al': 'aluminio',
|
|
'aluminum': 'aluminio',
|
|
'aluminio': 'aluminio'
|
|
};
|
|
|
|
const lower = element.toLowerCase().trim();
|
|
// Remove content in parentheses, e.g. "C (Carbon)" -> "c"
|
|
const clean = lower.replace(/\(.*?\)/g, '').trim();
|
|
return map[clean] || clean;
|
|
}
|
|
|
|
function getTolerance(elementName: string, productVal: number, thickness: number, norm: Standard): number {
|
|
if (!norm.tolerancias_analise_produto_astma6) return 0;
|
|
|
|
const tolRules = norm.tolerancias_analise_produto_astma6[elementName];
|
|
if (!tolRules) return 0;
|
|
|
|
if (tolRules.tabela && tolRules.tabela.length > 0) {
|
|
for (const faixa of tolRules.tabela) {
|
|
if (faixa.espessura_ate_mm && thickness <= faixa.espessura_ate_mm) {
|
|
return faixa.tolerancia_mais || 0;
|
|
}
|
|
if (faixa.espessura_maior_que_mm && thickness > faixa.espessura_maior_que_mm) {
|
|
return faixa.tolerancia_mais || 0;
|
|
}
|
|
if (faixa.limite_corrida_ate && productVal <= faixa.limite_corrida_ate) {
|
|
return faixa.tolerancia_mais || 0;
|
|
}
|
|
}
|
|
}
|
|
|
|
return tolRules.tolerancia_mais || 0;
|
|
}
|
|
|
|
export function validateCertificateAgainstStandard(extractedReport: ReportData, standard: Standard): ReportData {
|
|
const report = JSON.parse(JSON.stringify(extractedReport)) as ReportData;
|
|
const thickness = parseThickness(report.identification.product);
|
|
|
|
let allOk = true;
|
|
|
|
// Encontrar limites baseados no produto (simplificado: pegamos o primeiro grupo que atende a espessura, ou o flat limits)
|
|
let activeLimits: Record<string, number | null> = {};
|
|
|
|
if (standard.limites_quimicos_corrida) {
|
|
const grupos = Object.values(standard.limites_quimicos_corrida)[0] || [];
|
|
for (const limite of grupos) {
|
|
if (!limite.espessura_max_mm || thickness <= limite.espessura_max_mm) {
|
|
// Encontrou a faixa certa
|
|
for (const [key, val] of Object.entries(limite)) {
|
|
if (typeof val === 'number') {
|
|
activeLimits[key] = val;
|
|
}
|
|
}
|
|
break; // Pega o primeiro correspondente
|
|
}
|
|
}
|
|
} else if (standard.parametros_controle?.quimicos) {
|
|
activeLimits = { ...standard.parametros_controle.quimicos };
|
|
}
|
|
|
|
// Validate Chemical
|
|
if (report.compliance.chemical) {
|
|
for (const item of report.compliance.chemical) {
|
|
if (!item.element) continue;
|
|
|
|
const normElement = normalizeElementName(item.element);
|
|
const val = extractNumber(item.certificate);
|
|
|
|
if (val !== null) {
|
|
// Look for max limit
|
|
const maxLimit = activeLimits[`${normElement}_max`] || activeLimits[normElement];
|
|
const minLimit = activeLimits[`${normElement}_min`];
|
|
|
|
let status: 'OK' | 'FALHA' = 'OK';
|
|
let normStr = [];
|
|
|
|
// Product tolerance
|
|
const tolerance = getTolerance(normElement, maxLimit as number || 0, thickness, standard);
|
|
|
|
if (minLimit !== undefined && minLimit !== null) {
|
|
normStr.push(`Min: ${minLimit}`);
|
|
if (val < (minLimit - tolerance)) status = 'FALHA';
|
|
}
|
|
|
|
if (maxLimit !== undefined && maxLimit !== null) {
|
|
normStr.push(`Max: ${maxLimit}`);
|
|
if (val > (maxLimit + tolerance)) status = 'FALHA';
|
|
}
|
|
|
|
if (normStr.length > 0) {
|
|
item.norm = normStr.join(' - ');
|
|
} else {
|
|
item.norm = '-';
|
|
}
|
|
|
|
item.status = status;
|
|
if (status === 'FALHA') allOk = false;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Validate Mechanical
|
|
const mechLimits = standard.parametros_controle?.mecanicos || {};
|
|
if (report.compliance.mechanical) {
|
|
for (const item of report.compliance.mechanical) {
|
|
if (!item.property) continue;
|
|
|
|
const val = extractNumber(item.certificate);
|
|
if (val !== null) {
|
|
const propLower = item.property.toLowerCase();
|
|
let maxLimit: number | null = null;
|
|
let minLimit: number | null = null;
|
|
|
|
if (propLower.includes('yield') || propLower.includes('escoamento')) {
|
|
minLimit = mechLimits['limite_escoamento_min'] || mechLimits['yield_strength_min'] || null;
|
|
maxLimit = mechLimits['limite_escoamento_max'] || mechLimits['yield_strength_max'] || null;
|
|
} else if (propLower.includes('tensile') || propLower.includes('resistencia') || propLower.includes('tração') || propLower.includes('tracao')) {
|
|
minLimit = mechLimits['limite_resistencia_min'] || mechLimits['resistencia_tracao_min'] || mechLimits['tensile_strength_min'] || null;
|
|
maxLimit = mechLimits['limite_resistencia_max'] || mechLimits['resistencia_tracao_max'] || mechLimits['tensile_strength_max'] || null;
|
|
} else if (propLower.includes('elongation') || propLower.includes('alongamento')) {
|
|
minLimit = mechLimits['alongamento_min'] || mechLimits['alongamento_min_50mm'] || mechLimits['elongation_min'] || null;
|
|
}
|
|
|
|
let status: 'OK' | 'FALHA' = 'OK';
|
|
let normStr = [];
|
|
|
|
if (minLimit !== null && maxLimit !== null) {
|
|
normStr.push(`Min: ${minLimit} - Max: ${maxLimit}`);
|
|
if (val < minLimit) {
|
|
status = 'FALHA';
|
|
item.differencePercentage = `${(((val - minLimit) / minLimit) * 100).toFixed(1)}%`;
|
|
} else if (val > maxLimit) {
|
|
status = 'FALHA';
|
|
item.differencePercentage = `+${(((val - maxLimit) / maxLimit) * 100).toFixed(1)}%`;
|
|
} else {
|
|
item.differencePercentage = `+${(((val - minLimit) / minLimit) * 100).toFixed(1)}%`;
|
|
}
|
|
} else if (minLimit !== null) {
|
|
normStr.push(`Min: ${minLimit}`);
|
|
if (val < minLimit) status = 'FALHA';
|
|
item.differencePercentage = `${val >= minLimit ? '+' : ''}${(((val - minLimit) / minLimit) * 100).toFixed(1)}%`;
|
|
} else if (maxLimit !== null) {
|
|
normStr.push(`Max: ${maxLimit}`);
|
|
if (val > maxLimit) status = 'FALHA';
|
|
item.differencePercentage = `${val <= maxLimit ? '-' : '+'}${Math.abs(((val - maxLimit) / maxLimit) * 100).toFixed(1)}%`;
|
|
}
|
|
|
|
if (normStr.length > 0) {
|
|
item.norm = normStr.join(' | ');
|
|
} else {
|
|
item.norm = '-';
|
|
}
|
|
|
|
item.status = status;
|
|
if (status === 'FALHA') allOk = false;
|
|
}
|
|
}
|
|
}
|
|
|
|
report.compliance.status = allOk ? 'CONFORME' : 'NÃO CONFORME';
|
|
if (!allOk) {
|
|
report.compliance.nonComplianceReason = 'Valores fora dos limites da norma técnica considerando bitola e tolerâncias de produto.';
|
|
}
|
|
|
|
return report;
|
|
}
|