Files
SteelCheck/services/pdfService.ts
admtracksteel 97eb42c243 feat: multi-provider AI support with auto-detection
- Added support for Google Gemini, OpenAI, Anthropic, and Azure OpenAI
- Implemented API key validation with auto model detection
- Added Error Boundary for better error handling
- Migrated PDF generation to native jsPDF (better quality)
- Added PWA support with offline capabilities
- Implemented tests with Vitest
- Fixed language consistency (PT-BR)
- Improved accessibility (ARIA)
2026-04-04 19:32:00 +00:00

301 lines
8.9 KiB
TypeScript

import { jsPDF } from 'jspdf';
import type { ReportData } from '../types';
const COLORS = {
primary: '#1e40af',
secondary: '#6366f1',
success: '#059669',
danger: '#dc2626',
dark: '#1e293b',
gray: '#64748b',
light: '#f8fafc',
};
const formatDate = (dateStr: string): string => {
return dateStr || 'N/A';
};
const getStatusColor = (status: string): string => {
if (status === 'CONFORME' || status === 'OK') return COLORS.success;
if (status === 'NÃO CONFORME' || status === 'FALHA') return COLORS.danger;
return COLORS.gray;
};
export const generatePdfReport = (report: ReportData): jsPDF => {
const doc = new jsPDF({
orientation: 'portrait',
unit: 'mm',
format: 'a4',
});
const pageWidth = doc.internal.pageSize.getWidth();
const pageHeight = doc.internal.pageSize.getHeight();
const margin = 15;
let y = margin;
const addHeader = () => {
doc.setFillColor(COLORS.primary);
doc.rect(0, 0, pageWidth, 30, 'F');
doc.setTextColor(255, 255, 255);
doc.setFontSize(20);
doc.setFont('helvetica', 'bold');
doc.text('SteelCheck', margin, 18);
doc.setFontSize(10);
doc.setFont('helvetica', 'normal');
doc.text('Relatório de Análise Técnica de Qualidade', margin, 25);
y = 40;
};
const addFooter = (pageNum: number) => {
doc.setFillColor(COLORS.light);
doc.rect(0, pageHeight - 15, pageWidth, 15, 'F');
doc.setFontSize(8);
doc.setTextColor(COLORS.gray);
doc.text(`Página ${pageNum}`, pageWidth / 2, pageHeight - 8, { align: 'center' });
doc.text('SteelCheck - Análise assistida por IA', margin, pageHeight - 8);
};
addHeader();
doc.setTextColor(COLORS.dark);
doc.setFontSize(16);
doc.setFont('helvetica', 'bold');
doc.text('Relatório Técnico de Qualidade', pageWidth / 2, y, { align: 'center' });
y += 8;
doc.setFontSize(10);
doc.setFont('helvetica', 'normal');
doc.setTextColor(COLORS.gray);
doc.text('Análise de conformidade normativa assistida por IA', pageWidth / 2, y, { align: 'center' });
y += 12;
doc.setFillColor(COLORS.light);
doc.roundedRect(margin, y, pageWidth - 2 * margin, 25, 3, 3, 'F');
y += 5;
doc.setFontSize(12);
doc.setFont('helvetica', 'bold');
doc.setTextColor(COLORS.primary);
doc.text('Grau de Confiança da Análise', pageWidth / 2, y, { align: 'center' });
y += 8;
doc.setFontSize(36);
doc.setTextColor(COLORS.secondary);
doc.text(`${report.confidence}%`, pageWidth / 2, y + 5, { align: 'center' });
y += 20;
const sectionTitle = (title: string) => {
y += 5;
doc.setFillColor(COLORS.primary);
doc.rect(margin, y, 3, 10, 'F');
doc.setFontSize(12);
doc.setFont('helvetica', 'bold');
doc.setTextColor(COLORS.dark);
doc.text(title, margin + 8, y + 7);
doc.setDrawColor(200, 200, 200);
doc.line(margin + 8, y + 12, pageWidth - margin, y + 12);
y += 15;
};
const addKeyValue = (key: string, value: string) => {
doc.setFontSize(9);
doc.setFont('helvetica', 'bold');
doc.setTextColor(COLORS.gray);
doc.text(key.toUpperCase(), margin, y);
doc.setFont('helvetica', 'normal');
doc.setTextColor(COLORS.dark);
const valueLines = doc.splitTextToSize(value || 'N/A', pageWidth - 2 * margin - 40);
doc.text(valueLines, margin + 40, y);
y += 6 * (valueLines.length || 1);
};
sectionTitle('1. Dados de Identificação');
const ident = report.identification;
addKeyValue('Produto', ident.product);
addKeyValue('Norma(s)', ident.standards);
addKeyValue('Fabricante', ident.manufacturer);
addKeyValue('Nr. Certificado', ident.certificateNumber);
addKeyValue('Data', ident.certificateDate);
addKeyValue('Lotes', ident.batches);
addKeyValue('Corridas (Heats)', ident.heats);
addKeyValue('Quantidade', ident.quantity);
sectionTitle('2. Verificação de Conformidade');
const complianceStatusColor = report.compliance.status === 'CONFORME' ? COLORS.success : COLORS.danger;
doc.setFillColor(complianceStatusColor);
doc.roundedRect(pageWidth - margin - 40, y - 12, 35, 10, 2, 2, 'F');
doc.setTextColor(255, 255, 255);
doc.setFontSize(10);
doc.setFont('helvetica', 'bold');
doc.text(report.compliance.status, pageWidth - margin - 22, y - 6, { align: 'center' });
const addTable = (title: string, items: { property?: string; element?: string; test?: string; norm: string; certificate: string; status: string }[]) => {
doc.setFontSize(10);
doc.setFont('helvetica', 'bold');
doc.setTextColor(COLORS.dark);
doc.text(title, margin, y);
y += 6;
const colWidths = [45, 40, 40, 25];
const colX = [margin, margin + 45, margin + 85, margin + 125];
doc.setFillColor(240, 240, 240);
doc.rect(margin, y, pageWidth - 2 * margin, 8, 'F');
doc.setFontSize(8);
doc.setTextColor(COLORS.gray);
doc.text('Propriedade/Norma', colX[0] + 10, y + 5);
doc.text('Valor Norma', colX[1] + 5, y + 5);
doc.text('Valor Certificado', colX[2] + 5, y + 5);
doc.text('Status', colX[3] + 8, y + 5);
y += 8;
items.forEach((item) => {
const label = item.property || item.element || item.test || '';
doc.setFont('helvetica', 'normal');
doc.setTextColor(COLORS.dark);
doc.setFontSize(8);
const labelLines = doc.splitTextToSize(label, colWidths[0] - 5);
const normLines = doc.splitTextToSize(item.norm, colWidths[1] - 5);
const certLines = doc.splitTextToSize(item.certificate, colWidths[2] - 5);
const rowHeight = Math.max(labelLines.length, normLines.length, certLines.length) * 4 + 4;
doc.text(labelLines, colX[0], y + 4);
doc.text(normLines, colX[1], y + 4);
doc.text(certLines, colX[2], y + 4);
const statusColor = item.status === 'OK' ? COLORS.success : COLORS.danger;
doc.setFillColor(statusColor);
doc.roundedRect(colX[3], y, 20, 6, 1, 1, 'F');
doc.setTextColor(255, 255, 255);
doc.setFontSize(7);
doc.setFont('helvetica', 'bold');
doc.text(item.status, colX[3] + 10, y + 4, { align: 'center' });
y += rowHeight;
});
y += 5;
};
addTable('Propriedades Mecânicas', report.compliance.mechanical);
addTable('Composição Química (%)', report.compliance.chemical);
if (report.compliance.otherTests.length > 0) {
addTable('Outros Testes', report.compliance.otherTests);
}
if (report.compliance.status === 'CONFORME' && report.overPerformance.length > 0) {
sectionTitle('3. Destaques de Desempenho');
doc.setFontSize(9);
doc.setTextColor(COLORS.gray);
doc.text('Este material excede os requisitos mínimos normativos:', margin, y);
y += 6;
report.overPerformance.forEach((item) => {
doc.setFillColor(240, 255, 240);
doc.roundedRect(margin, y - 3, pageWidth - 2 * margin, 12, 2, 2, 'F');
doc.setFont('helvetica', 'bold');
doc.setTextColor(COLORS.dark);
doc.text(item.property, margin + 5, y + 2);
doc.setTextColor(COLORS.success);
doc.text(item.value, margin + 5, y + 8);
y += 15;
});
}
if (y > pageHeight - 50) {
doc.addPage();
y = margin;
addHeader();
}
sectionTitle('4. Normas Equivalentes');
doc.setFontSize(9);
doc.setTextColor(COLORS.gray);
doc.text(`Equivalências internacionais para ${report.identification.standards}:`, margin, y);
y += 8;
const boxWidth = 40;
const boxHeight = 15;
const gap = 5;
let x = margin;
report.equivalents.forEach((item) => {
if (x + boxWidth > pageWidth - margin) {
x = margin;
y += boxHeight + gap;
}
doc.setFillColor(248, 248, 255);
doc.roundedRect(x, y, boxWidth, boxHeight, 2, 2, 'F');
doc.setFontSize(7);
doc.setTextColor(COLORS.gray);
doc.text(item.system, x + 2, y + 4);
doc.setFontSize(9);
doc.setFont('helvetica', 'bold');
doc.setTextColor(COLORS.dark);
doc.text(item.norm, x + 2, y + 10);
x += boxWidth + gap;
});
const totalPages = doc.getNumberOfPages();
for (let i = 1; i <= totalPages; i++) {
doc.setPage(i);
addFooter(i);
}
return doc;
};
export const exportAsPdf = (elementId: string, fileName: string, action: 'preview' | 'download'): void => {
console.log('Using native jsPDF for better quality PDF generation');
const hiddenDiv = document.getElementById(elementId);
if (!hiddenDiv) {
console.error(`Element with id ${elementId} not found.`);
return;
}
try {
const jsonData = hiddenDiv.getAttribute('data-report');
if (!jsonData) {
console.error('Report data not found in element');
return;
}
const report: ReportData = JSON.parse(jsonData);
const pdf = generatePdfReport(report);
if (action === 'download') {
pdf.save(`${fileName}.pdf`);
} else {
const pdfBlob = pdf.output('blob');
const pdfUrl = URL.createObjectURL(pdfBlob);
window.open(pdfUrl, '_blank');
URL.revokeObjectURL(pdfUrl);
}
} catch (error) {
console.error('Error generating PDF:', error);
}
};