Compare commits

...

15 Commits

Author SHA1 Message Date
Ephraim Atta-Duncan
b4dbe1a4e0 fix: translations 2025-02-25 12:08:02 +00:00
Ephraim Atta-Duncan
21c1a2c25a chore: wip 2025-02-25 11:47:15 +00:00
Ephraim Atta-Duncan
ef66e99634 chore: wip 2025-02-25 11:37:54 +00:00
Ephraim Atta-Duncan
55dded30a7 chore: remove duplicateS 2025-02-25 11:23:35 +00:00
Ephraim Atta-Duncan
a12c4a67f1 chore: wip 2025-02-25 11:12:15 +00:00
Ephraim Atta-Duncan
59de996603 chore: wip 2025-02-25 11:00:28 +00:00
Ephraim Atta-Duncan
6f930ece4e chore: wip1 2025-02-25 10:41:28 +00:00
Ephraim Atta-Duncan
87f66edd95 chore: wip 2025-02-25 10:24:25 +00:00
Ephraim Atta-Duncan
3f4c3863e7 chore: wip 2025-02-25 09:59:50 +00:00
Ephraim Atta-Duncan
70a3f7b3e9 chore: wip 2025-02-25 09:44:10 +00:00
Ephraim Atta-Duncan
633274bab1 chore: wip 2025-02-25 08:21:00 +00:00
Ephraim Atta-Duncan
2cbe14572b chore: wip 2025-02-19 16:54:48 +00:00
Ephraim Atta-Duncan
442ba9d052 chore: wip 2025-02-19 08:41:26 +00:00
Ephraim Atta-Duncan
2cf61b92fd fix: typo 2025-02-19 08:17:36 +00:00
Ephraim Atta-Duncan
aedf101965 chore: minor changes 2025-02-19 01:08:56 +00:00
3 changed files with 191 additions and 85 deletions

View File

@@ -16,9 +16,13 @@ import { Input } from '@documenso/ui/primitives/input';
export type SigningVolume = { export type SigningVolume = {
id: number; id: number;
name: string; name: string;
email: string;
signingVolume: number; signingVolume: number;
createdAt: Date; createdAt: Date;
planId: string; planId: string;
userId?: number | null;
teamId?: number | null;
isTeam: boolean;
}; };
type LeaderboardTableProps = { type LeaderboardTableProps = {

View File

@@ -4,7 +4,7 @@ import { setupI18nSSR } from '@documenso/lib/client-only/providers/i18n.server';
import { getRequiredServerComponentSession } from '@documenso/lib/next-auth/get-server-component-session'; import { getRequiredServerComponentSession } from '@documenso/lib/next-auth/get-server-component-session';
import { isAdmin } from '@documenso/lib/next-auth/guards/is-admin'; import { isAdmin } from '@documenso/lib/next-auth/guards/is-admin';
import { LeaderboardTable } from './data-table-leaderboard'; import { LeaderboardTable, type SigningVolume } from './data-table-leaderboard';
import { search } from './fetch-leaderboard.actions'; import { search } from './fetch-leaderboard.actions';
type AdminLeaderboardProps = { type AdminLeaderboardProps = {
@@ -32,7 +32,7 @@ export default async function Leaderboard({ searchParams = {} }: AdminLeaderboar
const sortBy = searchParams.sortBy || 'signingVolume'; const sortBy = searchParams.sortBy || 'signingVolume';
const sortOrder = searchParams.sortOrder || 'desc'; const sortOrder = searchParams.sortOrder || 'desc';
const { leaderboard: signingVolume, totalPages } = await search({ const { leaderboard, totalPages } = await search({
search: searchString, search: searchString,
page, page,
perPage, perPage,
@@ -40,14 +40,22 @@ export default async function Leaderboard({ searchParams = {} }: AdminLeaderboar
sortOrder, sortOrder,
}); });
const typedSigningVolume: SigningVolume[] = leaderboard.map((item) => ({
...item,
name: item.name || '',
createdAt: item.createdAt || new Date(),
}));
return ( return (
<div> <div>
<h2 className="text-4xl font-semibold"> <div className="flex items-center">
<Trans>Signing Volume</Trans> <h2 className="text-4xl font-semibold">
</h2> <Trans>Signing Volume</Trans>
</h2>
</div>
<div className="mt-8"> <div className="mt-8">
<LeaderboardTable <LeaderboardTable
signingVolume={signingVolume} signingVolume={typedSigningVolume}
totalPages={totalPages} totalPages={totalPages}
page={page} page={page}
perPage={perPage} perPage={perPage}

View File

@@ -1,15 +1,7 @@
import { kyselyPrisma, sql } from '@documenso/prisma'; import { prisma } from '@documenso/prisma';
import { DocumentStatus, SubscriptionStatus } from '@documenso/prisma/client'; import { DocumentStatus, SubscriptionStatus } from '@documenso/prisma/client';
export type SigningVolume = { type GetSigningVolumeOptions = {
id: number;
name: string;
signingVolume: number;
createdAt: Date;
planId: string;
};
export type GetSigningVolumeOptions = {
search?: string; search?: string;
page?: number; page?: number;
perPage?: number; perPage?: number;
@@ -17,85 +9,187 @@ export type GetSigningVolumeOptions = {
sortOrder?: 'asc' | 'desc'; sortOrder?: 'asc' | 'desc';
}; };
export async function getSigningVolume({ export const getSigningVolume = async ({
search = '', search = '',
page = 1, page = 1,
perPage = 10, perPage = 10,
sortBy = 'signingVolume', sortBy = 'signingVolume',
sortOrder = 'desc', sortOrder = 'desc',
}: GetSigningVolumeOptions) { }: GetSigningVolumeOptions) => {
const offset = Math.max(page - 1, 0) * perPage; const validPage = Math.max(1, page);
const validPerPage = Math.max(1, perPage);
const skip = (validPage - 1) * validPerPage;
let findQuery = kyselyPrisma.$kysely const activeSubscriptions = await prisma.subscription.findMany({
.selectFrom('Subscription as s') where: {
.leftJoin('User as u', 's.userId', 'u.id') status: SubscriptionStatus.ACTIVE,
.leftJoin('Team as t', 's.teamId', 't.id') },
.leftJoin('Document as ud', (join) => select: {
join id: true,
.onRef('u.id', '=', 'ud.userId') planId: true,
.on('ud.status', '=', sql.lit(DocumentStatus.COMPLETED)) userId: true,
.on('ud.deletedAt', 'is', null) teamId: true,
.on('ud.teamId', 'is', null), createdAt: true,
) user: {
.leftJoin('Document as td', (join) => select: {
join id: true,
.onRef('t.id', '=', 'td.teamId') name: true,
.on('td.status', '=', sql.lit(DocumentStatus.COMPLETED)) email: true,
.on('td.deletedAt', 'is', null), createdAt: true,
) },
// @ts-expect-error - Raw SQL enum casting not properly typed by Kysely },
.where(sql`s.status = ${SubscriptionStatus.ACTIVE}::"SubscriptionStatus"`) team: {
.where((eb) => select: {
eb.or([ id: true,
eb('u.name', 'ilike', `%${search}%`), name: true,
eb('u.email', 'ilike', `%${search}%`), teamEmail: {
eb('t.name', 'ilike', `%${search}%`), select: {
]), email: true,
) },
.select([ },
's.id as id', createdAt: true,
'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']);
switch (sortBy) { const userSubscriptionsMap = new Map();
case 'name': const teamSubscriptionsMap = new Map();
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); activeSubscriptions.forEach((subscription) => {
const isTeam = !!subscription.teamId;
const countQuery = kyselyPrisma.$kysely if (isTeam && subscription.teamId) {
.selectFrom('Subscription as s') if (!teamSubscriptionsMap.has(subscription.teamId)) {
.leftJoin('User as u', 's.userId', 'u.id') teamSubscriptionsMap.set(subscription.teamId, {
.leftJoin('Team as t', 's.teamId', 't.id') id: subscription.id,
// @ts-expect-error - Raw SQL enum casting not properly typed by Kysely planId: subscription.planId,
.where(sql`s.status = ${SubscriptionStatus.ACTIVE}::"SubscriptionStatus"`) teamId: subscription.teamId,
.where((eb) => name: subscription.team?.name || '',
eb.or([ email: subscription.team?.teamEmail?.email || `Team ${subscription.team?.id}`,
eb('u.name', 'ilike', `%${search}%`), createdAt: subscription.team?.createdAt,
eb('u.email', 'ilike', `%${search}%`), isTeam: true,
eb('t.name', 'ilike', `%${search}%`), subscriptionIds: [subscription.id],
]), });
) } else {
.select(({ fn }) => [fn.countAll().as('count')]); 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 { return {
leaderboard: results, leaderboard: paginatedResults,
totalPages: Math.ceil(Number(count) / perPage), totalPages,
}; };
} };