From b03c5ab1a7e325f180097d4c7165387847bc3978 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) --- .../tables/admin-leaderboard-table.tsx | 37 ++++- .../server-only/admin/get-signing-volume.ts | 154 ++++++++---------- 2 files changed, 102 insertions(+), 89 deletions(-) diff --git a/apps/remix/app/components/tables/admin-leaderboard-table.tsx b/apps/remix/app/components/tables/admin-leaderboard-table.tsx index 44ab8a0a4..eca409eb9 100644 --- a/apps/remix/app/components/tables/admin-leaderboard-table.tsx +++ b/apps/remix/app/components/tables/admin-leaderboard-table.tsx @@ -2,7 +2,7 @@ import { useEffect, useMemo, useState, useTransition } from 'react'; import { msg } from '@lingui/core/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'; @@ -52,7 +52,15 @@ export const AdminLeaderboardTable = ({ onClick={() => handleColumnSort('name')} > {_(msg`Name`)} - + {sortBy === 'name' ? ( + sortOrder === 'asc' ? ( + + ) : ( + + ) + ) : ( + + )} ), accessorKey: 'name', @@ -78,7 +86,15 @@ export const AdminLeaderboardTable = ({ onClick={() => handleColumnSort('signingVolume')} > {_(msg`Signing Volume`)} - + {sortBy === 'signingVolume' ? ( + sortOrder === 'asc' ? ( + + ) : ( + + ) + ) : ( + + )} ), accessorKey: 'signingVolume', @@ -92,7 +108,15 @@ export const AdminLeaderboardTable = ({ onClick={() => handleColumnSort('createdAt')} > {_(msg`Created`)} - + {sortBy === 'createdAt' ? ( + sortOrder === 'asc' ? ( + + ) : ( + + ) + ) : ( + + )} ); }, @@ -100,7 +124,7 @@ export const AdminLeaderboardTable = ({ cell: ({ row }) => i18n.date(row.original.createdAt), }, ] satisfies DataTableColumnDef[]; - }, [sortOrder]); + }, [sortOrder, sortBy]); useEffect(() => { startTransition(() => { @@ -131,6 +155,9 @@ export const AdminLeaderboardTable = ({ 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 5b33b9e9e..bf2a5d038 100644 --- a/packages/lib/server-only/admin/get-signing-volume.ts +++ b/packages/lib/server-only/admin/get-signing-volume.ts @@ -1,6 +1,6 @@ -import { DocumentStatus, Prisma } from '@prisma/client'; +import { DocumentStatus } from '@prisma/client'; -import { prisma } from '@documenso/prisma'; +import { kyselyPrisma, sql } from '@documenso/prisma'; export type SigningVolume = { id: number; @@ -25,92 +25,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), }; }