From c6fb101a99f5de68a6b6f2ee8e6bca269910ecee Mon Sep 17 00:00:00 2001
From: Ephraim Duncan <55143799+ephraimduncan@users.noreply.github.com>
Date: Tue, 28 Jan 2025 02:05:40 +0000
Subject: [PATCH] fix: admin leaderboard query sorting (#1548)
---
.../leaderboard/data-table-leaderboard.tsx | 37 ++++-
.../server-only/admin/get-signing-volume.ts | 154 ++++++++----------
2 files changed, 102 insertions(+), 89 deletions(-)
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 596f0051d..84855b15f 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
@@ -4,7 +4,7 @@ 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 { ChevronDownIcon, ChevronUpIcon, ChevronsUpDown, 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';
@@ -54,7 +54,15 @@ export const LeaderboardTable = ({
onClick={() => handleColumnSort('name')}
>
{_(msg`Name`)}
-
+ {sortBy === 'name' ? (
+ sortOrder === 'asc' ? (
+
+ ) : (
+
+ )
+ ) : (
+
+ )}
),
accessorKey: 'name',
@@ -80,7 +88,15 @@ export const LeaderboardTable = ({
onClick={() => handleColumnSort('signingVolume')}
>
{_(msg`Signing Volume`)}
-
+ {sortBy === 'signingVolume' ? (
+ sortOrder === 'asc' ? (
+
+ ) : (
+
+ )
+ ) : (
+
+ )}
),
accessorKey: 'signingVolume',
@@ -94,7 +110,15 @@ export const LeaderboardTable = ({
onClick={() => handleColumnSort('createdAt')}
>
{_(msg`Created`)}
-
+ {sortBy === 'createdAt' ? (
+ sortOrder === 'asc' ? (
+
+ ) : (
+
+ )
+ ) : (
+
+ )}
);
},
@@ -102,7 +126,7 @@ export const LeaderboardTable = ({
cell: ({ row }) => i18n.date(row.original.createdAt),
},
] satisfies DataTableColumnDef[];
- }, [sortOrder]);
+ }, [sortOrder, sortBy]);
useEffect(() => {
startTransition(() => {
@@ -133,6 +157,9 @@ export const LeaderboardTable = ({
const handleColumnSort = (column: 'name' | 'createdAt' | 'signingVolume') => {
startTransition(() => {
updateSearchParams({
+ search: debouncedSearchString,
+ page,
+ perPage,
sortBy: column,
sortOrder: sortBy === column && sortOrder === 'asc' ? 'desc' : 'asc',
});
diff --git a/packages/lib/server-only/admin/get-signing-volume.ts b/packages/lib/server-only/admin/get-signing-volume.ts
index 497000501..964d68ea5 100644
--- a/packages/lib/server-only/admin/get-signing-volume.ts
+++ b/packages/lib/server-only/admin/get-signing-volume.ts
@@ -1,5 +1,5 @@
-import { prisma } from '@documenso/prisma';
-import { DocumentStatus, Prisma } from '@documenso/prisma/client';
+import { kyselyPrisma, sql } from '@documenso/prisma';
+import { DocumentStatus, SubscriptionStatus } from '@documenso/prisma/client';
export type SigningVolume = {
id: number;
@@ -24,92 +24,78 @@ export async function getSigningVolume({
sortBy = 'signingVolume',
sortOrder = 'desc',
}: 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 offset = Math.max(page - 1, 0) * perPage;
- const [subscriptions, totalCount] = await Promise.all([
- prisma.subscription.findMany({
- where: whereClause,
- include: {
- user: {
- select: {
- name: true,
- email: true,
- documents: {
- where: {
- status: DocumentStatus.COMPLETED,
- deletedAt: null,
- teamId: null,
- },
- },
- },
- },
- team: {
- select: {
- name: true,
- documents: {
- where: {
- status: DocumentStatus.COMPLETED,
- deletedAt: null,
- },
- },
- },
- },
- },
- orderBy:
- sortBy === 'name'
- ? [{ user: { name: sortOrder } }, { team: { name: sortOrder } }, { createdAt: 'desc' }]
- : sortBy === 'createdAt'
- ? [{ createdAt: sortOrder }]
- : undefined,
- skip: Math.max(page - 1, 0) * perPage,
- take: perPage,
- }),
- prisma.subscription.count({
- where: whereClause,
- }),
- ]);
+ let findQuery = kyselyPrisma.$kysely
+ .selectFrom('Subscription as s')
+ .leftJoin('User as u', 's.userId', 'u.id')
+ .leftJoin('Team as t', 's.teamId', 't.id')
+ .leftJoin('Document as ud', (join) =>
+ join
+ .onRef('u.id', '=', 'ud.userId')
+ .on('ud.status', '=', sql.lit(DocumentStatus.COMPLETED))
+ .on('ud.deletedAt', 'is', null)
+ .on('ud.teamId', 'is', null),
+ )
+ .leftJoin('Document as td', (join) =>
+ join
+ .onRef('t.id', '=', 'td.teamId')
+ .on('td.status', '=', sql.lit(DocumentStatus.COMPLETED))
+ .on('td.deletedAt', 'is', null),
+ )
+ // @ts-expect-error - Raw SQL enum casting not properly typed by Kysely
+ .where(sql`s.status = ${SubscriptionStatus.ACTIVE}::"SubscriptionStatus"`)
+ .where((eb) =>
+ eb.or([
+ eb('u.name', 'ilike', `%${search}%`),
+ eb('u.email', 'ilike', `%${search}%`),
+ eb('t.name', 'ilike', `%${search}%`),
+ ]),
+ )
+ .select([
+ 's.id as id',
+ 's.createdAt as createdAt',
+ 's.planId as planId',
+ sql`COALESCE(u.name, t.name, u.email, 'Unknown')`.as('name'),
+ sql`COUNT(DISTINCT ud.id) + COUNT(DISTINCT td.id)`.as('signingVolume'),
+ ])
+ .groupBy(['s.id', 'u.name', 't.name', 'u.email']);
- const leaderboardWithVolume: SigningVolume[] = subscriptions.map((subscription) => {
- const name =
- subscription.user?.name || subscription.team?.name || subscription.user?.email || 'Unknown';
- const userSignedDocs = subscription.user?.documents?.length || 0;
- const teamSignedDocs = subscription.team?.documents?.length || 0;
- return {
- id: subscription.id,
- name,
- signingVolume: userSignedDocs + teamSignedDocs,
- createdAt: subscription.createdAt,
- planId: subscription.planId,
- };
- });
-
- if (sortBy === 'signingVolume') {
- leaderboardWithVolume.sort((a, b) => {
- return sortOrder === 'desc'
- ? b.signingVolume - a.signingVolume
- : a.signingVolume - b.signingVolume;
- });
+ switch (sortBy) {
+ case 'name':
+ findQuery = findQuery.orderBy('name', sortOrder);
+ break;
+ case 'createdAt':
+ findQuery = findQuery.orderBy('createdAt', sortOrder);
+ break;
+ case 'signingVolume':
+ findQuery = findQuery.orderBy('signingVolume', sortOrder);
+ break;
+ default:
+ findQuery = findQuery.orderBy('signingVolume', 'desc');
}
+ findQuery = findQuery.limit(perPage).offset(offset);
+
+ const countQuery = kyselyPrisma.$kysely
+ .selectFrom('Subscription as s')
+ .leftJoin('User as u', 's.userId', 'u.id')
+ .leftJoin('Team as t', 's.teamId', 't.id')
+ // @ts-expect-error - Raw SQL enum casting not properly typed by Kysely
+ .where(sql`s.status = ${SubscriptionStatus.ACTIVE}::"SubscriptionStatus"`)
+ .where((eb) =>
+ eb.or([
+ eb('u.name', 'ilike', `%${search}%`),
+ eb('u.email', 'ilike', `%${search}%`),
+ eb('t.name', 'ilike', `%${search}%`),
+ ]),
+ )
+ .select(({ fn }) => [fn.countAll().as('count')]);
+
+ const [results, [{ count }]] = await Promise.all([findQuery.execute(), countQuery.execute()]);
+
return {
- leaderboard: leaderboardWithVolume,
- totalPages: Math.ceil(totalCount / perPage),
+ leaderboard: results,
+ totalPages: Math.ceil(Number(count) / perPage),
};
}