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