From f0dcf7e9bf01a4cc23d2be13741b6b38ace753ad Mon Sep 17 00:00:00 2001 From: Ephraim Duncan <55143799+ephraimduncan@users.noreply.github.com> Date: Thu, 24 Apr 2025 06:14:38 +0000 Subject: [PATCH] fix: signing volume query (#1753) This pull request updates the implementation of the admin leaderboard, enhancing data handling and improving type safety. It introduces clearer differentiation between users and teams, adds additional fields to track more relevant information, and refactors the querying logic to optimize performance and maintainability. --- .../tables/admin-leaderboard-table.tsx | 4 + .../_authenticated+/admin+/leaderboard.tsx | 23 +- .../server-only/admin/get-signing-volume.ts | 253 ++++++++++++------ 3 files changed, 195 insertions(+), 85 deletions(-) diff --git a/apps/remix/app/components/tables/admin-leaderboard-table.tsx b/apps/remix/app/components/tables/admin-leaderboard-table.tsx index eca409eb9..d7baf2cb9 100644 --- a/apps/remix/app/components/tables/admin-leaderboard-table.tsx +++ b/apps/remix/app/components/tables/admin-leaderboard-table.tsx @@ -14,9 +14,13 @@ import { Input } from '@documenso/ui/primitives/input'; export type SigningVolume = { id: number; name: string; + email: string; signingVolume: number; createdAt: Date; planId: string; + userId?: number | null; + teamId?: number | null; + isTeam: boolean; }; type LeaderboardTableProps = { diff --git a/apps/remix/app/routes/_authenticated+/admin+/leaderboard.tsx b/apps/remix/app/routes/_authenticated+/admin+/leaderboard.tsx index a258a21aa..2a6857e5e 100644 --- a/apps/remix/app/routes/_authenticated+/admin+/leaderboard.tsx +++ b/apps/remix/app/routes/_authenticated+/admin+/leaderboard.tsx @@ -2,7 +2,10 @@ import { Trans } from '@lingui/react/macro'; import { getSigningVolume } from '@documenso/lib/server-only/admin/get-signing-volume'; -import { AdminLeaderboardTable } from '~/components/tables/admin-leaderboard-table'; +import { + AdminLeaderboardTable, + type SigningVolume, +} from '~/components/tables/admin-leaderboard-table'; import type { Route } from './+types/leaderboard'; @@ -25,7 +28,7 @@ export async function loader({ request }: Route.LoaderArgs) { const perPage = Number(url.searchParams.get('perPage')) || 10; const search = url.searchParams.get('search') || ''; - const { leaderboard: signingVolume, totalPages } = await getSigningVolume({ + const { leaderboard, totalPages } = await getSigningVolume({ search, page, perPage, @@ -33,8 +36,14 @@ export async function loader({ request }: Route.LoaderArgs) { sortOrder, }); + const typedSigningVolume: SigningVolume[] = leaderboard.map((item) => ({ + ...item, + name: item.name || '', + createdAt: item.createdAt || new Date(), + })); + return { - signingVolume, + signingVolume: typedSigningVolume, totalPages, page, perPage, @@ -48,9 +57,11 @@ export default function Leaderboard({ loaderData }: Route.ComponentProps) { return (
-

- Signing Volume -

+
+

+ Signing Volume +

+
{ + const validPage = Math.max(1, page); + const validPerPage = Math.max(1, perPage); + const skip = (validPage - 1) * validPerPage; - 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), - ) - .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 activeSubscriptions = await prisma.subscription.findMany({ + where: { + status: SubscriptionStatus.ACTIVE, + }, + select: { + id: true, + planId: true, + userId: true, + teamId: true, + createdAt: true, + user: { + select: { + id: true, + name: true, + email: true, + createdAt: true, + }, + }, + team: { + select: { + id: true, + name: true, + teamEmail: { + select: { + email: true, + }, + }, + createdAt: true, + }, + }, + }, + }); - 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'); - } + const userSubscriptionsMap = new Map(); + const teamSubscriptionsMap = new Map(); - findQuery = findQuery.limit(perPage).offset(offset); + activeSubscriptions.forEach((subscription) => { + const isTeam = !!subscription.teamId; - const countQuery = kyselyPrisma.$kysely - .selectFrom('Subscription as s') - .leftJoin('User as u', 's.userId', 'u.id') - .leftJoin('Team as t', 's.teamId', 't.id') - .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')]); + if (isTeam && subscription.teamId) { + if (!teamSubscriptionsMap.has(subscription.teamId)) { + teamSubscriptionsMap.set(subscription.teamId, { + id: subscription.id, + planId: subscription.planId, + teamId: subscription.teamId, + name: subscription.team?.name || '', + email: subscription.team?.teamEmail?.email || `Team ${subscription.team?.id}`, + createdAt: subscription.team?.createdAt, + isTeam: true, + subscriptionIds: [subscription.id], + }); + } else { + const existingTeam = teamSubscriptionsMap.get(subscription.teamId); + existingTeam.subscriptionIds.push(subscription.id); + } + } else if (subscription.userId) { + if (!userSubscriptionsMap.has(subscription.userId)) { + userSubscriptionsMap.set(subscription.userId, { + id: subscription.id, + planId: subscription.planId, + userId: subscription.userId, + name: subscription.user?.name || '', + email: subscription.user?.email || '', + createdAt: subscription.user?.createdAt, + isTeam: false, + subscriptionIds: [subscription.id], + }); + } else { + const existingUser = userSubscriptionsMap.get(subscription.userId); + existingUser.subscriptionIds.push(subscription.id); + } + } + }); - const [results, [{ count }]] = await Promise.all([findQuery.execute(), countQuery.execute()]); + const subscriptions = [ + ...Array.from(userSubscriptionsMap.values()), + ...Array.from(teamSubscriptionsMap.values()), + ]; + + const filteredSubscriptions = search + ? subscriptions.filter((sub) => { + const searchLower = search.toLowerCase(); + return ( + sub.name?.toLowerCase().includes(searchLower) || + sub.email?.toLowerCase().includes(searchLower) + ); + }) + : subscriptions; + + const signingVolume = await Promise.all( + filteredSubscriptions.map(async (subscription) => { + let signingVolume = 0; + + if (subscription.userId && !subscription.isTeam) { + const personalCount = await prisma.document.count({ + where: { + userId: subscription.userId, + status: DocumentStatus.COMPLETED, + teamId: null, + }, + }); + + signingVolume += personalCount; + + const userTeams = await prisma.teamMember.findMany({ + where: { + userId: subscription.userId, + }, + select: { + teamId: true, + }, + }); + + if (userTeams.length > 0) { + const teamIds = userTeams.map((team) => team.teamId); + const teamCount = await prisma.document.count({ + where: { + teamId: { + in: teamIds, + }, + status: DocumentStatus.COMPLETED, + }, + }); + + signingVolume += teamCount; + } + } + + if (subscription.teamId) { + const teamCount = await prisma.document.count({ + where: { + teamId: subscription.teamId, + status: DocumentStatus.COMPLETED, + }, + }); + + signingVolume += teamCount; + } + + return { + ...subscription, + signingVolume, + }; + }), + ); + + const sortedResults = [...signingVolume].sort((a, b) => { + if (sortBy === 'name') { + return sortOrder === 'asc' + ? (a.name || '').localeCompare(b.name || '') + : (b.name || '').localeCompare(a.name || ''); + } + + if (sortBy === 'createdAt') { + const dateA = a.createdAt ? new Date(a.createdAt).getTime() : 0; + const dateB = b.createdAt ? new Date(b.createdAt).getTime() : 0; + return sortOrder === 'asc' ? dateA - dateB : dateB - dateA; + } + + return sortOrder === 'asc' + ? a.signingVolume - b.signingVolume + : b.signingVolume - a.signingVolume; + }); + + const paginatedResults = sortedResults.slice(skip, skip + validPerPage); + + const totalPages = Math.ceil(sortedResults.length / validPerPage); return { - leaderboard: results, - totalPages: Math.ceil(Number(count) / perPage), + leaderboard: paginatedResults, + totalPages, }; -} +};