'use client'; import { useEffect, useMemo, useState } from 'react'; import { AlertTriangle, CheckCircle2, CreditCard, Eye, Receipt, RefreshCw, Search } from 'lucide-react'; import { format } from 'date-fns'; import { toast } from 'sonner'; import { adminService } from '@/services/api/adminService'; import { formatCurrency } from '@/lib/utils'; import { SlideOver } from '@/components/ui/SlideOver'; import { StatsSparklineCard, datesToSparkline, weekOverWeekChange } from '@/components/ui/StatsSparklineCard'; import { TablePagination, useClientPagination } from '@/components/ui/TablePagination'; type InvoiceTab = 'ALL' | 'FINAL_PAYMENT_PENDING' | 'READY_FOR_DELIVERY' | 'CLOSED'; function getQuote(detail: any) { if (!detail) return null; if (detail.quote) return detail.quote; if (detail.active_quote) return detail.active_quote; if (Array.isArray(detail.quotes) && detail.quotes.length > 0) return detail.quotes[0]; return null; } function getPayments(detail: any): any[] { if (!detail) return []; if (Array.isArray(detail.payments)) return detail.payments; if (Array.isArray(detail.payment_records)) return detail.payment_records; return []; } function paymentTypeLabel(type: string) { switch (type) { case 'DEPOSIT': return 'Booking deposit'; case 'FINAL': return 'Final settlement'; case 'PARTIAL': return 'Partial payment'; default: return type || 'Payment'; } } function jobStatusBadgeClass(status?: string) { const s = (status || '').toUpperCase(); if (s === 'CLOSED' || s === 'REPAIR_COMPLETED') return 'bg-success text-white'; if (s === 'CANCELLED' || s === 'BOOKING_PENDING') return 'bg-destructive text-white'; if (s === 'FINAL_PAYMENT_PENDING' || s === 'QUOTE_SENT' || s === 'REPAIR_IN_PROGRESS') return 'bg-warning text-white'; if (s === 'READY_FOR_DELIVERY' || s === 'DEVICE_INTAKE' || s === 'QUOTE_ACCEPTED') return 'bg-primary text-white'; return 'bg-muted text-muted-foreground'; } function paymentBadgeClass(status?: string) { const s = (status || '').toUpperCase(); if (s === 'PAID') return 'bg-success text-white'; if (s === 'FAILED' || s === 'REFUNDED') return 'bg-destructive text-white'; if (s === 'PENDING') return 'bg-warning text-white'; return 'bg-muted text-muted-foreground'; } function formatStatus(status?: string) { return (status || 'Pending').replace(/_/g, ' '); } 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 ServiceInvoicesPage() { const [jobs, setJobs] = useState([]); const [loading, setLoading] = useState(true); const [searchQuery, setSearchQuery] = useState(''); const [filterStatus, setFilterStatus] = useState('ALL'); const [selectedJobId, setSelectedJobId] = useState(null); const [detail, setDetail] = useState(null); const [loadingDetail, setLoadingDetail] = useState(false); 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 jobs.'); } finally { setLoading(false); } }; useEffect(() => { loadJobs(); }, []); const openInvoice = async (jobId: string) => { setSelectedJobId(jobId); setDetail(null); setLoadingDetail(true); try { const next = await adminService.getServiceJobDetailsAdmin(jobId); setDetail(next); } catch (err: any) { toast.error(err.message || 'Failed to load invoice detail.'); setSelectedJobId(null); } finally { setLoadingDetail(false); } }; const closeInvoice = () => { setSelectedJobId(null); setDetail(null); }; const pendingCount = jobs.filter((j) => j.status === 'FINAL_PAYMENT_PENDING').length; const deliveryCount = jobs.filter((j) => j.status === 'READY_FOR_DELIVERY').length; const closedCount = jobs.filter((j) => j.status === 'CLOSED').length; const allJobDates = useMemo(() => jobs.map((j) => j.created_at), [jobs]); const pendingJobDates = useMemo( () => jobs.filter((j) => j.status === 'FINAL_PAYMENT_PENDING').map((j) => j.created_at), [jobs] ); const closedJobDates = useMemo( () => jobs.filter((j) => j.status === 'CLOSED').map((j) => j.created_at), [jobs] ); const filteredJobs = useMemo(() => { const q = searchQuery.trim().toLowerCase(); return jobs.filter((job) => { 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 statusMatch && (!q || haystack.includes(q)); }); }, [jobs, searchQuery, filterStatus]); const pager = useClientPagination(filteredJobs); const quote = getQuote(detail); const payments = getPayments(detail); const customerName = detail?.customer?.name || detail?.customer_name || 'Guest customer'; 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 renderJobBadge = (status?: string) => ( {formatStatus(status)} ); const renderPaymentBadge = (status?: string) => ( {formatStatus(status)} ); return (

Service Invoices

{jobs.length}
setFilterStatus('ALL')} /> setFilterStatus('FINAL_PAYMENT_PENDING')} /> setFilterStatus('CLOSED')} />
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: 'FINAL_PAYMENT_PENDING', label: `Payment pending (${pendingCount})` }, { id: 'READY_FOR_DELIVERY', label: `Ready for delivery (${deliveryCount})` }, { id: 'CLOSED', label: `Closed (${closedCount})` }, ] as { id: InvoiceTab; label: string }[] ).map((tab) => ( ))}
{loading ? (
Loading invoices...
) : (
{filteredJobs.length === 0 ? ( ) : ( pager.items.map((job) => ( openInvoice(job.job_id)} > )) )}
Job no. Customer Device Service Status Base price Action
{searchQuery || filterStatus !== 'ALL' ? 'No jobs match your search.' : 'No service invoices 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 || '—'} {renderJobBadge(job.status)} {money(job.base_price)}
)}
} > {loadingDetail ? (
Loading invoice...
) : !detail ? (
Failed to load invoice data.
) : (

{customerName}

{formatDeviceName(detail.device_brand, detail.device_model)} {detail.service_name ? ` · ${detail.service_name}` : ''}

{renderJobBadge(detail.status)}
{quote ? (

Cost estimate

{detail.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 (18%) {money(quote.tax)}
Total {money(quote.total)}
) : (

No quote generated yet.

)}

Payments

{payments.length > 0 ? ( payments.map((p: any, idx: number) => (

{paymentTypeLabel(p.payment_type)}

{p.paid_at && (

{format(new Date(p.paid_at), 'dd MMM yyyy')}

)}

{money(p.amount)}

{renderPaymentBadge(p.status)}
)) ) : (

No payments recorded for this job yet.

)}
)}
); }