'use client'; import { useEffect, useMemo, useState } from 'react'; import Link from 'next/link'; import { Plus, RefreshCw, Search } from 'lucide-react'; import { toast } from 'sonner'; import { adminService } from '@/services/api/adminService'; import { formatCurrency } from '@/lib/utils'; 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: 'CLOSED', label: 'Closed' }, ] as const; type QueueTab = 'all' | 'open' | 'closed'; function isClosedStatus(status?: string) { const s = (status || '').toUpperCase(); return s === 'CLOSED' || s === 'CANCELLED'; } function getStatusBadge(status?: string) { const s = (status || '').toUpperCase(); let label = s.replace(/_/g, ' '); let color = 'bg-gray-100 text-gray-700 border-gray-200'; const match = STATUS_OPTIONS.find((opt) => opt.value === s); if (match) label = match.label; if (s.includes('PENDING') || s.includes('BOOKING')) { color = 'bg-rose-50 text-rose-600 border-rose-200'; } else if (s.includes('QUOTE') || s.includes('INSPECTION')) { color = 'bg-purple-50 text-purple-600 border-purple-200'; } else if (s.includes('PAYMENT') || s.includes('FINAL')) { color = 'bg-amber-50 text-amber-700 border-amber-200'; } else if (s.includes('BOOKED') || s.includes('PICKUP') || s.includes('TRANSIT')) { color = 'bg-sky-50 text-sky-600 border-sky-200'; } else if (s.includes('REPAIR') || s.includes('RECEIVED') || s.includes('INTAKE')) { color = 'bg-blue-50 text-blue-600 border-blue-200'; } else if (s.includes('COMPLETED') || s.includes('CLOSED')) { color = 'bg-emerald-50 text-emerald-600 border-emerald-200'; } return ( {label} ); } export default function AdminServicesDashboard() { const [jobs, setJobs] = useState([]); const [loading, setLoading] = useState(true); const [searchQuery, setSearchQuery] = useState(''); const [queueTab, setQueueTab] = useState('all'); const [filterStatus, setFilterStatus] = useState('ALL'); const [updatingJobId, setUpdatingJobId] = useState(null); const loadJobs = 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) { toast.error(err.message || 'Failed to load service jobs'); } finally { setLoading(false); } }; 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}`; } useEffect(() => { loadJobs(); }, []); const [proofModalOpen, setProofModalOpen] = useState(false); const [proofJobId, setProofJobId] = useState(null); const [proofTargetStatus, setProofTargetStatus] = useState<'INSPECTION_COMPLETED' | 'READY_FOR_DELIVERY'>('INSPECTION_COMPLETED'); const handleStatusChange = async (jobId: string, newStatus: string) => { if (newStatus === 'INSPECTION_COMPLETED' || newStatus === 'READY_FOR_DELIVERY') { setProofJobId(jobId); setProofTargetStatus(newStatus); setProofModalOpen(true); return; } const previous = jobs.find((j) => j.job_id === jobId)?.status; setJobs((prev) => prev.map((j) => (j.job_id === jobId ? { ...j, status: newStatus } : j))); setUpdatingJobId(jobId); try { await adminService.updateServiceJobStatus(jobId, newStatus); toast.success('Service job status saved'); } catch (err: any) { setJobs((prev) => prev.map((j) => (j.job_id === jobId ? { ...j, status: previous } : j))); toast.error(err.message || 'Could not update the job status'); } finally { setUpdatingJobId(null); } }; const openCount = jobs.filter((j) => !isClosedStatus(j.status)).length; const closedCount = jobs.filter((j) => isClosedStatus(j.status)).length; const filteredJobs = useMemo(() => { const q = searchQuery.trim().toLowerCase(); return jobs.filter((job) => { const tabMatch = queueTab === 'all' || (queueTab === 'open' && !isClosedStatus(job.status)) || (queueTab === 'closed' && isClosedStatus(job.status)); const statusMatch = filterStatus === 'ALL' || job.status === filterStatus; const haystack = [ job.job_no, job.customer_name, job.customer_phone, job.customer_email, job.device_brand, job.device_model, job.service_name, job.status, ] .filter(Boolean) .join(' ') .toLowerCase(); return tabMatch && statusMatch && (!q || haystack.includes(q)); }); }, [jobs, searchQuery, queueTab, filterStatus]); const pager = useClientPagination(filteredJobs); 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 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 inputClass = 'h-9 px-3 bg-card border border-border crm-radius-control text-[13px] text-foreground focus:outline-none focus:border-primary'; return (

Service Jobs Queue

{jobs.length}
New walk-in
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: 'open', label: `Open (${openCount})` }, { id: 'closed', label: `Closed (${closedCount})` }, ] as { id: QueueTab; label: string }[] ).map((tab) => ( ))}
({ value: opt.value, label: opt.label })), ]} />
{loading ? (
Loading jobs...
) : (
{filteredJobs.length === 0 ? ( ) : ( pager.items.map((job) => ( )) )}
Job no. Customer Device / service Price Status Date
{searchQuery || queueTab !== 'all' || filterStatus !== 'ALL' ? 'No jobs match your search.' : 'No service jobs found.'}
{job.job_no || job.job_id}

{job.customer_name || 'Guest customer'}

{job.customer_phone && job.customer_phone !== 'N/A' ? job.customer_phone : job.customer_email && job.customer_email !== 'N/A' ? job.customer_email : '—'}

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

{job.service_name || '—'}

{typeof job.base_price === 'number' ? formatCurrency(job.base_price) : '—'} {getStatusBadge(job.status)} {job.created_at ? new Date(job.created_at).toLocaleDateString() : '—'}
)}
{proofJobId && ( { setProofModalOpen(false); setProofJobId(null); }} jobId={proofJobId} targetStatus={proofTargetStatus} onSuccess={() => { loadJobs(); }} /> )}
); }