'use client'; import { useSearchParams } from 'next/navigation'; import { useEffect, useMemo, useState } from 'react'; import { Calendar, Clock, CreditCard, Eye, ListTodo, Package, RefreshCw, Search, Truck, User, Wrench } from 'lucide-react'; import { toast } from 'sonner'; import { adminService } from '@/services/api/adminService'; import { formatCurrency } from '@/lib/utils'; import { SlideOver } from '@/components/ui/SlideOver'; import { CustomSelect } from '@/components/ui/CustomSelect'; import { TablePagination, useClientPagination } from '@/components/ui/TablePagination'; import { MediaProofModal } from '@/components/ui/MediaProofModal'; const STATUS_OPTIONS = [ { value: 'BOOKING_PENDING', label: 'Pending deposit' }, { value: 'BOOKED', label: 'Booked' }, { value: 'BOOKED_PICKUP', label: 'Booked pickup' }, { value: 'PICKUP_SCHEDULED', label: 'Pickup scheduled' }, { value: 'OUT_FOR_PICKUP', label: 'Out for pickup' }, { value: 'DEVICE_PICKED_UP', label: 'Device picked up' }, { value: 'IN_TRANSIT', label: 'In transit to store' }, { value: 'DELIVERED_TO_STORE', label: 'Delivered to store' }, { value: 'DEVICE_RECEIVED', label: 'Device received' }, { value: 'DEVICE_INTAKE', label: 'Intake complete' }, { value: 'INSPECTION_PENDING', label: 'Awaiting inspection' }, { value: 'INSPECTION_COMPLETED', label: 'Inspection done' }, { value: 'QUOTE_SENT', label: 'Quote sent' }, { value: 'QUOTE_ACCEPTED', label: 'Quote approved' }, { value: 'REPAIR_IN_PROGRESS', label: 'In repair' }, { value: 'REPAIR_COMPLETED', label: 'Repair complete' }, { value: 'FINAL_PAYMENT_PENDING', label: 'Payment required' }, { value: 'READY_FOR_DELIVERY', label: 'Ready for delivery' }, { value: 'CLOSED', label: 'Closed' }, ] as const; const BOOKED_PICKUP_STATUS_OPTIONS = [ { value: 'BOOKED_PICKUP', label: 'Booked Pickup' }, { value: 'PICKUP_SCHEDULED', label: 'Pickup Scheduled' }, { value: 'OUT_FOR_PICKUP', label: 'Out for Pickup' }, { value: 'DEVICE_PICKED_UP', label: 'Device Picked Up' }, { value: 'IN_TRANSIT', label: 'In Transit to Store' }, { value: 'DELIVERED_TO_STORE', label: 'Delivered to Store' }, { value: 'READY_FOR_RETURN', label: 'Ready for Return' }, { value: 'RETURN_IN_TRANSIT', label: 'Out for Return Delivery' }, { value: 'DELIVERED_TO_CUSTOMER', label: 'Delivered to Customer' }, ]; type QueueTab = 'all' | 'inspect' | 'repair' | 'closed'; function isPickupFulfillment(type?: string) { const t = (type || '').toUpperCase(); return t === 'DOORSTEP_PICKUP' || t === 'PICKUP'; } function renderFulfillmentBadge(type?: string) { const t = (type || '').toUpperCase(); if (t === 'DOORSTEP_PICKUP' || t === 'PICKUP') { return ( Doorstep Pickup ); } if (t === 'APPOINTMENT_WALK_IN' || t === 'APPOINTMENT') { return ( Appointment Walk-In ); } if (t === 'DELIVERY') { return ( Delivery ); } return ( Store Walk-In ); } function isUnpaidOnlineDraft(job: any): boolean { if (!job) return false; const status = (job.status || '').toUpperCase(); if (status !== 'BOOKING_PENDING') return false; const source = (job.source || '').toUpperCase(); if (source === 'WALK_IN' || source === 'STORE' || job.intake || job.device_intake) return false; const pmntStatus = String( job.payment_status || job.payment?.status || job.payments?.[0]?.status || '' ).toUpperCase(); return !['CAPTURED', 'PAID', 'COMPLETED', 'SUCCESS'].includes(pmntStatus); } function isClosedStatus(status?: string) { const s = (status || '').toUpperCase(); return s === 'CLOSED' || s === 'CANCELLED' || s === 'DELIVERED'; } function isInspectStatus(status?: string, job?: any) { if (job && isUnpaidOnlineDraft(job)) return false; const s = (status || '').toUpperCase(); return ( s === 'BOOKING_PENDING' || s === 'BOOKED' || s === 'BOOKED_PICKUP' || s === 'PICKUP_SCHEDULED' || s === 'OUT_FOR_PICKUP' || s === 'DEVICE_PICKED_UP' || s === 'IN_TRANSIT' || s === 'DELIVERED_TO_STORE' || s === 'DEVICE_RECEIVED' || s === 'DEVICE_INTAKE' || s === 'INSPECTION_PENDING' || s === 'INSPECTION_COMPLETED' || s === 'QUOTE_SENT' || s === 'QUOTE_ACCEPTED' ); } function isRepairStatus(status?: string) { const s = (status || '').toUpperCase(); return !isClosedStatus(s) && !isInspectStatus(s); } function statusBadgeClass(status?: string) { const s = (status || '').toUpperCase(); if (s === 'CLOSED' || s === 'REPAIR_COMPLETED' || s === 'READY_FOR_DELIVERY') return 'bg-success text-white'; if (s === 'CANCELLED' || s === 'BOOKING_PENDING') return 'bg-destructive text-white'; if (s === 'REPAIR_IN_PROGRESS' || s === 'INSPECTION_PENDING' || s === 'QUOTE_SENT' || s === 'FINAL_PAYMENT_PENDING') { return 'bg-warning text-white'; } if (s === 'DEVICE_INTAKE' || s === 'DEVICE_RECEIVED' || s === 'QUOTE_ACCEPTED' || s.includes('PICKUP') || s.includes('TRANSIT')) return 'bg-primary text-white'; return 'bg-muted text-muted-foreground'; } function formatStatus(status?: string) { if (!status) return 'Pending'; const s = status.toUpperCase(); if (s === 'BOOKED_PICKUP') return 'Booked pickup'; if (s === 'PICKUP_SCHEDULED') return 'Pickup scheduled'; if (s === 'OUT_FOR_PICKUP') return 'Out for pickup'; if (s === 'DEVICE_PICKED_UP') return 'Device picked up'; if (s === 'IN_TRANSIT') return 'In transit to store'; if (s === 'DELIVERED_TO_STORE') return 'Delivered to store'; return status.replace(/_/g, ' '); } function getQuote(job: any) { if (!job) return null; if (job.quote) return job.quote; if (job.active_quote) return job.active_quote; if (Array.isArray(job.quotes) && job.quotes.length > 0) return job.quotes[0]; return null; } function getIntake(job: any) { if (!job) return null; return job.intake || job.device_intake || job.intake_details || null; } function getInspection(job: any) { if (!job) return null; if (job.inspection) return job.inspection; if (Array.isArray(job.inspections) && job.inspections.length > 0) { return job.inspections[job.inspections.length - 1]; } return null; } function getCustomerName(job: any) { return job?.customer_name || job?.customer?.name || 'Guest customer'; } function getCustomerContact(job: any) { const phone = job?.customer_phone || job?.customer?.phone; const email = job?.customer_email || job?.customer?.email; if (phone && phone !== 'N/A') return phone; if (email && email !== 'N/A') return email; return '—'; } function money(value: unknown) { const n = typeof value === 'number' ? value : parseFloat(String(value ?? '')); return Number.isFinite(n) ? formatCurrency(n) : '—'; } function formatDeviceName(brand?: string, model?: string, fallback = '—') { const b = (brand || '').trim(); const m = (model || '').trim(); if (!b) return m || fallback; if (!m) return b || fallback; if (m.toLowerCase().startsWith(b.toLowerCase())) return m; return `${b} ${m}`; } export default function TechnicianPortalPage() { const searchParams = useSearchParams(); const targetJobId = searchParams.get('job_id') || searchParams.get('job'); const [jobs, setJobs] = useState([]); const [loading, setLoading] = useState(true); const [searchQuery, setSearchQuery] = useState(''); const [queueTab, setQueueTab] = useState('all'); const [activeJobId, setActiveJobId] = useState(null); const [activeJob, setActiveJob] = useState(null); const [loadingDetail, setLoadingDetail] = useState(false); const [updatingStatus, setUpdatingStatus] = useState(false); const [submittingWorkspace, setSubmittingWorkspace] = useState(false); const [courierName, setCourierName] = useState(''); const [awbNumber, setAwbNumber] = useState(''); const [pickupStatus, setPickupStatus] = useState('BOOKED_PICKUP'); const [savingLogistics, setSavingLogistics] = useState(false); const [inspectionData, setInspectionData] = useState({ result: 'CUSTOMER_REPORT_CONFIRMED', customer_report: '', confirmed_damage: '', additional_damage: '', notes: '', }); const [additionalCost, setAdditionalCost] = useState('0'); const loadAssignedJobs = async () => { setLoading(true); try { const data = await adminService.listServiceJobs(); const seen = new Set(); const unique = (data as any[]).filter((j) => { if (!j?.job_id || seen.has(j.job_id)) return false; seen.add(j.job_id); return true; }); setJobs(unique); } catch (err: any) { console.warn('Technician queue load:', err.message); } finally { setLoading(false); } }; useEffect(() => { loadAssignedJobs(); if (targetJobId) { handleSelectJob(targetJobId); } }, [targetJobId]); const handleSelectJob = async (jobId: string) => { setActiveJobId(jobId); setActiveJob(null); setLoadingDetail(true); try { const data = await adminService.getServiceJobDetailsAdmin(jobId); setActiveJob(data); setAdditionalCost('0'); setCourierName(data.courier_name || ''); setAwbNumber(data.awb_number || ''); setPickupStatus(data.pickup_status || 'BOOKED_PICKUP'); setInspectionData({ result: 'CUSTOMER_REPORT_CONFIRMED', customer_report: `Customer reported: ${data.service_name || 'repair'}`, confirmed_damage: '', additional_damage: '', notes: '', }); } catch (err: any) { toast.error(err.message || 'Failed to retrieve job details.'); setActiveJobId(null); } finally { setLoadingDetail(false); } }; const handleSaveLogistics = async () => { if (!activeJobId) return; setSavingLogistics(true); try { await adminService.updateServiceJobLogistics(activeJobId, { courier_name: courierName, awb_number: awbNumber, pickup_status: pickupStatus, }); toast.success('Booked pickup & logistics details saved successfully!'); handleSelectJob(activeJobId); loadAssignedJobs(); } catch (err: any) { toast.error(err.message || 'Failed to save logistics details.'); } finally { setSavingLogistics(false); } }; const closeWorkspace = () => { if (updatingStatus || submittingWorkspace) return; setActiveJobId(null); setActiveJob(null); }; const [proofModalOpen, setProofModalOpen] = useState(false); const [proofTargetStatus, setProofTargetStatus] = useState<'INSPECTION_COMPLETED' | 'READY_FOR_DELIVERY'>('INSPECTION_COMPLETED'); const handleStatusChange = async (newStatus: string) => { if (!activeJobId || !activeJob) return; if (newStatus === 'INSPECTION_COMPLETED' || newStatus === 'READY_FOR_DELIVERY') { setProofTargetStatus(newStatus); setProofModalOpen(true); return; } const previous = activeJob.status; setUpdatingStatus(true); setActiveJob({ ...activeJob, status: newStatus }); setJobs((prev) => prev.map((j) => (j.job_id === activeJobId ? { ...j, status: newStatus } : j))); try { await adminService.updateServiceJobStatus(activeJobId, newStatus); toast.success('Technician job status saved'); } catch (err: any) { setActiveJob({ ...activeJob, status: previous }); setJobs((prev) => prev.map((j) => (j.job_id === activeJobId ? { ...j, status: previous } : j))); toast.error(err.message || 'Failed to update status.'); } finally { setUpdatingStatus(false); } }; const handleInspectionSubmit = async (e: React.FormEvent) => { e.preventDefault(); if (!activeJobId || !activeJob) return; setSubmittingWorkspace(true); try { await adminService.submitDiagnosticInspection(activeJobId, { result: inspectionData.result, customer_report: inspectionData.customer_report || null, confirmed_damage: inspectionData.confirmed_damage || null, additional_damage: inspectionData.additional_damage || null, notes: inspectionData.notes || null, }); const baseCost = parseFloat(String(activeJob.base_price)) || 0; const extraCost = inspectionData.result === 'ADDITIONAL_DAMAGE_FOUND' ? parseFloat(additionalCost) || 0 : 0; const subtotal = baseCost; const additional_damage_amount = extraCost; const tax = (subtotal + additional_damage_amount) * 0.18; const total = subtotal + additional_damage_amount + tax; await adminService.createOrReviseQuote(activeJobId, { subtotal, tax: parseFloat(tax.toFixed(2)), additional_damage_amount, total: parseFloat(total.toFixed(2)), reason: inspectionData.result === 'ADDITIONAL_DAMAGE_FOUND' ? `Revised estimate: additional damage found — ${inspectionData.additional_damage}` : `Standard repair quote for ${activeJob.service_name}`, }); const updatedDetail = await adminService.getServiceJobDetailsAdmin(activeJobId); setActiveJob(updatedDetail); toast.success('Inspection saved and quote generated'); loadAssignedJobs(); } catch (err: any) { toast.error(err.message || 'Failed to submit inspection details.'); } finally { setSubmittingWorkspace(false); } }; const validJobs = useMemo(() => jobs.filter((j) => !isUnpaidOnlineDraft(j)), [jobs]); const inspectCount = validJobs.filter((j) => isInspectStatus(j.status, j)).length; const repairCount = validJobs.filter((j) => isRepairStatus(j.status)).length; const closedCount = validJobs.filter((j) => isClosedStatus(j.status)).length; const filteredJobs = useMemo(() => { const q = searchQuery.trim().toLowerCase(); return jobs.filter((job) => { // Unpaid online drafts are hidden from active queues unless searching explicitly by query if (isUnpaidOnlineDraft(job) && !q) return false; const tabMatch = queueTab === 'all' || (queueTab === 'inspect' && isInspectStatus(job.status, job)) || (queueTab === 'repair' && isRepairStatus(job.status)) || (queueTab === 'closed' && isClosedStatus(job.status)); const haystack = [ job.job_no, getCustomerName(job), getCustomerContact(job), job.device_brand, job.device_model, job.service_name, job.status, ] .filter(Boolean) .join(' ') .toLowerCase(); return tabMatch && (!q || haystack.includes(q)); }); }, [jobs, searchQuery, queueTab]); const pager = useClientPagination(filteredJobs); const quote = getQuote(activeJob); const intake = getIntake(activeJob); const lastInspection = getInspection(activeJob); const showInspectionForm = activeJob && isInspectStatus(activeJob.status, activeJob) && !isUnpaidOnlineDraft(activeJob); const dataCardShell = 'crm-radius-section border border-border bg-card shadow-[0_1px_3px_rgba(16,24,40,0.06)] overflow-hidden min-w-0'; const inputClass = 'w-full h-9 px-3 bg-card border border-border crm-radius-control text-[13px] text-foreground focus:outline-none focus:border-primary disabled:opacity-50'; const labelClass = 'block text-[11px] font-semibold text-muted-foreground uppercase mb-1.5'; const primaryButton = 'inline-flex items-center justify-center gap-2 h-9 px-4 crm-radius-control bg-primary hover:bg-primary/90 text-white text-[13px] font-semibold cursor-pointer disabled:opacity-50'; const secondaryButton = 'inline-flex items-center justify-center gap-2 h-9 px-4 crm-radius-control border border-border bg-card text-foreground text-[13px] font-medium hover:bg-muted cursor-pointer disabled:opacity-50'; const renderStatusBadge = (status?: string) => ( {formatStatus(status)} ); const quoteCard = quote ? (

Quote{quote.version != null ? ` v${quote.version}` : ''}

{quote.status && renderStatusBadge(quote.status)}
{activeJob?.service_name || 'Service'} {money(quote.subtotal)}
{Number(quote.additional_damage_amount) > 0 && (
{quote.additional_damage_description ? `Additional: ${quote.additional_damage_description}` : 'Additional damage'} + {money(quote.additional_damage_amount)}
)}
GST {money(quote.tax)}
Total {money(quote.total)}
{quote.reason &&

{quote.reason}

}
) : null; return (

Technician Workspace

{jobs.length}
setSearchQuery(e.target.value)} className="w-full h-9 pl-9 pr-3 crm-radius-control border border-border bg-card text-[13px] text-foreground placeholder:text-muted-foreground focus:outline-none focus:border-primary transition-colors" />
{( [ { id: 'all', label: `All (${jobs.length})` }, { id: 'inspect', label: `Inspect (${inspectCount})` }, { id: 'repair', label: `Repair (${repairCount})` }, { id: 'closed', label: `Closed (${closedCount})` }, ] as { id: QueueTab; label: string }[] ).map((tab) => ( ))}
{loading ? (
Loading jobs...
) : (
{filteredJobs.length === 0 ? ( ) : ( pager.items.map((job) => ( handleSelectJob(job.job_id)} > )) )}
Job no. Customer Device / service Price Status Action
{searchQuery || queueTab !== 'all' ? 'No jobs match your search.' : 'No jobs currently assigned.'}
{job.job_no || job.job_id}

{getCustomerName(job)}

{getCustomerContact(job)}

{formatDeviceName(job.device_brand, job.device_model)}

{job.service_name || '—'}

{money(job.base_price)} {renderStatusBadge(job.status)}
)}
} > {loadingDetail ? (
Loading job...
) : !activeJob ? (
Failed to load job details.
) : (
{showInspectionForm ? (

{formatDeviceName(activeJob.device_brand, activeJob.device_model, 'Device')}

{activeJob.service_name || '—'} · {getCustomerName(activeJob)}

{renderFulfillmentBadge(activeJob.fulfillment_type)} {renderStatusBadge(activeJob.status)}
{isUnpaidOnlineDraft(activeJob) && (
Advance Deposit Pending (20%)

Customer has initiated this booking but advance deposit payment is still pending. Status controls, pickup logistics, and diagnostic inspection are locked until advance payment is verified.

)} {activeJob.status === 'FINAL_PAYMENT_PENDING' && (
Final Payment Collection Required

Repair is completed. Waiting for final payment collection at device delivery or customer pickup.

)} {!isUnpaidOnlineDraft(activeJob) && activeJob.status !== 'FINAL_PAYMENT_PENDING' && (
({ value: s.value, label: s.label })), ...(!STATUS_OPTIONS.some((s) => s.value === activeJob.status) && activeJob.status ? [{ value: activeJob.status, label: formatStatus(activeJob.status) }] : []), ]} />
)} {/* 2nd Dropdown: Booked Pickup Status - ONLY VISIBLE IF PICKUP & DEPOSIT PAID & NOT FINAL_PAYMENT_PENDING */} {isPickupFulfillment(activeJob.fulfillment_type) && !isUnpaidOnlineDraft(activeJob) && activeJob.status !== 'FINAL_PAYMENT_PENDING' && (
{ setPickupStatus(val); if (!activeJobId) return; setSavingLogistics(true); try { await adminService.updateServiceJobLogistics(activeJobId, { courier_name: courierName, awb_number: awbNumber, pickup_status: val, }); toast.success(`Booked pickup status updated to ${formatStatus(val)}!`); loadAssignedJobs(); } catch (err: any) { toast.error(err.message || 'Failed to update pickup status.'); } finally { setSavingLogistics(false); } }} options={BOOKED_PICKUP_STATUS_OPTIONS} />
Courier & Tracking {activeJob?.fulfillment_type || 'DOORSTEP_PICKUP'}
setCourierName(e.target.value)} onBlur={handleSaveLogistics} className={inputClass} />
setAwbNumber(e.target.value)} onBlur={handleSaveLogistics} className={inputClass} />
)} {intake && (

Intake

{intake.power_status && (

Power: {formatStatus(intake.power_status)}

)} {intake.screen_condition && (

Screen: {formatStatus(intake.screen_condition)}

)} {intake.body_condition && (

Body: {formatStatus(intake.body_condition)}

)} {intake.accessories && (

Accessories: {intake.accessories}

)}
)} {quoteCard}

Diagnostic inspection

setInspectionData({ ...inspectionData, result: value })} options={[ { value: 'CUSTOMER_REPORT_CONFIRMED', label: 'Customer report confirmed' }, { value: 'ADDITIONAL_DAMAGE_FOUND', label: 'Additional damage found' }, { value: 'CUSTOMER_REPORT_NOT_CONFIRMED', label: 'Reported issue not confirmed' }, ]} />