299 lines
11 KiB
TypeScript
299 lines
11 KiB
TypeScript
"use client";
|
|
|
|
import { useState } from "react";
|
|
import { useQuery } from "@tanstack/react-query";
|
|
import { useParams, useRouter } from "next/navigation";
|
|
import { ArrowLeft, Wifi, FileText, Ticket, RefreshCw } 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 { Table, TableHead, TableBody, TableRow, Th, Td, EmptyState } from "@/components/ui/Table";
|
|
import { formatDate, formatCurrency } from "@/lib/utils";
|
|
import api from "@/lib/api";
|
|
import type { Client, Subscription, Invoice, Ticket as TicketType, PaginatedResponse, LegacyPaginatedResponse } from "@/types";
|
|
|
|
type Tab = "profile" | "subscriptions" | "invoices" | "tickets";
|
|
|
|
const statusColor: Record<string, "success" | "warning" | "danger" | "muted"> = {
|
|
active: "success",
|
|
ACTIVE: "success",
|
|
suspended: "warning",
|
|
SUSPENDED: "warning",
|
|
cancelled: "danger",
|
|
CANCELLED: "danger",
|
|
disconnected: "danger",
|
|
pending: "muted",
|
|
PENDING: "muted",
|
|
};
|
|
|
|
export default function ClientDetailPage() {
|
|
const { id } = useParams<{ id: string }>();
|
|
const router = useRouter();
|
|
const [activeTab, setActiveTab] = useState<Tab>("profile");
|
|
|
|
const { data: client, isLoading, refetch: refetchClient } = useQuery<Client>({
|
|
queryKey: ["client", id],
|
|
queryFn: async () => {
|
|
const res = await api.get<Client>(`/api/v1/clients/${id}`);
|
|
return res.data;
|
|
},
|
|
});
|
|
|
|
const { data: subscriptions, isError: subsError } = useQuery<Subscription[]>({
|
|
queryKey: ["client-subscriptions", id],
|
|
queryFn: async () => {
|
|
const res = await api.get<Subscription[]>(`/api/v1/clients/${id}/subscriptions`);
|
|
return Array.isArray(res.data) ? res.data : [];
|
|
},
|
|
enabled: activeTab === "subscriptions",
|
|
retry: false,
|
|
});
|
|
|
|
const { data: invoicesData } = useQuery<LegacyPaginatedResponse<Invoice>>({
|
|
queryKey: ["client-invoices", id],
|
|
queryFn: async () => {
|
|
const res = await api.get<LegacyPaginatedResponse<Invoice>>(`/api/v1/invoices?clientId=${id}&page=1&limit=20`);
|
|
return res.data;
|
|
},
|
|
enabled: activeTab === "invoices",
|
|
});
|
|
|
|
const { data: ticketsData } = useQuery<PaginatedResponse<TicketType>>({
|
|
queryKey: ["client-tickets", id],
|
|
queryFn: async () => {
|
|
const res = await api.get<PaginatedResponse<TicketType>>(`/api/v1/tickets?clientId=${id}&page=1&limit=20`);
|
|
return res.data;
|
|
},
|
|
enabled: activeTab === "tickets",
|
|
});
|
|
|
|
const tabs: { key: Tab; label: string; icon: React.ComponentType<{ className?: string }> }[] = [
|
|
{ key: "profile", label: "Profile", icon: ArrowLeft },
|
|
{ key: "subscriptions", label: "Subscriptions", icon: Wifi },
|
|
{ key: "invoices", label: "Invoices", icon: FileText },
|
|
{ key: "tickets", label: "Tickets", icon: Ticket },
|
|
];
|
|
|
|
if (isLoading) {
|
|
return (
|
|
<div className="space-y-4">
|
|
<div className="h-8 w-48 animate-pulse bg-gray-200 rounded" />
|
|
<Card>
|
|
<CardContent className="py-10">
|
|
<div className="h-32 animate-pulse bg-gray-100 rounded-lg" />
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
if (!client) {
|
|
return (
|
|
<div className="text-center py-20 text-gray-400">
|
|
<p>Client not found</p>
|
|
<Button variant="secondary" className="mt-4" onClick={() => router.push("/clients")}>
|
|
Back to Clients
|
|
</Button>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="space-y-4">
|
|
{/* Header */}
|
|
<div className="flex items-center gap-3">
|
|
<Button variant="ghost" size="sm" onClick={() => router.push("/clients")}>
|
|
<ArrowLeft className="h-4 w-4" />
|
|
</Button>
|
|
<div>
|
|
<h1 className="text-2xl font-bold text-gray-900">
|
|
{client.firstName} {client.lastName}
|
|
</h1>
|
|
<p className="text-sm text-gray-500">
|
|
{client.accountNumber} • {client.area?.name ?? ""}
|
|
</p>
|
|
</div>
|
|
<div className="ml-auto flex items-center gap-2">
|
|
<Badge variant={client.isActive ? "success" : "muted"}>
|
|
{client.isActive ? "Active" : "Inactive"}
|
|
</Badge>
|
|
<Button size="sm" variant="outline" onClick={() => refetchClient()}>
|
|
<RefreshCw className="h-4 w-4" />
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Tabs */}
|
|
<div className="flex border-b border-gray-200">
|
|
{[
|
|
{ key: "profile" as Tab, label: "Profile" },
|
|
{ key: "subscriptions" as Tab, label: "Subscriptions" },
|
|
{ key: "invoices" as Tab, label: "Invoices" },
|
|
{ key: "tickets" as Tab, label: "Tickets" },
|
|
].map((tab) => (
|
|
<button
|
|
key={tab.key}
|
|
onClick={() => setActiveTab(tab.key)}
|
|
className={`px-4 py-2 text-sm font-medium border-b-2 transition-colors ${
|
|
activeTab === tab.key
|
|
? "border-blue-600 text-blue-600"
|
|
: "border-transparent text-gray-500 hover:text-gray-700"
|
|
}`}
|
|
>
|
|
{tab.label}
|
|
</button>
|
|
))}
|
|
</div>
|
|
|
|
{/* Profile Tab */}
|
|
{activeTab === "profile" && (
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle>Client Profile</CardTitle>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<dl className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
|
{[
|
|
{ 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: "Joined", value: formatDate(client.createdAt) },
|
|
].map(({ label, value }) => (
|
|
<div key={label}>
|
|
<dt className="text-xs font-medium text-gray-400 uppercase tracking-wide">{label}</dt>
|
|
<dd className="mt-0.5 text-sm text-gray-900">{value}</dd>
|
|
</div>
|
|
))}
|
|
</dl>
|
|
</CardContent>
|
|
</Card>
|
|
)}
|
|
|
|
{/* Subscriptions Tab */}
|
|
{activeTab === "subscriptions" && (
|
|
<Card>
|
|
<CardHeader><CardTitle>Subscriptions</CardTitle></CardHeader>
|
|
<CardContent className="p-0">
|
|
<Table>
|
|
<TableHead>
|
|
<TableRow>
|
|
<Th>Plan</Th>
|
|
<Th>Status</Th>
|
|
<Th>Start Date</Th>
|
|
<Th>Monthly Rate</Th>
|
|
</TableRow>
|
|
</TableHead>
|
|
<TableBody>
|
|
{subsError ? (
|
|
<tr><td colSpan={4} className="py-8 text-center text-gray-400 text-sm">No data yet</td></tr>
|
|
) : !subscriptions || subscriptions.length === 0 ? (
|
|
<EmptyState message="No subscriptions yet" />
|
|
) : (
|
|
subscriptions.map((sub) => (
|
|
<TableRow key={sub.id} className="hover:bg-gray-50 transition-colors">
|
|
<Td className="font-medium">{sub.plan?.name ?? sub.planId}</Td>
|
|
<Td>
|
|
<Badge variant={statusColor[sub.status] ?? "muted"}>
|
|
{sub.status}
|
|
</Badge>
|
|
</Td>
|
|
<Td>{formatDate(sub.startDate)}</Td>
|
|
<Td>{formatCurrency(sub.plan?.monthlyPrice ?? sub.monthlyRate ?? sub.mrc ?? 0)}</Td>
|
|
</TableRow>
|
|
))
|
|
)}
|
|
</TableBody>
|
|
</Table>
|
|
</CardContent>
|
|
</Card>
|
|
)}
|
|
|
|
{/* Invoices Tab */}
|
|
{activeTab === "invoices" && (
|
|
<Card>
|
|
<CardHeader><CardTitle>Invoices</CardTitle></CardHeader>
|
|
<CardContent className="p-0">
|
|
<Table>
|
|
<TableHead>
|
|
<TableRow>
|
|
<Th>Invoice #</Th>
|
|
<Th>Amount</Th>
|
|
<Th>Due Date</Th>
|
|
<Th>Status</Th>
|
|
</TableRow>
|
|
</TableHead>
|
|
<TableBody>
|
|
{!invoicesData?.data || invoicesData.data.length === 0 ? (
|
|
<EmptyState message="No invoices yet" />
|
|
) : (
|
|
invoicesData.data.map((inv) => (
|
|
<TableRow key={inv.id}>
|
|
<Td className="font-mono text-xs">{inv.invoiceNumber ?? inv.id.slice(0, 8)}</Td>
|
|
<Td>{formatCurrency(inv.amount ?? inv.totalAmount ?? 0)}</Td>
|
|
<Td>{formatDate(inv.dueDate)}</Td>
|
|
<Td>
|
|
<Badge
|
|
variant={
|
|
inv.status === "paid" || inv.status === "PAID" ? "success" :
|
|
inv.status === "overdue" || inv.status === "OVERDUE" ? "danger" : "warning"
|
|
}
|
|
>
|
|
{inv.status}
|
|
</Badge>
|
|
</Td>
|
|
</TableRow>
|
|
))
|
|
)}
|
|
</TableBody>
|
|
</Table>
|
|
</CardContent>
|
|
</Card>
|
|
)}
|
|
|
|
{/* Tickets Tab */}
|
|
{activeTab === "tickets" && (
|
|
<Card>
|
|
<CardHeader><CardTitle>Tickets</CardTitle></CardHeader>
|
|
<CardContent className="p-0">
|
|
<Table>
|
|
<TableHead>
|
|
<TableRow>
|
|
<Th>Subject</Th>
|
|
<Th>Type</Th>
|
|
<Th>Priority</Th>
|
|
<Th>Status</Th>
|
|
<Th>Created</Th>
|
|
</TableRow>
|
|
</TableHead>
|
|
<TableBody>
|
|
{!ticketsData?.data || ticketsData.data.length === 0 ? (
|
|
<EmptyState message="No tickets yet" />
|
|
) : (
|
|
ticketsData.data.map((ticket) => (
|
|
<TableRow key={ticket.id}>
|
|
<Td className="font-medium">{ticket.subject}</Td>
|
|
<Td><Badge variant="muted">{ticket.type}</Badge></Td>
|
|
<Td>
|
|
<Badge variant={
|
|
ticket.priority === "urgent" ? "danger" :
|
|
ticket.priority === "high" ? "warning" : "muted"
|
|
}>{ticket.priority}</Badge>
|
|
</Td>
|
|
<Td><Badge variant="default">{ticket.status}</Badge></Td>
|
|
<Td className="text-xs text-gray-400">{formatDate(ticket.createdAt)}</Td>
|
|
</TableRow>
|
|
))
|
|
)}
|
|
</TableBody>
|
|
</Table>
|
|
</CardContent>
|
|
</Card>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|