diff --git a/netmanager-app/app/(authenticated)/clients/columns.tsx b/netmanager-app/app/(authenticated)/clients/columns.tsx deleted file mode 100644 index 398a793ae3..0000000000 --- a/netmanager-app/app/(authenticated)/clients/columns.tsx +++ /dev/null @@ -1,102 +0,0 @@ -"use client" - -import type { ColumnDef } from "@tanstack/react-table" -import { Button } from "@/components/ui/button" -import { ArrowUpDown, MoreHorizontal } from "lucide-react" -import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuLabel, - DropdownMenuSeparator, - DropdownMenuTrigger, -} from "@/components/ui/dropdown-menu" -import type { Client } from "@/app/types/clients" - -interface ColumnProps { - onActivate: (client: Client) => void - onDeactivate: (client: Client) => void -} - -export const columns = ({ onActivate, onDeactivate }: ColumnProps): ColumnDef[] => [ - { - accessorKey: "name", - header: ({ column }) => { - return ( - - ) - }, - }, - { - accessorKey: "_id", - header: "Client ID", - }, - { - accessorKey: "user.email", - header: "User Email", - }, - { - accessorKey: "access_token.expires", - header: "Token Expiry", - cell: ({ row }) => { - const accessToken = row.original.access_token - if (!accessToken || !accessToken.expires) return "N/A" - const expires = new Date(accessToken.expires) - if (isNaN(expires.getTime())) return "Invalid Date" - - const now = new Date() - const diffTime = expires.getTime() - now.getTime() - const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24)) - - if (diffDays < 0) return "Expired" - if (diffDays === 0) return "Expires today" - if (diffDays === 1) return "Expires tomorrow" - return `Expires in ${diffDays} days` - }, - }, - { - accessorKey: "isActive", - header: "Status", - cell: ({ row }) => { - const isActive = row.getValue("isActive") - return ( -
- {isActive ? "Activated" : "Not Activated"} -
- ) - }, - }, - { - id: "actions", - cell: ({ row }) => { - const client = row.original - - return ( - - - - - - Actions - navigator.clipboard.writeText(client._id)}> - Copy client ID - - - (client.isActive ? onDeactivate(client) : onActivate(client))}> - {client.isActive ? "Deactivate" : "Activate"} - - View client details - Update client information - - - ) - }, - }, -] - diff --git a/netmanager-app/app/(authenticated)/clients/data-table.tsx b/netmanager-app/app/(authenticated)/clients/data-table.tsx deleted file mode 100644 index 716e5a6389..0000000000 --- a/netmanager-app/app/(authenticated)/clients/data-table.tsx +++ /dev/null @@ -1,103 +0,0 @@ -"use client" - -import { - type ColumnDef, - flexRender, - getCoreRowModel, - useReactTable, - getPaginationRowModel, - getSortedRowModel, - type SortingState, - getFilteredRowModel, - type ColumnFiltersState, -} from "@tanstack/react-table" - -import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table" - -import { Button } from "@/components/ui/button" -import { Input } from "@/components/ui/input" -import React from "react" -import type { Client } from "@/app/types/clients" - -interface DataTableProps { - columns: ColumnDef[] - data: Client[] - onActivate: (client: Client) => void - onDeactivate: (client: Client) => void -} - -export function DataTable({ columns, data, onActivate, onDeactivate }: DataTableProps) { - const [sorting, setSorting] = React.useState([]) - const [columnFilters, setColumnFilters] = React.useState([]) - - const table = useReactTable({ - data, - columns: columns({ onActivate, onDeactivate }), - getCoreRowModel: getCoreRowModel(), - getPaginationRowModel: getPaginationRowModel(), - onSortingChange: setSorting, - getSortedRowModel: getSortedRowModel(), - onColumnFiltersChange: setColumnFilters, - getFilteredRowModel: getFilteredRowModel(), - state: { - sorting, - columnFilters, - }, - }) - - return ( -
-
- table.getColumn("name")?.setFilterValue(event.target.value)} - className="max-w-sm" - /> -
-
- - - {table.getHeaderGroups().map((headerGroup) => ( - - {headerGroup.headers.map((header) => { - return ( - - {header.isPlaceholder ? null : flexRender(header.column.columnDef.header, header.getContext())} - - ) - })} - - ))} - - - {table.getRowModel().rows?.length ? ( - table.getRowModel().rows.map((row) => ( - - {row.getVisibleCells().map((cell) => ( - {flexRender(cell.column.columnDef.cell, cell.getContext())} - ))} - - )) - ) : ( - - - No results. - - - )} - -
-
-
- - -
-
- ) -} - diff --git a/netmanager-app/app/(authenticated)/clients/page.tsx b/netmanager-app/app/(authenticated)/clients/page.tsx index f5aa65dcaa..def9e5c7df 100644 --- a/netmanager-app/app/(authenticated)/clients/page.tsx +++ b/netmanager-app/app/(authenticated)/clients/page.tsx @@ -2,12 +2,24 @@ import { useState, useEffect } from "react" import { Button } from "@/components/ui/button" -import { DataTable } from "./data-table" -import { columns } from "./columns" -import { ActivateClientDialog, DeactivateClientDialog } from "./dialogs" +import { Input } from "@/components/ui/input" +import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table" +import { ActivateClientDialog, DeactivateClientDialog } from "../../../components/clients/dialogs" import { getClientsApi, activateUserClientApi } from "@/core/apis/analytics" import { useToast } from "@/components/ui/use-toast" import type { Client } from "@/app/types/clients" +import { Search, ArrowUpDown, Loader2 } from "lucide-react" +import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu" +import { + Pagination, + PaginationContent, + PaginationItem, + PaginationLink, + PaginationNext, + PaginationPrevious, +} from "@/components/ui/pagination" + +const ITEMS_PER_PAGE = 8 const formatDate = (dateString: string | undefined): string => { if (!dateString) return "N/A" @@ -24,18 +36,24 @@ const formatDate = (dateString: string | undefined): string => { return `Expires in ${diffDays} days` } + const ClientManagement = () => { const [clients, setClients] = useState([]) const [loading, setLoading] = useState(false) const [selectedClient, setSelectedClient] = useState(null) const [activateDialogOpen, setActivateDialogOpen] = useState(false) const [deactivateDialogOpen, setDeactivateDialogOpen] = useState(false) + const [searchQuery, setSearchQuery] = useState("") + const [currentPage, setCurrentPage] = useState(1) + const [sortField, setSortField] = useState("name") + const [sortOrder, setSortOrder] = useState<"asc" | "desc">("asc") const { toast } = useToast() const fetchClients = async () => { setLoading(true) try { const response = await getClientsApi() + console.log(response.clients) setClients(response.clients) } catch (error) { toast({ @@ -48,6 +66,14 @@ const ClientManagement = () => { } } + const handleCopyClientId = (clientId: string) => { + navigator.clipboard.writeText(clientId) + toast({ + title: "Client ID Copied", + description: "The client ID has been copied to your clipboard.", + }) + } + useEffect(() => { fetchClients() }, []) @@ -73,9 +99,6 @@ const ClientManagement = () => { } } - const activatedClients = clients.filter((client) => client.isActive).length - const deactivatedClients = clients.filter((client) => !client.isActive).length - const handleActivateClick = (client: Client) => { setSelectedClient(client) setActivateDialogOpen(true) @@ -86,6 +109,33 @@ const ClientManagement = () => { setDeactivateDialogOpen(true) } + const handleSort = (field: keyof Client) => { + if (sortField === field) { + setSortOrder(sortOrder === "asc" ? "desc" : "asc") + } else { + setSortField(field) + setSortOrder("asc") + } + } + + const filteredClients = clients.filter( + (client) => + client.name.toLowerCase().includes(searchQuery.toLowerCase()) || + client.user.email.toLowerCase().includes(searchQuery.toLowerCase()), + ) + + const sortedClients = [...filteredClients].sort((a, b) => { + if (a[sortField] < b[sortField]) return sortOrder === "asc" ? -1 : 1 + if (a[sortField] > b[sortField]) return sortOrder === "asc" ? 1 : -1 + return 0 + }) + + const totalPages = Math.ceil(sortedClients.length / ITEMS_PER_PAGE) + const paginatedClients = sortedClients.slice((currentPage - 1) * ITEMS_PER_PAGE, currentPage * ITEMS_PER_PAGE) + + const activatedClients = clients.filter((client) => client.isActive).length + const deactivatedClients = clients.filter((client) => !client.isActive).length + const nearestExpiringToken = clients .filter((client) => client.access_token && client.access_token.expires) .filter((client) => { @@ -98,58 +148,155 @@ const ClientManagement = () => { return dateA - dateB })[0] - const nearestExpiryDate = nearestExpiringToken - ? formatDate(nearestExpiringToken.access_token.expires) - : "No upcoming expiries" return (
-
-

Client Management

- -
- -
-
-

Activated Clients

-

{activatedClients}

+ {loading ? ( +
+
-
-

Deactivated Clients

-

{deactivatedClients}

-
-
-

Nearest Token Expiry

-

- {nearestExpiringToken ? formatDate(nearestExpiringToken.access_token.expires) : "No upcoming expiries"} -

-

{nearestExpiringToken?.name || "N/A"}

-
-
- - - - selectedClient && handleActivateDeactivate(selectedClient._id, true)} - clientName={selectedClient?.name} - /> - - selectedClient && handleActivateDeactivate(selectedClient._id, false)} - clientName={selectedClient?.name} - /> + ) : ( + <> +
+

Client Management

+ +
+ +
+
+

Activated Clients

+

{activatedClients}

+
+
+

Deactivated Clients

+

{deactivatedClients}

+
+ +
+ +
+
+ + setSearchQuery(e.target.value)} + /> +
+ + + + + + handleSort("name")}> + Name {sortField === "name" && (sortOrder === "asc" ? "↑" : "↓")} + + handleSort("isActive")}> + Status {sortField === "isActive" && (sortOrder === "asc" ? "↑" : "↓")} + + + +
+ +
+ + + + Client Name + User Email + Token Expiry + Status + Actions + + + + {paginatedClients.map((client) => ( + + +
{client.name}
+
{client._id}
+
+ {client.user.email} + + {client.access_token?.expires + ? formatDate(client.access_token.expires) + : "N/A"} + + + + {client.isActive ? "Activated" : "Not Activated"} + + + + + + +
+ ))} +
+
+
+ +
+ + + + setCurrentPage((prev) => Math.max(prev - 1, 1))} + className={currentPage === 1 ? "pointer-events-none opacity-50" : "cursor-pointer"} + /> + + {Array.from({ length: totalPages }, (_, i) => i + 1).map((page) => ( + + setCurrentPage(page)} isActive={currentPage === page}> + {page} + + + ))} + + setCurrentPage((prev) => Math.min(prev + 1, totalPages))} + className={currentPage === totalPages ? "pointer-events-none opacity-50" : "cursor-pointer"} + /> + + + +
+ + selectedClient && handleActivateDeactivate(selectedClient._id, true)} + clientName={selectedClient?.name} + /> + + selectedClient && handleActivateDeactivate(selectedClient._id, false)} + clientName={selectedClient?.name} + /> + + )}
) } -export default ClientManagement; +export default ClientManagement diff --git a/netmanager-app/app/(authenticated)/clients/dialogs.tsx b/netmanager-app/components/clients/dialogs.tsx similarity index 100% rename from netmanager-app/app/(authenticated)/clients/dialogs.tsx rename to netmanager-app/components/clients/dialogs.tsx