- 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>
94 lines
3.5 KiB
TypeScript
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>
|
|
)
|
|
}
|