From 2b385ba86646e5df3e70ef39860a943ceff267fa Mon Sep 17 00:00:00 2001 From: Forge Date: Wed, 25 Mar 2026 16:15:17 +0800 Subject: [PATCH] restore: old src/ components, subscriptions, audit-log, client detail pages; fix tsconfig @/* paths; merge api.ts --- app/(app)/audit-log/page.tsx | 114 +++++++++++ app/(app)/clients/[id]/page.tsx | 298 ++++++++++++++++++++++++++++ app/(app)/subscriptions/page.tsx | 157 +++++++++++++++ components/layout/sidebar.tsx | 62 ++---- src/components/Providers.tsx | 29 +++ src/components/layout/AppLayout.tsx | 34 ++++ src/components/layout/Sidebar.tsx | 110 ++++++++++ src/components/layout/TopBar.tsx | 44 ++++ src/components/ui/Badge.tsx | 31 +++ src/components/ui/Button.tsx | 50 +++++ src/components/ui/Card.tsx | 34 ++++ src/components/ui/Input.tsx | 39 ++++ src/components/ui/Modal.tsx | 48 +++++ src/components/ui/Table.tsx | 58 ++++++ src/contexts/AuthContext.tsx | 71 +++++++ src/lib/api.ts | 39 ++++ src/lib/utils.ts | 31 +++ src/types/index.ts | 209 +++++++++++++++++++ tsconfig.json | 13 +- 19 files changed, 1423 insertions(+), 48 deletions(-) create mode 100644 app/(app)/audit-log/page.tsx create mode 100644 app/(app)/clients/[id]/page.tsx create mode 100644 app/(app)/subscriptions/page.tsx create mode 100644 src/components/Providers.tsx create mode 100644 src/components/layout/AppLayout.tsx create mode 100644 src/components/layout/Sidebar.tsx create mode 100644 src/components/layout/TopBar.tsx create mode 100644 src/components/ui/Badge.tsx create mode 100644 src/components/ui/Button.tsx create mode 100644 src/components/ui/Card.tsx create mode 100644 src/components/ui/Input.tsx create mode 100644 src/components/ui/Modal.tsx create mode 100644 src/components/ui/Table.tsx create mode 100644 src/contexts/AuthContext.tsx create mode 100644 src/lib/api.ts create mode 100644 src/lib/utils.ts create mode 100644 src/types/index.ts diff --git a/app/(app)/audit-log/page.tsx b/app/(app)/audit-log/page.tsx new file mode 100644 index 0000000..0316ce1 --- /dev/null +++ b/app/(app)/audit-log/page.tsx @@ -0,0 +1,114 @@ +"use client"; + +import { useState } from "react"; +import { useQuery } from "@tanstack/react-query"; +import { ChevronLeft, ChevronRight, RefreshCw } from "lucide-react"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/Card"; +import { Table, TableHead, TableBody, TableRow, Th, Td, EmptyState } from "@/components/ui/Table"; +import { Badge } from "@/components/ui/Badge"; +import { Button } from "@/components/ui/Button"; +import { formatDateTime } from "@/lib/utils"; +import api from "@/lib/api"; +import type { AuditLog, PaginatedResponse } from "@/types"; + +export default function AuditLogPage() { + const [page, setPage] = useState(1); + + const { data, isLoading, refetch } = useQuery>({ + queryKey: ["audit-logs", page], + queryFn: async () => { + const res = await api.get>(`/api/v1/audit-logs?page=${page}&limit=50`); + return res.data; + }, + }); + + const logs = data?.data ?? []; + const meta = data?.meta; + + return ( +
+
+
+

Audit Log

+

Track all system activity

+
+ +
+ + + Activity Log + + + + + + + + + + + + + {isLoading ? ( + Array.from({ length: 5 }).map((_, i) => ( + + {Array.from({ length: 5 }).map((_, j) => ( + + ))} + + )) + ) : logs.length === 0 ? ( + + ) : ( + logs.map((log) => ( + + + + + + + + )) + )} + +
TimestampUserActionEntityEntity ID
+ {formatDateTime(log.createdAt)} + + {log.user + ? `${log.user.firstName} ${log.user.lastName}` + : log.userId?.slice(0, 8) ?? "System"} + + + {log.action} + + + {log.entityType ?? log.entity ?? "—"} + + {log.entityId?.slice(0, 12) ?? "—"} +
+ + {meta && meta.totalPages > 1 && ( +
+

Page {meta.page} of {meta.totalPages}

+
+ + +
+
+ )} +
+
+
+ ); +} diff --git a/app/(app)/clients/[id]/page.tsx b/app/(app)/clients/[id]/page.tsx new file mode 100644 index 0000000..75c002f --- /dev/null +++ b/app/(app)/clients/[id]/page.tsx @@ -0,0 +1,298 @@ +"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 = { + 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("profile"); + + const { data: client, isLoading, refetch: refetchClient } = useQuery({ + queryKey: ["client", id], + queryFn: async () => { + const res = await api.get(`/api/v1/clients/${id}`); + return res.data; + }, + }); + + const { data: subscriptions, isError: subsError } = useQuery({ + queryKey: ["client-subscriptions", id], + queryFn: async () => { + const res = await api.get(`/api/v1/clients/${id}/subscriptions`); + return Array.isArray(res.data) ? res.data : []; + }, + enabled: activeTab === "subscriptions", + retry: false, + }); + + const { data: invoicesData } = useQuery>({ + queryKey: ["client-invoices", id], + queryFn: async () => { + const res = await api.get>(`/api/v1/invoices?clientId=${id}&page=1&limit=20`); + return res.data; + }, + enabled: activeTab === "invoices", + }); + + const { data: ticketsData } = useQuery>({ + queryKey: ["client-tickets", id], + queryFn: async () => { + const res = await api.get>(`/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 ( +
+
+ + +
+ + +
+ ); + } + + if (!client) { + return ( +
+

Client not found

+ +
+ ); + } + + return ( +
+ {/* Header */} +
+ +
+

+ {client.firstName} {client.lastName} +

+

+ {client.accountNumber} • {client.area?.name ?? ""} +

+
+
+ + {client.isActive ? "Active" : "Inactive"} + + +
+
+ + {/* Tabs */} +
+ {[ + { 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) => ( + + ))} +
+ + {/* Profile Tab */} + {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: "Joined", value: formatDate(client.createdAt) }, + ].map(({ label, value }) => ( +
+
{label}
+
{value}
+
+ ))} +
+
+
+ )} + + {/* Subscriptions Tab */} + {activeTab === "subscriptions" && ( + + Subscriptions + + + + + + + + + + + + {subsError ? ( + + ) : !subscriptions || subscriptions.length === 0 ? ( + + ) : ( + subscriptions.map((sub) => ( + + + + + + + )) + )} + +
PlanStatusStart DateMonthly Rate
No data yet
{sub.plan?.name ?? sub.planId} + + {sub.status} + + {formatDate(sub.startDate)}{formatCurrency(sub.plan?.monthlyPrice ?? sub.monthlyRate ?? sub.mrc ?? 0)}
+
+
+ )} + + {/* Invoices Tab */} + {activeTab === "invoices" && ( + + Invoices + + + + + + + + + + + + {!invoicesData?.data || invoicesData.data.length === 0 ? ( + + ) : ( + invoicesData.data.map((inv) => ( + + + + + + + )) + )} + +
Invoice #AmountDue DateStatus{inv.invoiceNumber ?? inv.id.slice(0, 8)}{formatCurrency(inv.amount ?? inv.totalAmount ?? 0)}{formatDate(inv.dueDate)} + + {inv.status} + +
+
+
+ )} + + {/* Tickets Tab */} + {activeTab === "tickets" && ( + + Tickets + + + + + + + + + + + + + {!ticketsData?.data || ticketsData.data.length === 0 ? ( + + ) : ( + ticketsData.data.map((ticket) => ( + + + + + + + + )) + )} + +
SubjectTypePriorityStatusCreated{ticket.subject}{ticket.type} + {ticket.priority} + {ticket.status}{formatDate(ticket.createdAt)}
+
+
+ )} +
+ ); +} diff --git a/app/(app)/subscriptions/page.tsx b/app/(app)/subscriptions/page.tsx new file mode 100644 index 0000000..64aa035 --- /dev/null +++ b/app/(app)/subscriptions/page.tsx @@ -0,0 +1,157 @@ +"use client"; + +import { useQuery } from "@tanstack/react-query"; +import { useRouter } from "next/navigation"; +import { RefreshCw, Wifi, AlertCircle } from "lucide-react"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/Card"; +import { Table, TableHead, TableBody, TableRow, Th, Td } from "@/components/ui/Table"; +import { Badge } from "@/components/ui/Badge"; +import { Button } from "@/components/ui/Button"; +import { formatDate, formatCurrency } from "@/lib/utils"; +import api from "@/lib/api"; +import type { PaginatedResponse } from "@/types"; + +interface Subscription { + id: string; + clientId: string; + status: string; + startDate: string; + endDate?: string; + client?: { firstName: string; lastName: string; accountNumber: string }; + plan?: { name: string; type: string; monthlyPrice: number }; + planId?: string; + monthlyRate?: number; + mrc?: number; + type?: string; +} + +const statusVariant: Record = { + ACTIVE: "success", + active: "success", + SUSPENDED: "warning", + suspended: "warning", + CANCELLED: "danger", + cancelled: "danger", + DISCONNECTED: "danger", + disconnected: "danger", + PENDING: "muted", + pending: "muted", +}; + +export default function SubscriptionsPage() { + const router = useRouter(); + + const { data, isLoading, isError, error, refetch, isFetching } = useQuery>({ + queryKey: ["subscriptions"], + queryFn: async () => { + const res = await api.get>("/api/v1/subscriptions?page=1&limit=50"); + return res.data; + }, + retry: false, + }); + + const subscriptions = data?.data ?? []; + const meta = data?.meta; + + const isNotFound = + isError && + (error as { response?: { status?: number } })?.response?.status === 404; + + return ( +
+
+
+

Subscriptions

+

{meta?.total ?? 0} total subscriptions

+
+ +
+ + + + All Subscriptions + + + {isLoading ? ( +
+ {Array.from({ length: 4 }).map((_, i) => ( +
+ ))} +
+ ) : isNotFound || (isError && !data) ? ( +
+
+ +
+

Subscriptions Module Not Yet Available

+

+ Subscription data will appear here once the module is deployed. +

+
+ + API endpoint not yet available +
+ +
+ ) : subscriptions.length === 0 ? ( +
+
+ +
+

No subscriptions yet.

+ +
+ ) : ( + + + + + + + + + + + + + {subscriptions.map((sub) => ( + sub.clientId && router.push(`/clients/${sub.clientId}`)} + > + + + + + + + + ))} + +
ClientPlanTypeStatusStart DateMRC + {sub.client ? ( +
+

{sub.client.firstName} {sub.client.lastName}

+

{sub.client.accountNumber}

+
+ ) : ( + + )} +
{sub.plan?.name ?? sub.planId ?? "—"}{sub.plan?.type ?? sub.type ?? "—"} + {sub.status} + {formatDate(sub.startDate)}{formatCurrency(sub.plan?.monthlyPrice ?? sub.monthlyRate ?? sub.mrc ?? 0)}
+ )} + + +
+ ); +} diff --git a/components/layout/sidebar.tsx b/components/layout/sidebar.tsx index 8d23bcf..4dd3f62 100644 --- a/components/layout/sidebar.tsx +++ b/components/layout/sidebar.tsx @@ -4,76 +4,58 @@ import Link from 'next/link'; import { usePathname } from 'next/navigation'; import { useAuthStore } from '@/lib/auth-store'; import { - LayoutDashboard, - Users, - FileText, - CreditCard, - ArrowLeftRight, - Ticket, - BarChart3, - Settings, + LayoutDashboard, Users, FileText, CreditCard, ArrowLeftRight, + Ticket, BarChart3, Settings, Wifi, ClipboardList, ScrollText, } from 'lucide-react'; const navItems = [ - { label: 'Dashboard', href: '/dashboard', icon: LayoutDashboard, roles: [] }, - { label: 'Clients', href: '/clients', icon: Users, roles: [] }, - { label: 'Invoices', href: '/invoices', icon: FileText, roles: [] }, - { label: 'Payments', href: '/payments', icon: CreditCard, roles: [] }, - { label: 'Remittances', href: '/remittances', icon: ArrowLeftRight, roles: [] }, - { label: 'Tickets', href: '/tickets', icon: Ticket, roles: [] }, - { label: 'Reports', href: '/reports', icon: BarChart3, roles: ['admin', 'staff'] }, - { label: 'Settings', href: '/settings/tenant', icon: Settings, roles: ['admin'] }, + { label: 'Dashboard', href: '/dashboard', icon: LayoutDashboard, roles: [] }, + { label: 'Clients', href: '/clients', icon: Users, roles: [] }, + { label: 'Subscriptions', href: '/subscriptions', icon: Wifi, roles: [] }, + { label: 'Invoices', href: '/invoices', icon: FileText, roles: [] }, + { label: 'Payments', href: '/payments', icon: CreditCard, roles: [] }, + { label: 'Remittances', href: '/remittances', icon: ArrowLeftRight, roles: [] }, + { label: 'Tickets', href: '/tickets', icon: Ticket, roles: [] }, + { label: 'Tasks', href: '/tasks', icon: ClipboardList, roles: ['admin','staff'] }, + { label: 'Reports', href: '/reports', icon: BarChart3, roles: ['admin','staff'] }, + { label: 'Audit Log', href: '/audit-log', icon: ScrollText, roles: ['admin'] }, + { label: 'Settings', href: '/settings/tenant', icon: Settings, roles: ['admin'] }, ]; export default function Sidebar() { const pathname = usePathname(); const user = useAuthStore((s) => s.user); - const userRoles = user?.roles ?? []; + const userRoles: string[] = (user?.roles ?? []).map((r: string) => r.toLowerCase()); const visibleItems = navItems.filter( (item) => item.roles.length === 0 || item.roles.some((r) => userRoles.includes(r)) ); return ( -