import { createFileRoute, useParams, useNavigate } from '@tanstack/react-router' import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query' import { repairBatchDetailOptions, repairBatchMutations, repairBatchKeys, repairBatchTicketsOptions } from '@/api/repairs' import { usePagination } from '@/hooks/use-pagination' import { DataTable, type Column } from '@/components/shared/data-table' import { Button } from '@/components/ui/button' import { Badge } from '@/components/ui/badge' import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card' import { Skeleton } from '@/components/ui/skeleton' import { ArrowLeft, Check, X, Plus, FileText } from 'lucide-react' import { BatchStatusProgress } from '@/components/repairs/batch-status-progress' import { toast } from 'sonner' import { useAuthStore } from '@/stores/auth.store' import jsPDF from 'jspdf' import type { RepairTicket } from '@/types/repair' export const Route = createFileRoute('/_authenticated/repair-batches/$batchId')({ validateSearch: (search: Record) => ({ page: Number(search.page) || 1, limit: Number(search.limit) || 100, q: (search.q as string) || undefined, sort: (search.sort as string) || undefined, order: (search.order as 'asc' | 'desc') || 'asc', }), component: RepairBatchDetailPage, }) const STATUS_LABELS: Record = { new: 'New', in_transit: 'In Transit', intake: 'Intake', diagnosing: 'Diagnosing', pending_approval: 'Pending Approval', approved: 'Approved', in_progress: 'In Progress', pending_parts: 'Pending Parts', ready: 'Ready', picked_up: 'Picked Up', delivered: 'Delivered', cancelled: 'Cancelled', } const ticketColumns: Column[] = [ { key: 'ticket_number', header: 'Ticket #', sortable: true, render: (t) => {t.ticketNumber} }, { key: 'item_description', header: 'Item', render: (t) => <>{t.itemDescription ?? '-'} }, { key: 'problem', header: 'Problem', render: (t) => {t.problemDescription} }, { key: 'status', header: 'Status', sortable: true, render: (t) => {STATUS_LABELS[t.status] ?? t.status} }, { key: 'estimated_cost', header: 'Estimate', render: (t) => <>{t.estimatedCost ? `$${t.estimatedCost}` : '-'}, }, ] function RepairBatchDetailPage() { const { batchId } = useParams({ from: '/_authenticated/repair-batches/$batchId' }) const navigate = useNavigate() const queryClient = useQueryClient() const hasPermission = useAuthStore((s) => s.hasPermission) const token = useAuthStore((s) => s.token) const { params, setPage, setSort } = usePagination() const { data: batch, isLoading } = useQuery(repairBatchDetailOptions(batchId)) const { data: ticketsData, isLoading: ticketsLoading } = useQuery(repairBatchTicketsOptions(batchId, params)) const approveMutation = useMutation({ mutationFn: () => repairBatchMutations.approve(batchId), onSuccess: () => { queryClient.invalidateQueries({ queryKey: repairBatchKeys.detail(batchId) }) toast.success('Batch approved') }, onError: (err) => toast.error(err.message), }) const rejectMutation = useMutation({ mutationFn: () => repairBatchMutations.reject(batchId), onSuccess: () => { queryClient.invalidateQueries({ queryKey: repairBatchKeys.detail(batchId) }) toast.success('Batch rejected') }, onError: (err) => toast.error(err.message), }) const statusMutation = useMutation({ mutationFn: (status: string) => repairBatchMutations.updateStatus(batchId, status), onSuccess: () => { queryClient.invalidateQueries({ queryKey: repairBatchKeys.detail(batchId) }) toast.success('Status updated') }, onError: (err) => toast.error(err.message), }) if (isLoading) { return
} if (!batch) { return

Batch not found

} const tickets = ticketsData?.data ?? [] const repairCount = ticketsData?.pagination.total ?? 0 const totalEstimate = tickets.reduce((sum, t) => sum + (t.estimatedCost ? parseFloat(t.estimatedCost) : 0), 0) const totalActual = tickets.reduce((sum, t) => sum + (t.actualCost ? parseFloat(t.actualCost) : 0), 0) function handleTicketClick(ticket: RepairTicket) { navigate({ to: '/repairs/$ticketId', params: { ticketId: ticket.id }, search: { page: 1, limit: 25, q: undefined, sort: undefined, order: 'asc' as const } }) } function handleAddRepair() { // Navigate to new repair with batch and account pre-linked navigate({ to: '/repairs/new', search: { batchId, batchName: batch!.batchNumber ?? '', accountId: batch!.accountId, contactName: batch!.contactName ?? '' } }) } async function generateBatchPdf() { if (!batch) return const doc = new jsPDF() let y = 20 doc.setFontSize(18) doc.setFont('helvetica', 'bold') doc.text('LunarFront', 14, y) y += 8 doc.setFontSize(12) doc.setFont('helvetica', 'normal') doc.text('Repair Batch Summary', 14, y) doc.setFontSize(14) doc.setFont('helvetica', 'bold') doc.text(`Batch #${batch.batchNumber ?? ''}`, 196, 20, { align: 'right' }) doc.setFontSize(10) doc.setFont('helvetica', 'normal') doc.text(STATUS_LABELS[batch.status] ?? batch.status, 196, 28, { align: 'right' }) y += 12 doc.setDrawColor(200) doc.line(14, y, 196, y) y += 8 // Contact info doc.setFontSize(10) doc.setFont('helvetica', 'bold') doc.text('Contact', 14, y) y += 5 doc.setFont('helvetica', 'normal') if (batch.contactName) { doc.text(batch.contactName, 14, y); y += 5 } if (batch.contactPhone) { doc.text(batch.contactPhone, 14, y); y += 5 } if (batch.contactEmail) { doc.text(batch.contactEmail, 14, y); y += 5 } y += 3 // Summary doc.setFont('helvetica', 'bold') doc.text(`Repairs: ${repairCount}`, 14, y) if (batch.dueDate) doc.text(`Due: ${new Date(batch.dueDate).toLocaleDateString()}`, 100, y) y += 8 if (batch.notes) { doc.setFont('helvetica', 'normal') doc.setFontSize(9) const noteLines = doc.splitTextToSize(batch.notes, 180) doc.text(noteLines, 14, y) y += noteLines.length * 4 + 4 } // Repairs table doc.setDrawColor(200) doc.line(14, y, 196, y) y += 6 doc.setFontSize(10) doc.setFont('helvetica', 'bold') doc.text('Repairs', 14, y) y += 6 // Table header doc.setFontSize(8) doc.setFillColor(245, 245, 245) doc.rect(14, y - 3, 182, 6, 'F') doc.text('Ticket #', 16, y) doc.text('Item', 40, y) doc.text('Problem', 100, y) doc.text('Status', 155, y) doc.text('Estimate', 190, y, { align: 'right' }) y += 5 doc.setFont('helvetica', 'normal') for (const ticket of tickets) { if (y > 270) { doc.addPage(); y = 20 } doc.text(ticket.ticketNumber ?? '-', 16, y) doc.text((ticket.itemDescription ?? '-').slice(0, 30), 40, y) doc.text(ticket.problemDescription.slice(0, 28), 100, y) doc.text(STATUS_LABELS[ticket.status] ?? ticket.status, 155, y) doc.text(ticket.estimatedCost ? `$${ticket.estimatedCost}` : '-', 190, y, { align: 'right' }) y += 5 } // Totals y += 3 doc.setDrawColor(200) doc.line(140, y, 196, y) y += 5 doc.setFont('helvetica', 'bold') doc.setFontSize(10) doc.text('Estimated Total:', 155, y, { align: 'right' }) doc.text(`$${totalEstimate.toFixed(2)}`, 190, y, { align: 'right' }) y += 5 if (totalActual > 0) { doc.text('Actual Total:', 155, y, { align: 'right' }) doc.text(`$${totalActual.toFixed(2)}`, 190, y, { align: 'right' }) y += 5 } // Footer y = 280 doc.setFontSize(8) doc.setFont('helvetica', 'normal') doc.setTextColor(150) doc.text(`Generated ${new Date().toLocaleString()} — Batch #${batch.batchNumber}`, 105, y, { align: 'center' }) const filename = `repair-batch-${batch.batchNumber}.pdf` doc.save(filename) // Upload to batch 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', batchId) formData.append('category', 'document') try { await fetch('/v1/files', { method: 'POST', headers: { Authorization: `Bearer ${token}` }, body: formData, }) } catch { /* non-critical */ } toast.success('Batch PDF downloaded') } return (

Batch #{batch.batchNumber}

{batch.contactName ?? 'No contact'}

{STATUS_LABELS[batch.status] ?? batch.status} {batch.approvalStatus}
{/* Status Progress Bar */} statusMutation.mutate(status) : undefined} /> {/* Actions */}
{hasPermission('repairs.admin') && batch.approvalStatus === 'pending' && ( <> )} {hasPermission('repairs.edit') && batch.status === 'intake' && ( )} {hasPermission('repairs.edit') && batch.status === 'in_progress' && ( )} {hasPermission('repairs.edit') && batch.status === 'completed' && ( )}
{/* Batch Info */}
Contact
Name: {batch.contactName ?? '-'}
Phone: {batch.contactPhone ?? '-'}
Email: {batch.contactEmail ?? '-'}
Summary
Repairs: {repairCount}
Due: {batch.dueDate ? new Date(batch.dueDate).toLocaleDateString() : '-'}
Estimated Total: ${totalEstimate.toFixed(2)}
{totalActual > 0 &&
Actual Total: ${totalActual.toFixed(2)}
} {batch.notes &&
Notes: {batch.notes}
}
{/* Tickets in batch */} Repairs ({repairCount}) {hasPermission('repairs.edit') && ( )}
) }