import { useState } from 'react' import { createFileRoute, useParams, useNavigate } from '@tanstack/react-router' import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query' import { repairTicketDetailOptions, repairTicketMutations, repairTicketKeys, repairLineItemListOptions, repairLineItemMutations, repairLineItemKeys, repairServiceTemplateListOptions, } from '@/api/repairs' import { api } from '@/lib/api-client' import { usePagination } from '@/hooks/use-pagination' import { StatusProgress } from '@/components/repairs/status-progress' import { TicketPhotos } from '@/components/repairs/ticket-photos' import { TicketNotes } from '@/components/repairs/ticket-notes' import { DataTable, type Column } from '@/components/shared/data-table' import { Button } from '@/components/ui/button' import { Badge } from '@/components/ui/badge' import { Input } from '@/components/ui/input' import { Label } from '@/components/ui/label' import { Textarea } from '@/components/ui/textarea' import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card' import { Skeleton } from '@/components/ui/skeleton' import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select' import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog' import { ArrowLeft, Plus, Trash2, RotateCcw, Save, Search } from 'lucide-react' import { PdfModal } from '@/components/repairs/pdf-modal' import { toast } from 'sonner' import { useAuthStore } from '@/stores/auth.store' import type { RepairLineItem } from '@/types/repair' export const Route = createFileRoute('/_authenticated/repairs/$ticketId')({ validateSearch: (search: Record) => ({ page: Number(search.page) || 1, limit: Number(search.limit) || 25, q: (search.q as string) || undefined, sort: (search.sort as string) || undefined, order: (search.order as 'asc' | 'desc') || 'asc', }), component: RepairTicketDetailPage, }) 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 for Pickup', picked_up: 'Picked Up', delivered: 'Delivered', cancelled: 'Cancelled', } const STATUS_FLOW = ['new', 'intake', 'diagnosing', 'pending_approval', 'approved', 'in_progress', 'ready', 'picked_up'] const TABS = [ { key: 'details', label: 'Details' }, { key: 'line-items', label: 'Line Items' }, { key: 'notes', label: 'Notes' }, { key: 'files', label: 'Photos & Docs' }, ] as const type TabKey = typeof TABS[number]['key'] function RepairTicketDetailPage() { const { ticketId } = useParams({ from: '/_authenticated/repairs/$ticketId' }) const navigate = useNavigate() const queryClient = useQueryClient() const hasPermission = useAuthStore((s) => s.hasPermission) const [activeTab, setActiveTab] = useState('details') const [addItemOpen, setAddItemOpen] = useState(false) const [editing, setEditing] = useState(false) const { params, setPage, setSort } = usePagination() const { data: ticket, isLoading } = useQuery(repairTicketDetailOptions(ticketId)) const { data: lineItemsData, isLoading: itemsLoading } = useQuery(repairLineItemListOptions(ticketId, params)) const [editFields, setEditFields] = useState>({}) const statusMutation = useMutation({ mutationFn: (status: string) => repairTicketMutations.updateStatus(ticketId, status), onSuccess: () => { queryClient.invalidateQueries({ queryKey: repairTicketKeys.detail(ticketId) }) toast.success('Status updated') }, onError: (err) => toast.error(err.message), }) const updateMutation = useMutation({ mutationFn: (data: Record) => repairTicketMutations.update(ticketId, data), onSuccess: () => { queryClient.invalidateQueries({ queryKey: repairTicketKeys.detail(ticketId) }) toast.success('Ticket updated') setEditing(false) }, onError: (err) => toast.error(err.message), }) const deleteItemMutation = useMutation({ mutationFn: repairLineItemMutations.delete, onSuccess: () => { queryClient.invalidateQueries({ queryKey: repairLineItemKeys.all(ticketId) }) toast.success('Line item removed') }, onError: (err) => toast.error(err.message), }) if (isLoading) { return
} if (!ticket) { return

Repair ticket not found

} function startEdit() { setEditFields({ customerName: ticket!.customerName, customerPhone: ticket!.customerPhone ?? '', itemDescription: ticket!.itemDescription ?? '', serialNumber: ticket!.serialNumber ?? '', conditionIn: ticket!.conditionIn ?? '', conditionInNotes: ticket!.conditionInNotes ?? '', problemDescription: ticket!.problemDescription, technicianNotes: ticket!.technicianNotes ?? '', estimatedCost: ticket!.estimatedCost ?? '', actualCost: ticket!.actualCost ?? '', }) setEditing(true) } function saveEdit() { const data: Record = {} if (editFields.customerName !== ticket!.customerName) data.customerName = editFields.customerName if (editFields.customerPhone !== (ticket!.customerPhone ?? '')) data.customerPhone = editFields.customerPhone || undefined if (editFields.itemDescription !== (ticket!.itemDescription ?? '')) data.itemDescription = editFields.itemDescription || undefined if (editFields.serialNumber !== (ticket!.serialNumber ?? '')) data.serialNumber = editFields.serialNumber || undefined if (editFields.conditionIn !== (ticket!.conditionIn ?? '')) data.conditionIn = editFields.conditionIn || undefined if (editFields.conditionInNotes !== (ticket!.conditionInNotes ?? '')) data.conditionInNotes = editFields.conditionInNotes || undefined if (editFields.problemDescription !== ticket!.problemDescription) data.problemDescription = editFields.problemDescription if (editFields.technicianNotes !== (ticket!.technicianNotes ?? '')) data.technicianNotes = editFields.technicianNotes || undefined if (editFields.estimatedCost !== (ticket!.estimatedCost ?? '')) data.estimatedCost = editFields.estimatedCost ? parseFloat(editFields.estimatedCost) : undefined if (editFields.actualCost !== (ticket!.actualCost ?? '')) data.actualCost = editFields.actualCost ? parseFloat(editFields.actualCost) : undefined if (Object.keys(data).length === 0) { setEditing(false); return } updateMutation.mutate(data) } const currentIdx = STATUS_FLOW.indexOf(ticket.status) const nextStatus = currentIdx >= 0 && currentIdx < STATUS_FLOW.length - 1 ? STATUS_FLOW[currentIdx + 1] : null const isTerminal = ['picked_up', 'delivered', 'cancelled'].includes(ticket.status) function handleStatusClick(status: string) { if (hasPermission('repairs.edit') && ticket && status !== ticket.status) { statusMutation.mutate(status) } } const lineItemColumns: Column[] = [ { key: 'item_type', header: 'Type', sortable: true, render: (i) => (
{i.itemType.replace('_', ' ')} {i.itemType === 'consumable' && Internal}
) }, { key: 'description', header: 'Description', render: (i) => <>{i.description} }, { key: 'qty', header: 'Qty', render: (i) => <>{i.qty} }, { key: 'unit_price', header: 'Unit Price', render: (i) => <>${i.unitPrice} }, { key: 'total_price', header: 'Total', render: (i) => ${i.totalPrice} }, { key: 'actions', header: '', render: (i) => hasPermission('repairs.admin') ? ( ) : null, }, ] return (
{/* Header */}

Ticket #{ticket.ticketNumber}

{ticket.customerName} — {ticket.itemDescription ?? 'No item description'}

{/* Status Progress Bar */} {/* Status Actions */}
{hasPermission('repairs.edit') && !isTerminal && ( <> {nextStatus && ( )} {ticket.status === 'new' && ( )} {ticket.status === 'in_progress' && ( )} {ticket.status === 'pending_parts' && ( )} {hasPermission('repairs.admin') && ( )} )} {ticket.status === 'cancelled' && hasPermission('repairs.admin') && ( )}
{/* Tabs */}
{TABS.map((tab) => ( ))}
{/* Tab Content */} {activeTab === 'details' && ( Ticket Details {hasPermission('repairs.edit') && !editing && ( )} {editing && (
)}
{editing ? (
setEditFields((p) => ({ ...p, customerName: e.target.value }))} />
setEditFields((p) => ({ ...p, customerPhone: e.target.value }))} />
setEditFields((p) => ({ ...p, itemDescription: e.target.value }))} />
setEditFields((p) => ({ ...p, serialNumber: e.target.value }))} />
setEditFields((p) => ({ ...p, estimatedCost: e.target.value }))} />
setEditFields((p) => ({ ...p, actualCost: e.target.value }))} />