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.
176 lines
5.9 KiB
TypeScript
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>
|
|
)
|
|
}
|