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.
This commit is contained in:
Ephraim Duncan
2025-04-24 06:14:38 +00:00
committed by GitHub
parent 6540291055
commit f0dcf7e9bf
3 changed files with 195 additions and 85 deletions

View File

@ -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 = {

View File

@ -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 (
<div>
<div className="flex items-center">
<h2 className="text-4xl font-semibold">
<Trans>Signing Volume</Trans>
</h2>
</div>
<div className="mt-8">
<AdminLeaderboardTable
signingVolume={signingVolume}

View File

@ -1,16 +1,7 @@
import { DocumentStatus, SubscriptionStatus } from '@prisma/client';
import { prisma } from '@documenso/prisma';
import { DocumentStatus, SubscriptionStatus } from '@documenso/prisma/client';
import { kyselyPrisma, sql } from '@documenso/prisma';
export type SigningVolume = {
id: number;
name: string;
signingVolume: number;
createdAt: Date;
planId: string;
};
export type GetSigningVolumeOptions = {
type GetSigningVolumeOptions = {
search?: string;
page?: number;
perPage?: number;
@ -18,83 +9,187 @@ export type GetSigningVolumeOptions = {
sortOrder?: 'asc' | 'desc';
};
export async function getSigningVolume({
export const getSigningVolume = async ({
search = '',
page = 1,
perPage = 10,
sortBy = 'signingVolume',
sortOrder = 'desc',
}: GetSigningVolumeOptions) {
const offset = Math.max(page - 1, 0) * perPage;
}: GetSigningVolumeOptions) => {
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<string>`COALESCE(u.name, t.name, u.email, 'Unknown')`.as('name'),
sql<number>`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();
activeSubscriptions.forEach((subscription) => {
const isTeam = !!subscription.teamId;
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 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;
}
}
findQuery = findQuery.limit(perPage).offset(offset);
if (subscription.teamId) {
const teamCount = await prisma.document.count({
where: {
teamId: subscription.teamId,
status: DocumentStatus.COMPLETED,
},
});
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')]);
const [results, [{ count }]] = await Promise.all([findQuery.execute(), countQuery.execute()]);
signingVolume += teamCount;
}
return {
leaderboard: results,
totalPages: Math.ceil(Number(count) / perPage),
...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: paginatedResults,
totalPages,
};
};