/** * @page Service bookings list (`app/services/tracking/page.tsx`) * @purpose Look up a job number and list the customer's bookings. Details open on a separate page. */ 'use client'; import { Suspense, useEffect, useMemo, useState } from 'react'; import { useRouter, useSearchParams } from 'next/navigation'; import Link from 'next/link'; import { formatCurrency } from '@/lib/utils'; import { serviceBookingService } from '@/services/api/serviceBooking'; import { setAccessToken } from '@/services/api/client'; import { useAuthStore } from '@/store/authStore'; import { Clock, Loader2, Search, ChevronRight, Smartphone, KeyRound } from 'lucide-react'; import { toast } from 'sonner'; import { AnnouncementBar } from '@/layout/AnnouncementBar'; import { Header } from '@/layout/Header'; import { Navbar } from '@/layout/Navbar'; import { Footer } from '@/layout/Footer'; import { StickyHeaderSpacer } from '@/components/StickyHeaderSpacer'; import { FloatingButtons } from '@/components/FloatingButtons'; import GoogleAuthModal from '@/components/auth/GoogleAuthModal'; function unwrapJob(raw: any): any { if (!raw) return null; if (raw.job && typeof raw.job === 'object') return { ...raw, ...raw.job }; return raw; } function statusBadgeClass(status?: string) { const s = String(status || '').toUpperCase(); if (s === 'CLOSED' || s === 'DELIVERED' || s === 'BOOKED') { return 'border-emerald-200 text-emerald-700 bg-emerald-50'; } if (s === 'CANCELLED' || s === 'BOOKING_PENDING' || s === 'PENDING') { return 'border-amber-200 text-amber-700 bg-amber-50'; } if (s === 'FINAL_PAYMENT_PENDING') { return 'border-red-200 text-red-700 bg-red-50'; } return 'border-primary/20 text-primary bg-primary/5'; } function formatStatus(status?: string) { return String(status || 'UNKNOWN').replace(/_/g, ' '); } function firstPayment(job: any): any | null { if (!job) return null; const list = Array.isArray(job.payments) ? job.payments : Array.isArray(job.payment_records) ? job.payment_records : job.payment ? [job.payment] : []; return list[0] || null; } function resolvePayment(job: any) { const payment = firstPayment(job); const basePrice = Number(job?.base_price ?? job?.quoted_price ?? 0); const paidFromPayment = Number(payment?.amount ?? payment?.paid_amount ?? 0); const paidAmount = paidFromPayment > 0 ? paidFromPayment : basePrice > 0 ? basePrice * 0.2 : 0; const status = payment?.status || payment?.payment_status || (String(job?.status || '').toUpperCase() === 'BOOKING_PENDING' ? 'PENDING' : paidAmount > 0 ? 'CAPTURED' : '—'); return { paidAmount, status }; } function jobDetailPath(item: { job_id?: string; job_no?: string }, fallback?: string) { const id = item.job_id || item.job_no || fallback || ''; return id ? `/services/tracking/${encodeURIComponent(id)}` : ''; } function TrackingListContent() { const router = useRouter(); const searchParams = useSearchParams(); const { isAuthenticated, accessToken, authReady, bootstrapAuth } = useAuthStore(); const [inputJobNo, setInputJobNo] = useState(''); const [debouncedJobNo, setDebouncedJobNo] = useState(''); const [myJobs, setMyJobs] = useState([]); const [loadingMyJobs, setLoadingMyJobs] = useState(false); const [authModalOpen, setAuthModalOpen] = useState(false); useEffect(() => { const timer = window.setTimeout(() => { setDebouncedJobNo(inputJobNo.trim()); }, 350); return () => window.clearTimeout(timer); }, [inputJobNo]); const filteredJobs = useMemo(() => { const q = debouncedJobNo.toLowerCase(); if (!q) return myJobs; return myJobs.filter((job) => { const haystack = [ job.job_no, job.job_id, job.service_name, job.device_brand, job.device_model, job.status, ] .filter(Boolean) .join(' ') .toLowerCase(); return haystack.includes(q); }); }, [myJobs, debouncedJobNo]); useEffect(() => { const fromQuery = searchParams.get('job_id') || searchParams.get('job_no'); if (fromQuery) { router.replace(`/services/tracking/${encodeURIComponent(fromQuery)}`); } }, [searchParams, router]); useEffect(() => { void bootstrapAuth(); }, [bootstrapAuth]); useEffect(() => { if (accessToken) setAccessToken(accessToken); }, [accessToken]); useEffect(() => { if (!authReady || !isAuthenticated) { setMyJobs([]); setLoadingMyJobs(false); return; } void loadMyBookings(); }, [authReady, isAuthenticated, accessToken]); const loadMyBookings = async () => { setLoadingMyJobs(true); const byKey = new Map(); const addJob = (raw: any) => { const item = unwrapJob(raw); if (!item) return; const key = String(item.job_id || item.job_no || ''); if (!key) return; byKey.set(key, item); }; try { const token = useAuthStore.getState().accessToken; if (token) setAccessToken(token); const local = serviceBookingService.readLocalJobs(); local.forEach((ref) => { if (ref.job_id || ref.job_no) { addJob({ job_id: ref.job_id, job_no: ref.job_no, status: 'BOOKED', service_name: 'Device repair', }); } }); await Promise.all( local.map(async (ref) => { const id = ref.job_id || ref.job_no; if (!id) return; try { addJob(await serviceBookingService.fetchServiceJob(id)); } catch { /* keep stub */ } }) ); const sorted = Array.from(byKey.values()).sort((a, b) => { const ta = new Date(a.created_at || a.updated_at || 0).getTime(); const tb = new Date(b.created_at || b.updated_at || 0).getTime(); return tb - ta; }); setMyJobs(sorted); } finally { setLoadingMyJobs(false); } }; const openJobPage = (item?: { job_id?: string; job_no?: string }, fallback?: string) => { const path = jobDetailPath(item || {}, fallback); if (!path) return; router.push(path); }; const handleSearchSubmit = (e: React.FormEvent) => { e.preventDefault(); const q = inputJobNo.trim(); if (!q) { toast.warning( 'Please enter a valid Service Job Number (e.g. SRV-1787644172).' ); return; } const match = myJobs.find( (j) => j.job_no === q || j.job_id === q ); openJobPage(match, q); }; if (!authReady) { return (
Loading...
); } if (!isAuthenticated) { return (

Track Your Device Repair

Sign in to see your repair bookings and track job status.

setAuthModalOpen(false)} />
); } return (

Track Your Device Repair

Enter your Service Booking Reference ID (e.g., SRV-1787644172)

setInputJobNo(e.target.value)} className="w-full pl-10 pr-4 py-2.5 bg-white border border-gray-200 rounded-lg text-[13px] font-mono text-[#1a1a1a] outline-none focus:border-primary focus:ring-2 focus:ring-primary/15" />

Your bookings

{loadingMyJobs ? (
Loading your bookings...
) : myJobs.length === 0 ? (
No service bookings found yet.
Book a repair
) : filteredJobs.length === 0 ? (
No bookings match “{debouncedJobNo}”.
) : (
{filteredJobs.map((item) => { const id = item.job_id || item.job_no; const device = [item.device_brand, item.device_model] .filter(Boolean) .join(' '); const pay = resolvePayment(item); return ( ); })}
)}
setAuthModalOpen(false)} />
); } function PageShell({ children }: { children: React.ReactNode }) { return (
{children}
); } export default function ServiceJobTrackingQueryPage() { return ( Loading tracking... } > ); }