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