import { useState, useMemo } from 'react'; import { pb } from '../../lib/pocketbase'; import { Search, X, ChevronRight, AlertTriangle } from 'lucide-react'; import type { TechnicianAssignment, AvailableRepairOrder } from '../../lib/technicianData'; import { fetchTechnicianAssignments, fetchAvailableRepairOrders, createTechnicianAssignment, clockElapsedMinutes, parseRepairOrderServices } from '../../lib/technicianData'; import { useTechnicianContext, useTechnicianNav } from '../../components/technician/TechnicianContext'; import { useToast } from '../../components/ui/Toast'; import { ListSkeleton } from '../../components/ui/ListSkeleton'; import { ListError } from '../../components/ui/ListError'; import { formatElapsed } from '../../lib/format'; import { usePagedList } from '../../hooks/usePagedList'; type StatusTab = 'all' | 'active' | 'completed' | 'available'; export default function TechnicianJobs() { const { navigateTech } = useTechnicianNav(); const { showToast } = useToast(); const { effectiveUserId, isImpersonating } = useTechnicianContext(); const authUserId = pb.authStore.model?.id as string | undefined; const shopUserId = isImpersonating ? authUserId : (pb.authStore.model as Record)?.shopUserId as string | undefined; const { items: assignments, loading, error: assignmentsError, } = usePagedList( (page, perPage) => { if (!effectiveUserId) return Promise.resolve({ items: [], page: 1, perPage, totalItems: 0, totalPages: 1 }); return fetchTechnicianAssignments(effectiveUserId, page, perPage); }, 50, ); const { items: availableRDs, loading: availableLoading, error: availableError, } = usePagedList( (page, perPage) => { if (!shopUserId) return Promise.resolve({ items: [], page: 1, perPage, totalItems: 0, totalPages: 1 }); return fetchAvailableRepairOrders(shopUserId, page, perPage); }, 50, ); const [search, setSearch] = useState(''); const [statusTab, setStatusTab] = useState('active'); const [claimingId, setClaimingId] = useState(null); const [claimError, setClaimError] = useState(null); const filtered = useMemo(() => { let list = assignments; if (statusTab === 'active') { list = list.filter( (a) => a.status === 'pending' || a.status === 'in_progress' || a.status === 'waiting_parts', ); } else if (statusTab === 'completed') { list = list.filter((a) => a.status === 'completed'); } if (search.trim()) { const q = search.toLowerCase(); list = list.filter( (a) => a.roNumber?.toLowerCase().includes(q) || a.vehicleInfo?.toLowerCase().includes(q) || a.vin?.toLowerCase().includes(q), ); } return list; }, [assignments, statusTab, search]); const assignedROIds = useMemo(() => { return new Set(assignments.map((a) => a.repairOrderId)); }, [assignments]); const unclaimedRDs = useMemo(() => { return availableRDs.filter((ro) => !assignedROIds.has(ro.id)); }, [availableRDs, assignedROIds]); const handleClaim = async (ro: AvailableRepairOrder) => { setClaimError(null); if (!effectiveUserId) { showToast('You must be signed in to claim a job.', 'error'); return; } const shopUserId = isImpersonating ? authUserId : (pb.authStore.model as Record)?.shopUserId as string | undefined; if (!shopUserId) { showToast('Your account is not linked to a shop. Contact your shop owner.', 'error'); return; } setClaimingId(ro.id); try { const full = await pb.collection('repairOrders').getOne(ro.id) as Record; const serviceList = parseRepairOrderServices(full.services).map((s) => ({ id: s.id, name: s.name, description: s.description || '', status: s.status || 'pending', })); if (serviceList.length === 0) { showToast('This repair order has no services to claim.', 'error'); setClaimingId(null); return; } const allIds = serviceList.map((s: { id: string }) => s.id); const assignment = await createTechnicianAssignment( { id: ro.id, roNumber: (full.roNumber as string) || '', vehicleInfo: (full.vehicleInfo as string) || '', vin: (full.vin as string) || '', mileage: (full.mileage as string) || '', serviceLocation: (full.serviceLocation as string) || '', services: serviceList, }, effectiveUserId, allIds, shopUserId, ); showToast('Job claimed successfully.', 'success'); navigateTech(`/technician/jobs/${assignment.id}`); } catch (err) { console.error('Failed to claim job', err); showToast('Failed to claim job. Please try again.', 'error'); setClaimingId(null); } }; const tabs: { id: StatusTab; label: string }[] = [ { id: 'active', label: 'Active' }, { id: 'available', label: 'Available' }, { id: 'all', label: 'All' }, { id: 'completed', label: 'Completed' }, ]; return (
{/* Search */}
setSearch(e.target.value)} className="w-full rounded-xl border border-gray-200 bg-white py-3 pl-10 pr-10 text-sm text-gray-900 placeholder-gray-400 focus:border-green-400 focus:outline-none focus:ring-2 focus:ring-green-100 dark:border-gray-800 dark:bg-gray-900 dark:text-white dark:placeholder-gray-500 dark:focus:border-green-600 dark:focus:ring-green-900/30" /> {search && ( )}
{/* Tabs */}
{tabs.map((tab) => ( ))}
{/* Error banner */} {claimError && (
{claimError}
)} {/* List */} {statusTab === 'available' ? ( availableLoading ? ( ) : availableError ? ( window.location.reload()} /> ) : unclaimedRDs.length === 0 ? (

No active repair orders available to claim.

) : (
{unclaimedRDs.map((ro) => { const statusBadge: Record = { active: 'bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-400', in_progress: 'bg-yellow-100 text-yellow-700 dark:bg-yellow-900/30 dark:text-yellow-400', waiting_parts: 'bg-orange-100 text-orange-700 dark:bg-orange-900/30 dark:text-orange-400', waiting_pickup: 'bg-teal-100 text-teal-700 dark:bg-teal-900/30 dark:text-teal-400', }; const badge = statusBadge[ro.status] ?? 'bg-gray-100 text-gray-600 dark:bg-gray-800 dark:text-gray-400'; return (
{ro.roNumber || '—'} {ro.status.replace(/_/g, ' ')}

{ro.vehicleInfo || 'No vehicle info'}

{ro.vin && VIN {ro.vin}} {ro.mileage && {ro.mileage} mi} {ro.serviceCount} service{ro.serviceCount !== 1 ? 's' : ''}
); })}
) ) : loading ? ( ) : assignmentsError ? ( window.location.reload()} /> ) : filtered.length === 0 ? (

{search ? 'No results match your search.' : 'No assigned jobs yet.'}

) : (
{filtered.map((job) => { const running = job.clockEntries.some((e) => !e.end); const statusColors: Record = { pending: 'border-l-gray-300 dark:border-l-gray-700', in_progress: 'border-l-yellow-400 dark:border-l-yellow-600', waiting_parts: 'border-l-orange-400 dark:border-l-orange-600', completed: 'border-l-green-400 dark:border-l-green-600', }; return ( ); })}
)}
); }