"use client"; import { useState } from "react"; import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; import { useParams, useRouter } from "next/navigation"; import { ArrowLeft, FileText, Ticket, CreditCard, RefreshCw, CheckCircle2, Circle, MapPin, Zap, AlertTriangle, } from "lucide-react"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/Card"; import { Badge } from "@/components/ui/Badge"; import { Button } from "@/components/ui/Button"; import { Modal } from "@/components/ui/Modal"; import { Input } from "@/components/ui/Input"; import { Table, TableHead, TableBody, TableRow, Th, Td, EmptyState } from "@/components/ui/Table"; import { formatDate, formatCurrency } from "@/lib/utils"; import api from "@/lib/api"; import { toast } from "sonner"; import type { Client, Subscription, Invoice, Ticket as TicketType, Payment, LegacyPaginatedResponse } from "@/types"; type Tab = "profile" | "subscriptions" | "invoices" | "payments" | "tickets"; const statusColor: Record = { ACTIVE: "success", active: "success", SUSPENDED: "warning", suspended: "warning", CANCELLED: "danger", cancelled: "danger", DISCONNECTED: "danger", PENDING: "muted", pending: "muted", }; const invColor: Record = { PAID: "success", paid: "success", PARTIAL: "warning", partial: "warning", OVERDUE: "danger", overdue: "danger", SENT: "muted", DRAFT: "muted", VOID: "muted", }; const channelColors: Record = { CASH: "bg-green-100 text-green-700", GCASH: "bg-blue-100 text-blue-700", MAYA: "bg-purple-100 text-purple-700", BANK_TRANSFER: "bg-yellow-100 text-yellow-700", CHECK: "bg-gray-100 text-gray-700", }; // ─── Activation Flow Types ──────────────────────────────────────────────────── interface ActivationState { installationTicketId: string | null; // existing INSTALLATION ticket (OPEN/IN_PROGRESS) installationResolved: boolean; locationPinned: boolean; // client has lat/lng set invoiceCreated: boolean; // at least one invoice exists (prepaid: before activation) activationTicketId: string | null; // the INSTALLATION ticket used for activation step activationResolved: boolean; clientActive: boolean; } // ─── Activation Flow Card ───────────────────────────────────────────────────── function ActivationFlowCard({ client, subscriptions, tickets, invoices, onRefresh, }: { client: Client; subscriptions: Subscription[]; tickets: TicketType[]; invoices: Invoice[]; onRefresh: () => void; }) { const qc = useQueryClient(); const [showPinModal, setShowPinModal] = useState(false); const [pinForm, setPinForm] = useState({ lat: String(client.lat ?? ""), lng: String(client.lng ?? "") }); const [showActivationTicketModal, setShowActivationTicketModal] = useState(false); const pendingSub = subscriptions.find(s => s.status === "PENDING"); const billingType = pendingSub?.type ?? "POSTPAID"; // Derive state from data const installationTickets = tickets.filter(t => t.type === "INSTALLATION"); const openInstallTicket = installationTickets.find(t => t.status === "OPEN" || t.status === "IN_PROGRESS"); const resolvedInstallTickets = installationTickets.filter(t => t.status === "RESOLVED" || t.status === "CLOSED"); // Activation ticket = an INSTALLATION ticket created after the first one is resolved // We identify it by: resolved install exists + there's another ticket also INSTALLATION const activationTicket = resolvedInstallTickets.length > 1 ? resolvedInstallTickets[resolvedInstallTickets.length - 1] // latest resolved = activation : installationTickets.find(t => (t.status === "OPEN" || t.status === "IN_PROGRESS") && resolvedInstallTickets.length > 0 ); const state: ActivationState = { installationTicketId: openInstallTicket?.id ?? null, installationResolved: resolvedInstallTickets.length > 0, locationPinned: !!(client.lat && client.lng), invoiceCreated: invoices.length > 0, activationTicketId: activationTicket?.id ?? null, activationResolved: !!(activationTicket && (activationTicket.status === "RESOLVED" || activationTicket.status === "CLOSED")), clientActive: client.isActive && (!pendingSub || pendingSub.status !== "PENDING"), }; // PREPAID steps: resolve install → pin location → create invoice → create activation ticket → resolve → activate // POSTPAID steps: resolve install → pin location → create activation ticket → resolve → activate → create invoice const isPrepaid = billingType === "PREPAID"; // Mutations const resolveInstallMutation = useMutation({ mutationFn: async (ticketId: string) => { await api.patch(`/api/v1/tickets/${ticketId}`, { status: "RESOLVED" }); }, onSuccess: () => { toast.success("Installation ticket resolved!"); onRefresh(); qc.invalidateQueries({ queryKey: ["client-tickets", client.id] }); }, onError: (e: any) => toast.error(e.response?.data?.message ?? "Failed to resolve ticket"), }); const pinLocationMutation = useMutation({ mutationFn: async () => { const lat = parseFloat(pinForm.lat); const lng = parseFloat(pinForm.lng); if (isNaN(lat) || isNaN(lng)) throw new Error("Invalid coordinates"); await api.patch(`/api/v1/clients/${client.id}`, { lat, lng }); }, onSuccess: () => { toast.success("Location pinned!"); setShowPinModal(false); onRefresh(); qc.invalidateQueries({ queryKey: ["client", client.id] }); }, onError: (e: any) => toast.error(e.response?.data?.message ?? e.message ?? "Failed to save location"), }); const generateInvoiceMutation = useMutation({ mutationFn: async () => { await api.post(`/api/v1/invoices/generate/${client.id}`, {}); }, onSuccess: () => { toast.success("Invoice generated!"); onRefresh(); qc.invalidateQueries({ queryKey: ["client-invoices", client.id] }); }, onError: (e: any) => toast.error(e.response?.data?.message ?? "Failed to generate invoice"), }); const createActivationTicketMutation = useMutation({ mutationFn: async () => { await api.post("/api/v1/tickets", { clientId: client.id, subject: `Activation — ${client.firstName} ${client.lastName} (${client.accountNumber})`, type: "INSTALLATION", priority: "HIGH", }); }, onSuccess: () => { toast.success("Activation ticket created!"); setShowActivationTicketModal(false); onRefresh(); qc.invalidateQueries({ queryKey: ["client-tickets", client.id] }); }, onError: (e: any) => toast.error(e.response?.data?.message ?? "Failed to create ticket"), }); const resolveActivationMutation = useMutation({ mutationFn: async (ticketId: string) => { // 1. Resolve activation ticket await api.patch(`/api/v1/tickets/${ticketId}`, { status: "RESOLVED" }); // 2. Set client to active await api.patch(`/api/v1/clients/${client.id}`, { isActive: true }); // 3. Activate pending subscription if any if (pendingSub) { await api.patch(`/api/v1/clients/${client.id}/subscriptions/${pendingSub.id}/activate`, {}); } }, onSuccess: () => { toast.success("Client activated! 🎉"); onRefresh(); qc.invalidateQueries({ queryKey: ["client", client.id] }); qc.invalidateQueries({ queryKey: ["client-subscriptions", client.id] }); qc.invalidateQueries({ queryKey: ["client-tickets", client.id] }); }, onError: (e: any) => toast.error(e.response?.data?.message ?? "Activation failed"), }); const generatePostpaidInvoiceMutation = useMutation({ mutationFn: async () => { await api.post(`/api/v1/invoices/generate/${client.id}`, {}); }, onSuccess: () => { toast.success("First month invoice generated!"); onRefresh(); qc.invalidateQueries({ queryKey: ["client-invoices", client.id] }); }, onError: (e: any) => toast.error(e.response?.data?.message ?? "Failed to generate invoice"), }); // Build steps based on billing type type StepStatus = "done" | "active" | "pending"; interface Step { label: string; description: string; status: StepStatus; action?: React.ReactNode; } const steps: Step[] = isPrepaid ? [ { label: "Resolve Installation", description: "Mark the installation ticket as resolved", status: state.installationResolved ? "done" : "active", action: !state.installationResolved && state.installationTicketId ? ( ) : undefined, }, { label: "Pin Installation Location", description: "Save the client's GPS coordinates", status: state.locationPinned ? "done" : state.installationResolved ? "active" : "pending", action: !state.locationPinned && state.installationResolved ? ( ) : undefined, }, { label: "Create Invoice", description: "Generate the first month invoice (client pays before activation)", status: state.invoiceCreated ? "done" : (state.installationResolved && state.locationPinned) ? "active" : "pending", action: !state.invoiceCreated && state.installationResolved && state.locationPinned ? ( ) : undefined, }, { label: "Create Activation Ticket", description: "Open a ticket to confirm service activation", status: state.activationTicketId ? "done" : (state.invoiceCreated) ? "active" : "pending", action: !state.activationTicketId && state.invoiceCreated ? ( ) : undefined, }, { label: "Resolve Activation → Client Active", description: "Resolve the activation ticket — client status will be set to Active automatically", status: state.clientActive ? "done" : (state.activationTicketId && !state.activationResolved) ? "active" : "pending", action: state.activationTicketId && !state.activationResolved && !state.clientActive ? ( ) : undefined, }, ] : [ { label: "Resolve Installation", description: "Mark the installation ticket as resolved", status: state.installationResolved ? "done" : "active", action: !state.installationResolved && state.installationTicketId ? ( ) : undefined, }, { label: "Pin Installation Location", description: "Save the client's GPS coordinates", status: state.locationPinned ? "done" : state.installationResolved ? "active" : "pending", action: !state.locationPinned && state.installationResolved ? ( ) : undefined, }, { label: "Create Activation Ticket", description: "Open a ticket to confirm service activation", status: state.activationTicketId ? "done" : (state.installationResolved && state.locationPinned) ? "active" : "pending", action: !state.activationTicketId && state.installationResolved && state.locationPinned ? ( ) : undefined, }, { label: "Resolve Activation → Client Active", description: "Resolve the activation ticket — client status will be set to Active automatically", status: state.clientActive ? "done" : (state.activationTicketId && !state.activationResolved) ? "active" : "pending", action: state.activationTicketId && !state.activationResolved && !state.clientActive ? ( ) : undefined, }, { label: "Generate First Month Invoice", description: "Create the first invoice after client is active (postpaid)", status: (state.clientActive && state.invoiceCreated) ? "done" : state.clientActive ? "active" : "pending", action: state.clientActive && !state.invoiceCreated ? ( ) : undefined, }, ]; const allDone = steps.every(s => s.status === "done"); if (allDone) return null; return ( <>
Activation Flow — {isPrepaid ? "Prepaid" : "Postpaid"} {pendingSub?.plan?.name ?? "Pending"}
    {steps.map((step, i) => (
  1. {step.status === "done" ? ( ) : step.status === "active" ? (
    ) : ( )}

    {step.label}

    {step.status !== "done" && (

    {step.description}

    )}
    {step.action &&
    {step.action}
    }
  2. ))}
{/* Pin Location Modal */} setShowPinModal(false)} title="Pin Installation Location">

Enter the GPS coordinates for this installation site.

setPinForm(f => ({ ...f, lat: e.target.value }))} /> setPinForm(f => ({ ...f, lng: e.target.value }))} />

💡 Tip: Get coordinates from Google Maps → right-click the location → copy lat/lng.

); } // ─── Main Page ──────────────────────────────────────────────────────────────── export default function ClientDetailPage() { const { id } = useParams<{ id: string }>(); const router = useRouter(); const qc = useQueryClient(); const [activeTab, setActiveTab] = useState("profile"); const [payInvoice, setPayInvoice] = useState(null); const [payForm, setPayForm] = useState({ amount: "", channel: "CASH", referenceNumber: "", notes: "" }); const { data: client, isLoading, refetch: refetchClient } = useQuery({ queryKey: ["client", id], queryFn: async () => { const r = await api.get(`/api/v1/clients/${id}`); return r.data; }, }); const { data: subscriptions = [] } = useQuery({ queryKey: ["client-subscriptions", id], queryFn: async () => { const r = await api.get<{ data: Subscription[] }>(`/api/v1/clients/${id}/subscriptions`); return (r.data as any).data ?? []; }, }); const { data: allTickets = [] } = useQuery({ queryKey: ["client-tickets-all", id], queryFn: async () => { const r = await api.get>(`/api/v1/tickets?clientId=${id}&page=1&limit=50`); return (r.data as any).data ?? []; }, }); const { data: invoicesData, refetch: refetchInvoices } = useQuery>({ queryKey: ["client-invoices", id], queryFn: async () => { const r = await api.get>(`/api/v1/invoices?clientId=${id}&page=1&limit=30`); return r.data; }, }); const { data: paymentsData } = useQuery>({ queryKey: ["client-payments", id], queryFn: async () => { const r = await api.get>(`/api/v1/payments?clientId=${id}&page=1&limit=30`); return r.data; }, enabled: activeTab === "payments", }); const recordPayment = useMutation({ mutationFn: async () => { await api.post("/api/v1/payments", { clientId: id, invoiceId: payInvoice?.id, amount: Number(payForm.amount), channel: payForm.channel, referenceNumber: payForm.referenceNumber || undefined, notes: payForm.notes || undefined, paymentDate: new Date().toISOString(), }); }, onSuccess: () => { toast.success("Payment recorded!"); setPayInvoice(null); setPayForm({ amount: "", channel: "CASH", referenceNumber: "", notes: "" }); qc.invalidateQueries({ queryKey: ["client-invoices", id] }); qc.invalidateQueries({ queryKey: ["client-payments", id] }); refetchInvoices(); }, onError: (e: any) => toast.error(e.response?.data?.message ?? "Payment failed"), }); const tabs = [ { key: "profile" as Tab, label: "Profile" }, { key: "subscriptions" as Tab, label: "Subscriptions" }, { key: "invoices" as Tab, label: "Invoices" }, { key: "payments" as Tab, label: "Payments" }, { key: "tickets" as Tab, label: "Tickets" }, ]; if (isLoading) return (
); if (!client) return (

Client not found

); const sub = client.subscriptions?.[0]; const clientStatus = sub?.status ?? (client.isActive ? "ACTIVE" : "INACTIVE"); const hasPendingSub = subscriptions.some(s => s.status === "PENDING"); const needsActivation = !client.isActive || hasPendingSub; const handleRefresh = () => { refetchClient(); qc.invalidateQueries({ queryKey: ["client-subscriptions", id] }); qc.invalidateQueries({ queryKey: ["client-tickets-all", id] }); qc.invalidateQueries({ queryKey: ["client-invoices", id] }); }; return (
{/* Header */}

{client.firstName} {client.lastName}

{client.accountNumber} · {client.area?.name ?? "No area"}

{clientStatus}
{/* Activation Flow Banner — only when client is not yet active */} {needsActivation && ( )} {/* Tabs */}
{tabs.map(tab => ( ))}
{/* Profile */} {activeTab === "profile" && ( Client Profile
{[ { label: "Account Number", value: client.accountNumber }, { label: "Full Name", value: `${client.firstName} ${client.lastName}` }, { label: "Email", value: client.email || "—" }, { label: "Phone", value: client.phone }, { label: "Address", value: client.address || "—" }, { label: "Area", value: client.area?.name || "—" }, { label: "Status", value: client.isActive ? "Active" : "Inactive" }, { label: "Location", value: client.lat && client.lng ? `${client.lat}, ${client.lng}` : "Not pinned" }, { label: "Joined", value: formatDate(client.createdAt) }, ].map(({ label, value }) => (
{label}
{value}
))}
Portal Access
{client.portalAccessEnabled ? Enabled : Disabled }
)} {/* Subscriptions */} {activeTab === "subscriptions" && ( Subscriptions {subscriptions.length === 0 ? : subscriptions.map(sub => ( )) }
PlanTypeStatusStart DateMonthly Rate {sub.plan?.name ?? "—"} {sub.type ?? sub.plan?.type ?? "—"} {sub.status} {formatDate(sub.startDate)} {formatCurrency(Number(sub.monthlyPrice ?? sub.plan?.monthlyPrice ?? 0))}
)} {/* Invoices */} {activeTab === "invoices" && ( Invoices {!invoicesData?.data || invoicesData.data.length === 0 ? : invoicesData.data.map((inv: any) => ( )) }
Invoice #TotalBalanceDue DateStatus {inv.invoiceNumber ?? inv.id.slice(0, 8)} {formatCurrency(Number(inv.total ?? inv.amount ?? 0))} 0 ? "text-red-600 font-medium" : "text-gray-500"}> {formatCurrency(Number(inv.balance ?? 0))} {inv.dueDate ? formatDate(inv.dueDate) : "—"} {inv.status} {["SENT", "PARTIAL", "OVERDUE"].includes(inv.status) && ( )}
)} {/* Payments */} {activeTab === "payments" && ( Payment History {!paymentsData?.data || paymentsData.data.length === 0 ? : paymentsData.data.map((p: any) => ( )) }
DateAmountChannelReferenceNotes {p.paymentDate ? formatDate(p.paymentDate) : formatDate(p.createdAt)} {formatCurrency(Number(p.amount))} {p.channel} {p.referenceNumber ?? p.orNumber ?? "—"} {p.notes ?? "—"}
)} {/* Tickets */} {activeTab === "tickets" && ( Tickets {allTickets.length === 0 ? : allTickets.map(t => ( )) }
SubjectTypePriorityStatusCreated {t.subject} {t.type} {t.priority} {t.status} {formatDate(t.createdAt)}
)} {/* Pay Invoice Modal */} setPayInvoice(null)} title={`Record Payment — ${payInvoice?.invoiceNumber ?? ""}`}> {payInvoice && (
Invoice Total{formatCurrency(Number((payInvoice as any).total ?? payInvoice.amount))}
Amount Paid{formatCurrency(Number((payInvoice as any).amountPaid ?? 0))}
Balance Due{formatCurrency(Number((payInvoice as any).balance ?? payInvoice.amount))}
setPayForm(f => ({ ...f, amount: e.target.value }))} />
setPayForm(f => ({ ...f, referenceNumber: e.target.value }))} /> setPayForm(f => ({ ...f, notes: e.target.value }))} />
)}
); }