Files
lunarfront-app/packages/admin/src/routes/_authenticated/repairs/index.tsx
Ryan Moon 01cff80f2b Add repair list filters, template management page, and backend filter support
Repairs list now has a filter panel with status (defaults to active only),
condition, batch/individual toggle, and date range filters for intake and
promised dates. Added Batch column to the repairs table. Backend list
endpoint accepts filter query params for status, condition, dates, and
batch membership. Template management page (admin only) with CRUD for
common repair services (rehair, string change, etc.) with instrument
type, size, and default pricing. Sidebar updated with Repair Templates
link gated on repairs.admin permission.
2026-03-29 10:13:38 -05:00

176 lines
5.9 KiB
TypeScript

import { createFileRoute, useNavigate } from '@tanstack/react-router'
import { useQuery } from '@tanstack/react-query'
import { useState } from 'react'
import { repairTicketListOptions } from '@/api/repairs'
import { usePagination } from '@/hooks/use-pagination'
import { DataTable, type Column } from '@/components/shared/data-table'
import { RepairFilterPanel, DEFAULT_FILTERS, type RepairFilters } from '@/components/repairs/repair-filters'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Badge } from '@/components/ui/badge'
import { Plus, Search } from 'lucide-react'
import { useAuthStore } from '@/stores/auth.store'
import type { RepairTicket } from '@/types/repair'
import type { PaginationInput } from '@forte/shared/schemas'
export const Route = createFileRoute('/_authenticated/repairs/')({
validateSearch: (search: Record<string, unknown>) => ({
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') || 'desc',
}),
component: RepairsListPage,
})
function statusBadge(status: string) {
const variants: Record<string, 'default' | 'secondary' | 'destructive' | 'outline'> = {
in_transit: 'secondary',
intake: 'outline',
diagnosing: 'secondary',
pending_approval: 'secondary',
approved: 'default',
in_progress: 'default',
pending_parts: 'secondary',
ready: 'default',
picked_up: 'outline',
delivered: 'outline',
cancelled: 'destructive',
}
const labels: Record<string, string> = {
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',
}
return <Badge variant={variants[status] ?? 'outline'}>{labels[status] ?? status}</Badge>
}
const columns: Column<RepairTicket>[] = [
{
key: 'ticket_number',
header: 'Ticket #',
sortable: true,
render: (t) => <span className="font-mono text-sm">{t.ticketNumber ?? '-'}</span>,
},
{
key: 'customer_name',
header: 'Customer',
sortable: true,
render: (t) => <span className="font-medium">{t.customerName}</span>,
},
{
key: 'instrument',
header: 'Instrument',
render: (t) => <>{t.instrumentDescription ?? '-'}</>,
},
{
key: 'status',
header: 'Status',
sortable: true,
render: (t) => statusBadge(t.status),
},
{
key: 'batch',
header: 'Batch',
render: (t) => t.repairBatchId ? <Badge variant="outline">Batch</Badge> : <span className="text-muted-foreground">-</span>,
},
{
key: 'intake_date',
header: 'Intake',
sortable: true,
render: (t) => <>{new Date(t.intakeDate).toLocaleDateString()}</>,
},
{
key: 'promised_date',
header: 'Promised',
sortable: true,
render: (t) => <>{t.promisedDate ? new Date(t.promisedDate).toLocaleDateString() : '-'}</>,
},
{
key: 'estimated_cost',
header: 'Estimate',
render: (t) => <>{t.estimatedCost ? `$${t.estimatedCost}` : '-'}</>,
},
]
function RepairsListPage() {
const navigate = useNavigate()
const hasPermission = useAuthStore((s) => s.hasPermission)
const { params, setPage, setSearch, setSort } = usePagination()
const [searchInput, setSearchInput] = useState(params.q ?? '')
const [filters, setFilters] = useState<RepairFilters>(DEFAULT_FILTERS)
// Build query params with filters
const queryParams: Record<string, unknown> = { ...params }
if (filters.status?.length) queryParams.status = filters.status.join(',')
if (filters.conditionIn?.length) queryParams.conditionIn = filters.conditionIn.join(',')
if (filters.isBatch !== undefined) queryParams.isBatch = String(filters.isBatch)
if (filters.intakeDateFrom) queryParams.intakeDateFrom = filters.intakeDateFrom
if (filters.intakeDateTo) queryParams.intakeDateTo = filters.intakeDateTo
if (filters.promisedDateFrom) queryParams.promisedDateFrom = filters.promisedDateFrom
if (filters.promisedDateTo) queryParams.promisedDateTo = filters.promisedDateTo
const { data, isLoading } = useQuery(repairTicketListOptions(queryParams as PaginationInput))
function handleSearchSubmit(e: React.FormEvent) {
e.preventDefault()
setSearch(searchInput)
}
function handleRowClick(ticket: RepairTicket) {
navigate({ to: '/repairs/$ticketId', params: { ticketId: ticket.id }, search: {} as any })
}
return (
<div className="space-y-6">
<div className="flex items-center justify-between">
<h1 className="text-2xl font-bold">Repairs</h1>
{hasPermission('repairs.edit') && (
<Button onClick={() => navigate({ to: '/repairs/new' })}>
<Plus className="mr-2 h-4 w-4" />
New Repair
</Button>
)}
</div>
<form onSubmit={handleSearchSubmit} className="flex gap-2 max-w-sm">
<div className="relative flex-1">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
<Input
placeholder="Search repairs..."
value={searchInput}
onChange={(e) => setSearchInput(e.target.value)}
className="pl-9"
/>
</div>
<Button type="submit" variant="secondary">Search</Button>
</form>
<RepairFilterPanel filters={filters} onChange={setFilters} />
<DataTable
columns={columns}
data={data?.data ?? []}
loading={isLoading}
page={params.page}
totalPages={data?.pagination.totalPages ?? 1}
total={data?.pagination.total ?? 0}
sort={params.sort}
order={params.order}
onPageChange={setPage}
onSort={setSort}
onRowClick={handleRowClick}
/>
</div>
)
}