import jsPDF from 'jspdf' import type { RepairTicket, RepairLineItem, RepairNote } from '@/types/repair' const STATUS_LABELS: Record = { in_transit: 'In Transit', intake: 'Intake', diagnosing: 'Diagnosing', pending_approval: 'Pending Approval', approved: 'Approved', in_progress: 'In Progress', pending_parts: 'Pending Parts', ready: 'Ready for Pickup', picked_up: 'Picked Up', delivered: 'Delivered', cancelled: 'Cancelled', } interface GeneratePdfOptions { ticket: RepairTicket lineItems: RepairLineItem[] notes: RepairNote[] includeNotes: boolean companyName?: string } export function generateRepairPdf({ ticket, lineItems, notes, includeNotes, companyName = 'LunarFront' }: GeneratePdfOptions): jsPDF { const doc = new jsPDF() let y = 20 // Header doc.setFontSize(18) doc.setFont('helvetica', 'bold') doc.text(companyName, 14, y) y += 8 doc.setFontSize(12) doc.setFont('helvetica', 'normal') doc.text('Repair Ticket', 14, y) // Ticket number and status — right aligned doc.setFontSize(14) doc.setFont('helvetica', 'bold') doc.text(`#${ticket.ticketNumber ?? ''}`, 196, 20, { align: 'right' }) doc.setFontSize(10) doc.setFont('helvetica', 'normal') doc.text(STATUS_LABELS[ticket.status] ?? ticket.status, 196, 28, { align: 'right' }) doc.text(new Date(ticket.intakeDate).toLocaleDateString(), 196, 34, { align: 'right' }) y += 12 // Divider doc.setDrawColor(200) doc.line(14, y, 196, y) y += 8 // Customer info doc.setFontSize(10) doc.setFont('helvetica', 'bold') doc.text('Customer', 14, y) doc.text('Item', 110, y) y += 5 doc.setFont('helvetica', 'normal') doc.text(ticket.customerName, 14, y) doc.text(ticket.itemDescription ?? '-', 110, y) y += 5 if (ticket.customerPhone) { doc.text(ticket.customerPhone, 14, y); y += 5 } if (ticket.serialNumber) { doc.text(`S/N: ${ticket.serialNumber}`, 110, y - 5) } y += 3 // Problem description doc.setFont('helvetica', 'bold') doc.text('Problem Description', 14, y) y += 5 doc.setFont('helvetica', 'normal') const problemLines = doc.splitTextToSize(ticket.problemDescription, 180) doc.text(problemLines, 14, y) y += problemLines.length * 4 + 5 // Dates doc.setFontSize(9) const dateInfo = [ `Intake: ${new Date(ticket.intakeDate).toLocaleDateString()}`, ticket.promisedDate ? `Promised: ${new Date(ticket.promisedDate).toLocaleDateString()}` : null, ticket.completedDate ? `Completed: ${new Date(ticket.completedDate).toLocaleDateString()}` : null, ].filter(Boolean).join(' | ') doc.text(dateInfo, 14, y) y += 8 // Line items table if (lineItems.length > 0) { doc.setDrawColor(200) doc.line(14, y, 196, y) y += 6 doc.setFontSize(10) doc.setFont('helvetica', 'bold') doc.text('Line Items', 14, y) y += 6 // Table header doc.setFontSize(8) doc.setFillColor(245, 245, 245) doc.rect(14, y - 3, 182, 6, 'F') doc.text('Type', 16, y) doc.text('Description', 40, y) doc.text('Qty', 130, y, { align: 'right' }) doc.text('Unit Price', 155, y, { align: 'right' }) doc.text('Total', 190, y, { align: 'right' }) y += 5 // Table rows doc.setFont('helvetica', 'normal') for (const item of lineItems) { if (y > 270) { doc.addPage(); y = 20 } doc.text(item.itemType.replace('_', ' '), 16, y) const descLines = doc.splitTextToSize(item.description, 85) doc.text(descLines, 40, y) doc.text(String(item.qty), 130, y, { align: 'right' }) doc.text(`$${item.unitPrice}`, 155, y, { align: 'right' }) doc.text(`$${item.totalPrice}`, 190, y, { align: 'right' }) y += descLines.length * 4 + 2 } // Total y += 2 doc.setDrawColor(200) doc.line(140, y, 196, y) y += 5 doc.setFont('helvetica', 'bold') doc.setFontSize(10) const total = lineItems.reduce((sum, i) => sum + parseFloat(i.totalPrice), 0) doc.text('Total:', 155, y, { align: 'right' }) doc.text(`$${total.toFixed(2)}`, 190, y, { align: 'right' }) y += 4 if (ticket.estimatedCost) { doc.setFont('helvetica', 'normal') doc.setFontSize(9) doc.text(`Estimated: $${ticket.estimatedCost}`, 190, y, { align: 'right' }) y += 4 } y += 4 } // Customer-visible notes if (includeNotes) { const customerNotes = notes.filter((n) => n.visibility === 'customer') if (customerNotes.length > 0) { if (y > 250) { doc.addPage(); y = 20 } doc.setDrawColor(200) doc.line(14, y, 196, y) y += 6 doc.setFontSize(10) doc.setFont('helvetica', 'bold') doc.text('Notes', 14, y) y += 6 doc.setFontSize(9) doc.setFont('helvetica', 'normal') for (const note of customerNotes) { if (y > 270) { doc.addPage(); y = 20 } doc.setFont('helvetica', 'bold') doc.text(`${note.authorName} — ${new Date(note.createdAt).toLocaleDateString()}`, 14, y) y += 4 doc.setFont('helvetica', 'normal') const noteLines = doc.splitTextToSize(note.content, 180) doc.text(noteLines, 14, y) y += noteLines.length * 3.5 + 4 } } } // Footer if (y > 270) { doc.addPage(); y = 20 } y = 280 doc.setFontSize(8) doc.setFont('helvetica', 'normal') doc.setTextColor(150) doc.text(`Generated ${new Date().toLocaleString()} — Ticket #${ticket.ticketNumber}`, 105, y, { align: 'center' }) return doc } export async function generateAndUploadPdf( options: GeneratePdfOptions, ticketId: string, token: string | null, ): Promise { const doc = generateRepairPdf(options) const filename = `repair-${options.ticket.ticketNumber}-${options.ticket.status}.pdf` // Download for user doc.save(filename) // Also upload to ticket documents const blob = doc.output('blob') const formData = new FormData() formData.append('file', new File([blob], filename, { type: 'application/pdf' })) formData.append('entityType', 'repair_ticket') formData.append('entityId', ticketId) formData.append('category', 'document') try { await fetch('/v1/files', { method: 'POST', headers: { Authorization: `Bearer ${token}` }, body: formData, }) } catch { // Download succeeded even if upload fails — non-critical } }