diff --git a/apps/web/src/app/(dashboard)/admin/leaderboard/data-table-leaderboard.tsx b/apps/web/src/app/(dashboard)/admin/leaderboard/data-table-leaderboard.tsx index 58e448446..7e1a30651 100644 --- a/apps/web/src/app/(dashboard)/admin/leaderboard/data-table-leaderboard.tsx +++ b/apps/web/src/app/(dashboard)/admin/leaderboard/data-table-leaderboard.tsx @@ -8,20 +8,16 @@ import { ChevronDownIcon as CaretSortIcon, Loader } from 'lucide-react'; import { useDebouncedValue } from '@documenso/lib/client-only/hooks/use-debounced-value'; import { useUpdateSearchParams } from '@documenso/lib/client-only/hooks/use-update-search-params'; -import { Button } from '@documenso/ui/primitives/button'; 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 { Input } from '@documenso/ui/primitives/input'; export type SigningVolume = { - customer_id: number; - customer_type: 'User' | 'Team'; - customer_created_at: Date; - total_documents: bigint; - completed_documents: bigint; - customer_email: string; - customer_name: string; + id: number; + name: string; + signingVolume: number; + createdAt: Date; }; type LeaderboardTableProps = { @@ -46,57 +42,53 @@ export const LeaderboardTable = ({ const columns = useMemo(() => { return [ + { + header: 'ID', + accessorKey: 'id', + cell: ({ row }) =>
{row.original.id}
, + size: 10, + }, { header: ({ column }) => ( - + ), - accessorKey: 'customer_name', - cell: ({ row }) =>
{row.getValue('customer_name')}
, + accessorKey: 'name', + cell: ({ row }) =>
{row.getValue('name')}
, + size: 250, }, { header: ({ column }) => ( - - ), - accessorKey: 'customer_email', - cell: ({ row }) =>
{row.getValue('customer_email')}
, - }, - { - header: ({ column }) => ( - + ), - accessorKey: 'completed_documents', - cell: ({ row }) =>
{Number(row.getValue('completed_documents'))}
, + accessorKey: 'signingVolume', + cell: ({ row }) =>
{Number(row.getValue('signingVolume'))}
, }, { - header: ({ column }) => ( - - ), - accessorKey: 'customer_type', - cell: ({ row }) =>
{row.getValue('customer_type')}
, + header: ({ column }) => { + return ( +
column.toggleSorting(column.getIsSorted() === 'asc')} + > + {_(msg`Created`)} + +
+ ); + }, + accessorKey: 'createdAt', + cell: ({ row }) =>
{row.original.createdAt.toLocaleDateString()}
, }, ] satisfies DataTableColumnDef[]; }, []); diff --git a/apps/web/src/app/(dashboard)/admin/leaderboard/leaderboard-table.tsx b/apps/web/src/app/(dashboard)/admin/leaderboard/leaderboard-table.tsx deleted file mode 100644 index 58e448446..000000000 --- a/apps/web/src/app/(dashboard)/admin/leaderboard/leaderboard-table.tsx +++ /dev/null @@ -1,155 +0,0 @@ -'use client'; - -import { useEffect, useMemo, useState, useTransition } from 'react'; - -import { msg } from '@lingui/macro'; -import { useLingui } from '@lingui/react'; -import { ChevronDownIcon as CaretSortIcon, Loader } from 'lucide-react'; - -import { useDebouncedValue } from '@documenso/lib/client-only/hooks/use-debounced-value'; -import { useUpdateSearchParams } from '@documenso/lib/client-only/hooks/use-update-search-params'; -import { Button } from '@documenso/ui/primitives/button'; -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 { Input } from '@documenso/ui/primitives/input'; - -export type SigningVolume = { - customer_id: number; - customer_type: 'User' | 'Team'; - customer_created_at: Date; - total_documents: bigint; - completed_documents: bigint; - customer_email: string; - customer_name: string; -}; - -type LeaderboardTableProps = { - signingVolume: SigningVolume[]; - totalPages: number; - perPage: number; - page: number; -}; - -export const LeaderboardTable = ({ - signingVolume, - totalPages, - perPage, - page, -}: LeaderboardTableProps) => { - const { _ } = useLingui(); - - const [isPending, startTransition] = useTransition(); - const updateSearchParams = useUpdateSearchParams(); - const [searchString, setSearchString] = useState(''); - const debouncedSearchString = useDebouncedValue(searchString, 1000); - - const columns = useMemo(() => { - return [ - { - header: ({ column }) => ( - - ), - accessorKey: 'customer_name', - cell: ({ row }) =>
{row.getValue('customer_name')}
, - }, - { - header: ({ column }) => ( - - ), - accessorKey: 'customer_email', - cell: ({ row }) =>
{row.getValue('customer_email')}
, - }, - { - header: ({ column }) => ( - - ), - accessorKey: 'completed_documents', - cell: ({ row }) =>
{Number(row.getValue('completed_documents'))}
, - }, - { - header: ({ column }) => ( - - ), - accessorKey: 'customer_type', - cell: ({ row }) =>
{row.getValue('customer_type')}
, - }, - ] satisfies DataTableColumnDef[]; - }, []); - - useEffect(() => { - startTransition(() => { - updateSearchParams({ - search: debouncedSearchString, - page: 1, - perPage, - }); - }); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [debouncedSearchString]); - - const onPaginationChange = (page: number, perPage: number) => { - startTransition(() => { - updateSearchParams({ - page, - perPage, - }); - }); - }; - - const handleChange = (e: React.ChangeEvent) => { - setSearchString(e.target.value); - }; - - return ( -
- - - {(table) => } - - - {isPending && ( -
- -
- )} -
- ); -}; diff --git a/apps/web/src/app/(dashboard)/admin/leaderboard/page.tsx b/apps/web/src/app/(dashboard)/admin/leaderboard/page.tsx index b7fc938f7..e92ab0ea3 100644 --- a/apps/web/src/app/(dashboard)/admin/leaderboard/page.tsx +++ b/apps/web/src/app/(dashboard)/admin/leaderboard/page.tsx @@ -28,7 +28,8 @@ export default async function Leaderboard({ searchParams = {} }: AdminLeaderboar const perPage = Number(searchParams.perPage) || 10; const searchString = searchParams.search || ''; - const { signingVolume, totalPages } = await search(searchString, page, perPage); + // todo: change the name + const { leaderboard: signingVolume, totalPages } = await search(searchString, page, perPage); return (
diff --git a/packages/lib/server-only/admin/get-signing-volume.ts b/packages/lib/server-only/admin/get-signing-volume.ts index 1fc8de115..f1d5dcd8e 100644 --- a/packages/lib/server-only/admin/get-signing-volume.ts +++ b/packages/lib/server-only/admin/get-signing-volume.ts @@ -1,15 +1,11 @@ -import { Prisma } from '@prisma/client'; - import { prisma } from '@documenso/prisma'; +import { Prisma } from '@documenso/prisma/client'; export type SigningVolume = { - customer_id: number; - customer_type: 'User' | 'Team'; - customer_created_at: Date; - total_documents: bigint; - completed_documents: bigint; - customer_email: string; - customer_name: string; + id: number; + name: string; + signingVolume: number; + createdAt: Date; }; export type GetSigningVolumeOptions = { @@ -18,111 +14,101 @@ export type GetSigningVolumeOptions = { perPage?: number; }; -export const getSigningVolume = async ({ +export async function getSigningVolume({ search = '', page = 1, perPage = 10, -}: GetSigningVolumeOptions = {}): Promise<{ - signingVolume: SigningVolume[]; - totalPages: number; -}> => { - const offset = (page - 1) * perPage; - - const whereClause = search - ? `AND (LOWER(COALESCE(u.name, t.name)) LIKE $1 OR LOWER(COALESCE(u.email, te.email)) LIKE $1)` - : ''; - - const searchParam = search ? [`%${search.toLowerCase()}%`] : []; - - const [results, totalCount] = await prisma.$transaction(async (tx) => { - const signingVolumeQuery = tx.$queryRaw` - WITH paying_customers AS ( - SELECT DISTINCT - COALESCE(s."userId", t."ownerUserId") AS customer_id, - CASE - WHEN s."userId" IS NOT NULL THEN 'User' - ELSE 'Team' - END AS customer_type, - COALESCE(s."createdAt", t."createdAt") AS customer_created_at - FROM "Subscription" s - FULL OUTER JOIN "Team" t ON s."teamId" = t.id - WHERE s.status = 'ACTIVE' - ), - document_counts AS ( - SELECT - COALESCE(d."userId", t."ownerUserId") AS customer_id, - COUNT(DISTINCT d.id) AS total_documents, - COUNT(DISTINCT CASE WHEN d.status = 'COMPLETED' THEN d.id END) AS completed_documents - FROM "Document" d - LEFT JOIN "Team" t ON d."teamId" = t.id - GROUP BY COALESCE(d."userId", t."ownerUserId") - ) - SELECT - pc.customer_id, - pc.customer_type, - pc.customer_created_at, - COALESCE(dc.total_documents, 0) AS total_documents, - COALESCE(dc.completed_documents, 0) AS completed_documents, - CASE - WHEN pc.customer_type = 'User' THEN u.email - ELSE te.email - END AS customer_email, - CASE - WHEN pc.customer_type = 'User' THEN u.name - ELSE t.name - END AS customer_name - FROM paying_customers pc - LEFT JOIN document_counts dc ON pc.customer_id = dc.customer_id - LEFT JOIN "User" u ON pc.customer_id = u.id AND pc.customer_type = 'User' - LEFT JOIN "Team" t ON pc.customer_id = t."ownerUserId" AND pc.customer_type = 'Team' - LEFT JOIN "TeamEmail" te ON t.id = te."teamId" - WHERE 1=1 ${Prisma.raw(whereClause)} - ORDER BY dc.completed_documents DESC NULLS LAST, pc.customer_created_at DESC - LIMIT ${perPage} OFFSET ${offset} - `; - - const totalCountQuery = tx.$queryRaw<[{ count: bigint }]>` - SELECT COUNT(*) as count - FROM ( - WITH paying_customers AS ( - SELECT DISTINCT - COALESCE(s."userId", t."ownerUserId") AS customer_id, - CASE - WHEN s."userId" IS NOT NULL THEN 'User' - ELSE 'Team' - END AS customer_type, - COALESCE(s."createdAt", t."createdAt") AS customer_created_at - FROM "Subscription" s - FULL OUTER JOIN "Team" t ON s."teamId" = t.id - WHERE s.status = 'ACTIVE' - ) - SELECT - pc.customer_id, - CASE - WHEN pc.customer_type = 'User' THEN u.email - ELSE te.email - END AS customer_email, - CASE - WHEN pc.customer_type = 'User' THEN u.name - ELSE t.name - END AS customer_name - FROM paying_customers pc - LEFT JOIN "User" u ON pc.customer_id = u.id AND pc.customer_type = 'User' - LEFT JOIN "Team" t ON pc.customer_id = t."ownerUserId" AND pc.customer_type = 'Team' - LEFT JOIN "TeamEmail" te ON t.id = te."teamId" - WHERE 1=1 ${Prisma.raw(whereClause)} - ) subquery - `; - - const [signingVolumeResults, totalCountResults] = await Promise.all([ - signingVolumeQuery, - totalCountQuery, - ]); - - return [signingVolumeResults, totalCountResults]; +}: GetSigningVolumeOptions) { + const whereClause = Prisma.validator()({ + status: 'ACTIVE', + OR: [ + { + User: { + OR: [ + { name: { contains: search, mode: 'insensitive' } }, + { email: { contains: search, mode: 'insensitive' } }, + ], + }, + }, + { + team: { + name: { contains: search, mode: 'insensitive' }, + }, + }, + ], }); - const totalPages = Math.ceil(Number(totalCount[0].count) / perPage); + const [subscriptions, totalCount] = await Promise.all([ + prisma.subscription.findMany({ + where: whereClause, + select: { + id: true, + createdAt: true, + User: { + select: { + id: true, + name: true, + email: true, + Document: { + where: { + status: 'COMPLETED', + deletedAt: null, + }, + select: { + id: true, + }, + }, + }, + }, + team: { + select: { + id: true, + name: true, + document: { + where: { + status: 'COMPLETED', + deletedAt: null, + }, + select: { + id: true, + }, + }, + }, + }, + }, + orderBy: [ + { + User: { + Document: { + _count: 'desc', + }, + }, + }, + ], + skip: Math.max(page - 1, 0) * perPage, + take: perPage, + }), + prisma.subscription.count({ + where: whereClause, + }), + ]); - return { signingVolume: results, totalPages }; -}; + const leaderboardWithVolume: SigningVolume[] = subscriptions.map((subscription) => { + const name = + subscription.User?.name || subscription.team?.name || subscription.User?.email || 'Unknown'; + const signingVolume = + (subscription.User?.Document.length || 0) + (subscription.team?.document.length || 0); + + return { + id: subscription.id, + name, + signingVolume, + createdAt: subscription.createdAt, + }; + }); + + return { + leaderboard: leaderboardWithVolume, + totalPages: Math.ceil(totalCount / perPage), + }; +} diff --git a/packages/lib/translations/de/web.po b/packages/lib/translations/de/web.po index 064a86f87..c5f58b996 100644 --- a/packages/lib/translations/de/web.po +++ b/packages/lib/translations/de/web.po @@ -961,6 +961,7 @@ msgid "Create your account and start using state-of-the-art document signing. Op msgstr "" #: apps/web/src/app/(dashboard)/admin/documents/document-results.tsx:62 +#: apps/web/src/app/(dashboard)/admin/leaderboard/data-table-leaderboard.tsx:85 #: apps/web/src/app/(dashboard)/documents/[id]/document-page-view-information.tsx:35 #: apps/web/src/app/(dashboard)/documents/data-table.tsx:54 #: apps/web/src/app/(dashboard)/settings/security/passkeys/user-passkeys-data-table.tsx:65 @@ -1002,8 +1003,8 @@ msgstr "" #: apps/web/src/app/(dashboard)/admin/leaderboard/data-table-leaderboard.tsx:94 #: apps/web/src/app/(dashboard)/admin/leaderboard/leaderboard-table.tsx:94 -msgid "Customer Type" -msgstr "" +#~ msgid "Customer Type" +#~ msgstr "" #: apps/web/src/app/(dashboard)/settings/billing/billing-plans.tsx:28 msgid "Daily" @@ -1396,8 +1397,6 @@ msgid "Edit webhook" msgstr "" #: apps/web/src/app/(dashboard)/admin/documents/[id]/recipient-item.tsx:166 -#: apps/web/src/app/(dashboard)/admin/leaderboard/data-table-leaderboard.tsx:68 -#: apps/web/src/app/(dashboard)/admin/leaderboard/leaderboard-table.tsx:68 #: apps/web/src/app/(dashboard)/admin/users/[id]/page.tsx:114 #: apps/web/src/app/(dashboard)/admin/users/data-table-users.tsx:71 #: apps/web/src/app/(dashboard)/templates/use-template-dialog.tsx:213 @@ -1990,8 +1989,7 @@ msgid "My templates" msgstr "" #: apps/web/src/app/(dashboard)/admin/documents/[id]/recipient-item.tsx:148 -#: apps/web/src/app/(dashboard)/admin/leaderboard/data-table-leaderboard.tsx:55 -#: apps/web/src/app/(dashboard)/admin/leaderboard/leaderboard-table.tsx:55 +#: apps/web/src/app/(dashboard)/admin/leaderboard/data-table-leaderboard.tsx:57 #: apps/web/src/app/(dashboard)/admin/users/[id]/page.tsx:99 #: apps/web/src/app/(dashboard)/admin/users/data-table-users.tsx:66 #: apps/web/src/app/(dashboard)/settings/security/passkeys/user-passkeys-data-table-actions.tsx:144 @@ -2596,8 +2594,7 @@ msgstr "" msgid "Search by document title" msgstr "" -#: apps/web/src/app/(dashboard)/admin/leaderboard/data-table-leaderboard.tsx:133 -#: apps/web/src/app/(dashboard)/admin/leaderboard/leaderboard-table.tsx:133 +#: apps/web/src/app/(dashboard)/admin/leaderboard/data-table-leaderboard.tsx:125 #: apps/web/src/app/(dashboard)/admin/users/data-table-users.tsx:144 msgid "Search by name or email" msgstr "" @@ -2819,9 +2816,8 @@ msgstr "" msgid "Signing up..." msgstr "" -#: apps/web/src/app/(dashboard)/admin/leaderboard/data-table-leaderboard.tsx:81 -#: apps/web/src/app/(dashboard)/admin/leaderboard/leaderboard-table.tsx:81 -#: apps/web/src/app/(dashboard)/admin/leaderboard/page.tsx:36 +#: apps/web/src/app/(dashboard)/admin/leaderboard/data-table-leaderboard.tsx:71 +#: apps/web/src/app/(dashboard)/admin/leaderboard/page.tsx:37 msgid "Signing Volume" msgstr "" diff --git a/packages/lib/translations/en/web.po b/packages/lib/translations/en/web.po index 05c4c426d..0b5d5b07c 100644 --- a/packages/lib/translations/en/web.po +++ b/packages/lib/translations/en/web.po @@ -956,6 +956,7 @@ msgid "Create your account and start using state-of-the-art document signing. Op msgstr "Create your account and start using state-of-the-art document signing. Open and beautiful signing is within your grasp." #: apps/web/src/app/(dashboard)/admin/documents/document-results.tsx:62 +#: apps/web/src/app/(dashboard)/admin/leaderboard/data-table-leaderboard.tsx:85 #: apps/web/src/app/(dashboard)/documents/[id]/document-page-view-information.tsx:35 #: apps/web/src/app/(dashboard)/documents/data-table.tsx:54 #: apps/web/src/app/(dashboard)/settings/security/passkeys/user-passkeys-data-table.tsx:65 @@ -1002,8 +1003,8 @@ msgstr "Current plan: {0}" #: apps/web/src/app/(dashboard)/admin/leaderboard/data-table-leaderboard.tsx:94 #: apps/web/src/app/(dashboard)/admin/leaderboard/leaderboard-table.tsx:94 -msgid "Customer Type" -msgstr "Customer Type" +#~ msgid "Customer Type" +#~ msgstr "Customer Type" #: apps/web/src/app/(dashboard)/settings/billing/billing-plans.tsx:28 msgid "Daily" @@ -1400,8 +1401,6 @@ msgid "Edit webhook" msgstr "Edit webhook" #: apps/web/src/app/(dashboard)/admin/documents/[id]/recipient-item.tsx:166 -#: apps/web/src/app/(dashboard)/admin/leaderboard/data-table-leaderboard.tsx:68 -#: apps/web/src/app/(dashboard)/admin/leaderboard/leaderboard-table.tsx:68 #: apps/web/src/app/(dashboard)/admin/users/[id]/page.tsx:114 #: apps/web/src/app/(dashboard)/admin/users/data-table-users.tsx:71 #: apps/web/src/app/(dashboard)/templates/use-template-dialog.tsx:213 @@ -2008,8 +2007,7 @@ msgid "My templates" msgstr "My templates" #: apps/web/src/app/(dashboard)/admin/documents/[id]/recipient-item.tsx:148 -#: apps/web/src/app/(dashboard)/admin/leaderboard/data-table-leaderboard.tsx:55 -#: apps/web/src/app/(dashboard)/admin/leaderboard/leaderboard-table.tsx:55 +#: apps/web/src/app/(dashboard)/admin/leaderboard/data-table-leaderboard.tsx:57 #: apps/web/src/app/(dashboard)/admin/users/[id]/page.tsx:99 #: apps/web/src/app/(dashboard)/admin/users/data-table-users.tsx:66 #: apps/web/src/app/(dashboard)/settings/security/passkeys/user-passkeys-data-table-actions.tsx:144 @@ -2614,8 +2612,7 @@ msgstr "Search" msgid "Search by document title" msgstr "Search by document title" -#: apps/web/src/app/(dashboard)/admin/leaderboard/data-table-leaderboard.tsx:133 -#: apps/web/src/app/(dashboard)/admin/leaderboard/leaderboard-table.tsx:133 +#: apps/web/src/app/(dashboard)/admin/leaderboard/data-table-leaderboard.tsx:125 #: apps/web/src/app/(dashboard)/admin/users/data-table-users.tsx:144 msgid "Search by name or email" msgstr "Search by name or email" @@ -2841,9 +2838,8 @@ msgstr "Signing in..." msgid "Signing up..." msgstr "Signing up..." -#: apps/web/src/app/(dashboard)/admin/leaderboard/data-table-leaderboard.tsx:81 -#: apps/web/src/app/(dashboard)/admin/leaderboard/leaderboard-table.tsx:81 -#: apps/web/src/app/(dashboard)/admin/leaderboard/page.tsx:36 +#: apps/web/src/app/(dashboard)/admin/leaderboard/data-table-leaderboard.tsx:71 +#: apps/web/src/app/(dashboard)/admin/leaderboard/page.tsx:37 msgid "Signing Volume" msgstr "Signing Volume"