import jsPDF from 'jspdf'; import type { ChecklistItem, Measurement } from '@/stores/useModelStore'; interface ReportData { fileName: string; fileSize: number; fineTuning: { posX: number; posY: number; posZ: number; rotY: number }; checklist: ChecklistItem[]; measurements: Measurement[]; screenshots: string[]; scaleLabel?: string; } const STATUS_LABELS: Record = { pending: 'Pendente', approved: 'Aprovado', rejected: 'Reprovado', }; const STATUS_COLORS: Record = { pending: [180, 180, 50], approved: [34, 197, 94], rejected: [239, 68, 68], }; export async function generateInspectionReport(data: ReportData) { const pdf = new jsPDF({ orientation: 'portrait', unit: 'mm', format: 'a4' }); const pageW = pdf.internal.pageSize.getWidth(); const margin = 15; const contentW = pageW - margin * 2; let y = margin; const addPage = () => { pdf.addPage(); y = margin; }; const checkSpace = (needed: number) => { if (y + needed > 280) addPage(); }; // ── Header ── pdf.setFillColor(15, 23, 42); pdf.rect(0, 0, pageW, 35, 'F'); pdf.setFont('helvetica', 'bold'); pdf.setFontSize(18); pdf.setTextColor(255, 255, 255); pdf.text('TrackkSteelXR', margin, 16); pdf.setFontSize(10); pdf.setFont('helvetica', 'normal'); pdf.setTextColor(148, 163, 184); pdf.text('Relatório de Inspeção', margin, 24); const dateStr = new Date().toLocaleDateString('pt-BR', { day: '2-digit', month: '2-digit', year: 'numeric', hour: '2-digit', minute: '2-digit', }); pdf.text(dateStr, pageW - margin, 24, { align: 'right' }); y = 45; // ── Model Info ── pdf.setFontSize(12); pdf.setFont('helvetica', 'bold'); pdf.setTextColor(30, 41, 59); pdf.text('Informações do Modelo', margin, y); y += 7; pdf.setFontSize(9); pdf.setFont('helvetica', 'normal'); pdf.setTextColor(71, 85, 105); const infoLines = [ ['Arquivo:', data.fileName], ['Tamanho:', `${(data.fileSize / (1024 * 1024)).toFixed(2)} MB`], ['Escala:', `${data.scaleLabel ?? '1:1'} — valores em mm reais`], ]; for (const [label, value] of infoLines) { pdf.setFont('helvetica', 'bold'); pdf.text(label, margin, y); pdf.setFont('helvetica', 'normal'); pdf.text(value, margin + 25, y); y += 5; } // ── Fine Tuning ── y += 5; checkSpace(30); pdf.setFontSize(12); pdf.setFont('helvetica', 'bold'); pdf.setTextColor(30, 41, 59); pdf.text('Ajuste Fino (Offsets)', margin, y); y += 7; pdf.setFontSize(9); pdf.setFont('helvetica', 'normal'); pdf.setTextColor(71, 85, 105); const ft = data.fineTuning; const ftLines = [ ['Posição X:', `${ft.posX.toFixed(3)} m`], ['Posição Y:', `${ft.posY.toFixed(3)} m`], ['Posição Z:', `${ft.posZ.toFixed(3)} m`], ['Rotação Y:', `${ft.rotY.toFixed(1)}°`], ]; for (const [label, value] of ftLines) { pdf.setFont('helvetica', 'bold'); pdf.text(label, margin, y); pdf.setFont('helvetica', 'normal'); pdf.text(value, margin + 25, y); y += 5; } // ── Checklist ── y += 5; checkSpace(50); pdf.setFontSize(12); pdf.setFont('helvetica', 'bold'); pdf.setTextColor(30, 41, 59); pdf.text('Checklist de Inspeção', margin, y); y += 3; // Summary bar const approved = data.checklist.filter(i => i.status === 'approved').length; const rejected = data.checklist.filter(i => i.status === 'rejected').length; const pending = data.checklist.filter(i => i.status === 'pending').length; y += 5; pdf.setFontSize(8); pdf.setFont('helvetica', 'normal'); pdf.setTextColor(71, 85, 105); pdf.text(`Aprovados: ${approved} | Reprovados: ${rejected} | Pendentes: ${pending}`, margin, y); y += 7; for (const item of data.checklist) { checkSpace(20); const color = STATUS_COLORS[item.status] || [100, 100, 100]; // Status pill pdf.setFillColor(color[0], color[1], color[2]); pdf.roundedRect(margin, y - 3.5, 18, 5, 1.5, 1.5, 'F'); pdf.setFontSize(7); pdf.setFont('helvetica', 'bold'); pdf.setTextColor(255, 255, 255); pdf.text(STATUS_LABELS[item.status] || item.status, margin + 9, y, { align: 'center' }); // Label pdf.setFontSize(9); pdf.setFont('helvetica', 'normal'); pdf.setTextColor(30, 41, 59); pdf.text(item.label, margin + 22, y); y += 5; // Note if (item.note) { pdf.setFontSize(8); pdf.setTextColor(100, 116, 139); const noteLines = pdf.splitTextToSize(`Nota: ${item.note}`, contentW - 22); pdf.text(noteLines, margin + 22, y); y += noteLines.length * 4; } if (item.audioUrl) { pdf.setFontSize(7); pdf.setTextColor(100, 116, 139); pdf.text('🎙️ Áudio gravado anexado', margin + 22, y); y += 4; } y += 3; } // ── Measurements ── if (data.measurements.length > 0) { y += 3; checkSpace(30); pdf.setFontSize(12); pdf.setFont('helvetica', 'bold'); pdf.setTextColor(30, 41, 59); pdf.text('Medições', margin, y); y += 7; // Table header pdf.setFillColor(241, 245, 249); pdf.rect(margin, y - 4, contentW, 6, 'F'); pdf.setFontSize(8); pdf.setFont('helvetica', 'bold'); pdf.setTextColor(71, 85, 105); pdf.text('#', margin + 2, y); pdf.text('Ponto A (mm)', margin + 12, y); pdf.text('Ponto B (mm)', margin + 65, y); pdf.text('Distância', margin + 120, y); y += 6; pdf.setFont('helvetica', 'normal'); pdf.setFontSize(8); pdf.setTextColor(30, 41, 59); data.measurements.forEach((m, idx) => { checkSpace(8); if (idx % 2 === 0) { pdf.setFillColor(248, 250, 252); pdf.rect(margin, y - 4, contentW, 6, 'F'); } const fmtPt = (p: { x: number; y: number; z: number }) => `${(p.x * 1000).toFixed(1)}, ${(p.y * 1000).toFixed(1)}, ${(p.z * 1000).toFixed(1)}`; pdf.text(`${idx + 1}`, margin + 2, y); pdf.text(fmtPt(m.pointA), margin + 12, y); pdf.text(fmtPt(m.pointB), margin + 65, y); pdf.setFont('helvetica', 'bold'); pdf.text(`${m.distanceMM.toFixed(1)} mm`, margin + 120, y); pdf.setFont('helvetica', 'normal'); y += 6; }); } // ── Screenshots ── if (data.screenshots.length > 0) { addPage(); pdf.setFontSize(12); pdf.setFont('helvetica', 'bold'); pdf.setTextColor(30, 41, 59); pdf.text('Screenshots / Anotações', margin, y); y += 8; for (let i = 0; i < data.screenshots.length; i++) { checkSpace(100); try { const imgW = contentW; const imgH = imgW * 0.56; // ~16:9 pdf.addImage(data.screenshots[i], 'PNG', margin, y, imgW, imgH); y += imgH + 3; pdf.setFontSize(7); pdf.setFont('helvetica', 'normal'); pdf.setTextColor(148, 163, 184); pdf.text(`Captura ${i + 1} de ${data.screenshots.length}`, margin, y); y += 8; } catch { pdf.setFontSize(8); pdf.setTextColor(200, 50, 50); pdf.text(`[Erro ao inserir screenshot ${i + 1}]`, margin, y); y += 8; } } } // ── Footer on last page ── const pageH = pdf.internal.pageSize.getHeight(); pdf.setFillColor(241, 245, 249); pdf.rect(0, pageH - 12, pageW, 12, 'F'); pdf.setFontSize(7); pdf.setFont('helvetica', 'normal'); pdf.setTextColor(148, 163, 184); pdf.text('Gerado por TrackkSteelXR', margin, pageH - 5); pdf.text(`Página ${pdf.getNumberOfPages()}`, pageW - margin, pageH - 5, { align: 'right' }); // Save const safeName = data.fileName.replace(/\.[^.]+$/, ''); pdf.save(`Inspecao_${safeName}_${new Date().toISOString().slice(0, 10)}.pdf`); }