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"