diff --git a/apps/remix/app/components/tables/admin-organisation-stats-table.tsx b/apps/remix/app/components/tables/admin-organisation-stats-table.tsx new file mode 100644 index 000000000..97275e1c1 --- /dev/null +++ b/apps/remix/app/components/tables/admin-organisation-stats-table.tsx @@ -0,0 +1,188 @@ +import { useUpdateSearchParams } from '@documenso/lib/client-only/hooks/use-update-search-params'; +import { ZUrlSearchParamsSchema } from '@documenso/lib/types/search-params'; +import { currentMonthlyPeriod } from '@documenso/lib/universal/monthly-period'; +import { trpc } from '@documenso/trpc/react'; +import type { DataTableColumnDef } from '@documenso/ui/primitives/data-table'; +import { DataTable } from '@documenso/ui/primitives/data-table'; +import { DataTablePagination } from '@documenso/ui/primitives/data-table-pagination'; +import { Skeleton } from '@documenso/ui/primitives/skeleton'; +import { TableCell } from '@documenso/ui/primitives/table'; +import { useLingui } from '@lingui/react/macro'; +import { ChevronDownIcon, ChevronsUpDownIcon, ChevronUpIcon } from 'lucide-react'; +import { useMemo } from 'react'; +import { Link, useSearchParams } from 'react-router'; + +type OrderByColumn = 'documentCount' | 'emailCount' | 'apiCount' | 'totalCount'; +type OrderByDirection = 'asc' | 'desc'; + +const parseOrderByColumn = (value: string | undefined): OrderByColumn | undefined => { + if (value === 'documentCount' || value === 'emailCount' || value === 'apiCount' || value === 'totalCount') { + return value; + } + + return undefined; +}; + +const parseOrderByDirection = (value: string | undefined): OrderByDirection => { + return value === 'asc' ? 'asc' : 'desc'; +}; + +export const AdminOrganisationStatsTable = () => { + const { t } = useLingui(); + + const [searchParams] = useSearchParams(); + const updateSearchParams = useUpdateSearchParams(); + + const parsedSearchParams = ZUrlSearchParamsSchema.parse(Object.fromEntries(searchParams ?? [])); + + // Default to the current month. + const period = searchParams?.get('period') ?? currentMonthlyPeriod(); + const claimId = searchParams?.get('claimId') || undefined; + const orderByColumn = parseOrderByColumn(searchParams?.get('orderByColumn') ?? undefined); + const orderByDirection = parseOrderByDirection(searchParams?.get('orderByDirection') ?? undefined); + + const { data, isLoading, isLoadingError } = trpc.admin.organisation.findStats.useQuery({ + query: parsedSearchParams.query, + page: parsedSearchParams.page, + perPage: parsedSearchParams.perPage, + period, + claimId, + orderByColumn, + orderByDirection, + }); + + const onPaginationChange = (page: number, perPage: number) => { + updateSearchParams({ + page, + perPage, + }); + }; + + const handleColumnSort = (column: OrderByColumn) => { + const nextDirection = orderByColumn === column && orderByDirection === 'desc' ? 'asc' : 'desc'; + + updateSearchParams({ + orderByColumn: column, + orderByDirection: nextDirection, + page: 1, + }); + }; + + const results = data ?? { + data: [], + perPage: 10, + currentPage: 1, + totalPages: 1, + }; + + const columns = useMemo(() => { + const sortableHeader = (label: string, column: OrderByColumn) => ( + + ); + + return [ + { + header: t`Organisation`, + accessorKey: 'organisationName', + cell: ({ row }) => ( + + {row.original.organisationName} + + ), + }, + { + header: t`Claim`, + accessorKey: 'originalClaimId', + cell: ({ row }) => {row.original.originalClaimId ?? '—'}, + }, + { + header: t`Period`, + accessorKey: 'period', + cell: ({ row }) => row.original.period, + }, + { + header: () => sortableHeader(t`Documents`, 'documentCount'), + accessorKey: 'documentCount', + cell: ({ row }) => row.original.documentCount, + }, + { + header: () => sortableHeader(t`Emails`, 'emailCount'), + accessorKey: 'emailCount', + cell: ({ row }) => row.original.emailCount, + }, + { + header: () => sortableHeader(t`API`, 'apiCount'), + accessorKey: 'apiCount', + cell: ({ row }) => row.original.apiCount, + }, + { + header: () => sortableHeader(t`Total`, 'totalCount'), + accessorKey: 'totalCount', + cell: ({ row }) => {row.original.totalCount}, + }, + ] satisfies DataTableColumnDef<(typeof results)['data'][number]>[]; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [t, orderByColumn, orderByDirection]); + + return ( +
+ + + + + + + + + + + + + + + + + + + + + + + + ), + }} + > + {(table) => } + +
+ ); +}; diff --git a/apps/remix/app/routes/_authenticated+/admin+/_layout.tsx b/apps/remix/app/routes/_authenticated+/admin+/_layout.tsx index af24ab0bf..af00d92cb 100644 --- a/apps/remix/app/routes/_authenticated+/admin+/_layout.tsx +++ b/apps/remix/app/routes/_authenticated+/admin+/_layout.tsx @@ -10,6 +10,7 @@ import { BarChart3, Building2Icon, FileStack, + LineChartIcon, MailIcon, Settings, Trophy, @@ -153,6 +154,20 @@ export default function AdminLayout({ loaderData }: Route.ComponentProps) { + +