Files
lunarfront-app/packages/admin/src/components/station-repairs/repair-tech-view.tsx
ryan 0411df57eb feat: repairs station — technician workbench
- Focused single-ticket workbench with sections: Work, Parts, Photos, Notes
- Template quick-add for line items
- Auto-filters to assigned tickets for the logged-in technician
- Ticket selector when multiple assigned
- Permission routing: repairs.edit → desk view, view-only → tech workbench

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 01:37:01 +00:00

94 lines
3.5 KiB
TypeScript

import { useState } from 'react'
import { useQuery } from '@tanstack/react-query'
import { api } from '@/lib/api-client'
import { usePOSStore } from '@/stores/pos.store'
import type { RepairTicket } from '@/types/repair'
import type { PaginatedResponse } from '@lunarfront/shared/schemas'
import { RepairWorkbench } from './repair-workbench'
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
import { Badge } from '@/components/ui/badge'
import { Wrench } from 'lucide-react'
const STATUS_LABELS: Record<string, string> = {
new: 'New', intake: 'Intake', diagnosing: 'Diagnosing',
pending_approval: 'Pending', approved: 'Approved',
in_progress: 'In Progress', pending_parts: 'Parts',
ready: 'Ready',
}
export function RepairTechView() {
const cashier = usePOSStore((s) => s.cashier)
const [selectedTicketId, setSelectedTicketId] = useState<string | null>(null)
// Fetch tickets assigned to current user (active statuses only)
const { data } = useQuery({
queryKey: ['repair-tickets', 'tech-assigned', cashier?.id],
queryFn: () => api.get<PaginatedResponse<RepairTicket>>('/v1/repair-tickets', {
page: 1,
limit: 50,
sort: 'created_at',
order: 'asc',
q: undefined,
// Filter to active statuses — the API will return all if no status filter, we filter client-side
}),
enabled: !!cashier?.id,
staleTime: 15_000,
refetchInterval: 30_000,
})
// Filter to tickets assigned to this technician in active statuses
const activeStatuses = ['diagnosing', 'pending_approval', 'approved', 'in_progress', 'pending_parts', 'ready']
const myTickets = (data?.data ?? []).filter(t =>
t.assignedTechnicianId === cashier?.id && activeStatuses.includes(t.status)
)
// Auto-select first ticket
if (!selectedTicketId && myTickets.length > 0) {
setSelectedTicketId(myTickets[0].id)
}
if (myTickets.length === 0) {
return (
<div className="flex items-center justify-center h-full text-muted-foreground">
<div className="text-center space-y-2">
<Wrench className="h-12 w-12 mx-auto opacity-20" />
<p className="text-lg font-medium">No assigned tickets</p>
<p className="text-sm">Tickets assigned to you will appear here</p>
</div>
</div>
)
}
return (
<div className="flex flex-col h-full">
{/* Ticket selector */}
{myTickets.length > 1 && (
<div className="flex items-center gap-2 px-4 py-2 border-b border-border shrink-0 bg-muted/30">
<span className="text-sm text-muted-foreground">Ticket:</span>
<Select value={selectedTicketId ?? ''} onValueChange={setSelectedTicketId}>
<SelectTrigger className="h-8 w-72">
<SelectValue />
</SelectTrigger>
<SelectContent>
{myTickets.map(t => (
<SelectItem key={t.id} value={t.id}>
<span className="flex items-center gap-2">
#{t.ticketNumber} {t.customerName}
<Badge variant="outline" className="text-[10px]">{STATUS_LABELS[t.status] ?? t.status}</Badge>
</span>
</SelectItem>
))}
</SelectContent>
</Select>
<span className="text-xs text-muted-foreground">{myTickets.length} active</span>
</div>
)}
{/* Workbench */}
<div className="flex-1 min-h-0 overflow-hidden">
{selectedTicketId && <RepairWorkbench ticketId={selectedTicketId} />}
</div>
</div>
)
}