fix(M3-M11): clients meta pagination, client detail types, sidebar icon imports, payments error state; add E2E specs for all modules
This commit is contained in:
@@ -13,7 +13,7 @@ import { Table, TableHead, TableBody, TableRow, Th, Td, EmptyState } from "@/com
|
||||
import { formatDate, formatCurrency } from "@/lib/utils";
|
||||
import api from "@/lib/api";
|
||||
import { toast } from "sonner";
|
||||
import type { Client, Subscription, Invoice, Ticket as TicketType, Payment, PaginatedResponse, LegacyPaginatedResponse } from "@/types";
|
||||
import type { Client, Subscription, Invoice, Ticket as TicketType, Payment, LegacyPaginatedResponse } from "@/types";
|
||||
|
||||
type Tab = "profile" | "subscriptions" | "invoices" | "payments" | "tickets";
|
||||
|
||||
@@ -74,9 +74,9 @@ export default function ClientDetailPage() {
|
||||
enabled: activeTab === "payments",
|
||||
});
|
||||
|
||||
const { data: ticketsData } = useQuery<PaginatedResponse<TicketType>>({
|
||||
const { data: ticketsData } = useQuery<LegacyPaginatedResponse<TicketType>>({
|
||||
queryKey: ["client-tickets", id],
|
||||
queryFn: async () => { const r = await api.get<PaginatedResponse<TicketType>>(`/api/v1/tickets?clientId=${id}&page=1&limit=20`); return r.data; },
|
||||
queryFn: async () => { const r = await api.get<LegacyPaginatedResponse<TicketType>>(`/api/v1/tickets?clientId=${id}&page=1&limit=20`); return r.data; },
|
||||
enabled: activeTab === "tickets",
|
||||
});
|
||||
|
||||
@@ -192,7 +192,7 @@ export default function ClientDetailPage() {
|
||||
<Td><Badge variant="muted">{sub.type ?? sub.plan?.type ?? "—"}</Badge></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>
|
||||
<Td>{formatCurrency(Number(sub.monthlyPrice ?? sub.plan?.monthlyPrice ?? 0))}</Td>
|
||||
</TableRow>
|
||||
))
|
||||
}
|
||||
|
||||
@@ -20,7 +20,8 @@ interface Client {
|
||||
area: { id: string; name: string } | null;
|
||||
subscriptions: Array<{ status: string; type: string; monthlyPrice: string; plan?: { name: string }; }>;
|
||||
}
|
||||
interface ClientsResponse { data: Client[]; total: number; page: number; limit: number; }
|
||||
interface ClientsMeta { total: number; page: number; limit: number; totalPages: number; }
|
||||
interface ClientsResponse { data: Client[]; meta: ClientsMeta; }
|
||||
|
||||
const statusVariant: Record<string, "success" | "warning" | "danger" | "muted"> = {
|
||||
ACTIVE: "success", PENDING: "warning", SUSPENDED: "danger", DISCONNECTED: "muted", CANCELLED: "muted",
|
||||
@@ -78,7 +79,7 @@ export default function ClientsPage() {
|
||||
});
|
||||
|
||||
const clients = data?.data ?? [];
|
||||
const total = data?.total ?? 0;
|
||||
const total = data?.meta?.total ?? 0;
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
@@ -155,8 +156,8 @@ export default function ClientsPage() {
|
||||
<div className="flex items-center justify-between px-4 py-3 border-t text-sm text-gray-500">
|
||||
<span>Showing {(page - 1) * 20 + 1}–{Math.min(page * 20, total)} of {total}</span>
|
||||
<div className="flex gap-2">
|
||||
<button disabled={page === 1} onClick={() => setPage(p => p - 1)} className="px-3 py-1 border rounded-md disabled:opacity-40">Prev</button>
|
||||
<button disabled={page * 20 >= total} onClick={() => setPage(p => p + 1)} className="px-3 py-1 border rounded-md disabled:opacity-40">Next</button>
|
||||
<button disabled={page === 1} onClick={() => setPage(p => p - 1)} className="px-3 py-1 border rounded-md disabled:opacity-40 cursor-pointer">Prev</button>
|
||||
<button disabled={page >= (data?.meta?.totalPages ?? 1)} onClick={() => setPage(p => p + 1)} className="px-3 py-1 border rounded-md disabled:opacity-40 cursor-pointer">Next</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import { useState } from "react";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { RefreshCw, CreditCard } from "lucide-react";
|
||||
import { RefreshCw, CreditCard, AlertCircle } from "lucide-react";
|
||||
import { Card, CardContent, CardHeader } from "@/components/ui/Card";
|
||||
import { Table, TableHead, TableBody, TableRow, Th, Td, EmptyState } from "@/components/ui/Table";
|
||||
import { Badge } from "@/components/ui/Badge";
|
||||
@@ -35,7 +35,7 @@ export default function PaymentsPage() {
|
||||
const [page, setPage] = useState(1);
|
||||
const [selected, setSelected] = useState<Payment | null>(null);
|
||||
|
||||
const { data, isLoading, refetch } = useQuery<PaymentsResponse>({
|
||||
const { data, isLoading, isError, refetch } = useQuery<PaymentsResponse>({
|
||||
queryKey: ["payments", search, channelFilter, page],
|
||||
queryFn: async () => {
|
||||
const params = new URLSearchParams({ page: String(page), limit: "20" });
|
||||
@@ -85,6 +85,8 @@ export default function PaymentsPage() {
|
||||
Array.from({ length: 8 }).map((_, i) => (
|
||||
<TableRow key={i}><Td colSpan={7}><div className="h-4 bg-gray-100 rounded animate-pulse" /></Td></TableRow>
|
||||
))
|
||||
) : isError ? (
|
||||
<TableRow><Td colSpan={7}><div className="flex items-center gap-2 py-6 justify-center text-red-400 text-sm"><AlertCircle size={16} />Failed to load payments. <button onClick={() => refetch()} className="underline">Retry</button></div></Td></TableRow>
|
||||
) : payments.length === 0 ? (
|
||||
<EmptyState colSpan={7} message="No payments found" icon={<CreditCard size={24} />} />
|
||||
) : payments.map(p => (
|
||||
|
||||
Reference in New Issue
Block a user