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), }; }