diff --git a/apps/web/src/app/(dashboard)/admin/stats/monthly-active-users-chart.tsx b/apps/web/src/app/(dashboard)/admin/stats/monthly-active-users-chart.tsx new file mode 100644 index 000000000..1ffd6d216 --- /dev/null +++ b/apps/web/src/app/(dashboard)/admin/stats/monthly-active-users-chart.tsx @@ -0,0 +1,84 @@ +'use client'; + +import { DateTime } from 'luxon'; +import type { TooltipProps } from 'recharts'; +import { Bar, BarChart, ResponsiveContainer, Tooltip, XAxis, YAxis } from 'recharts'; +import type { NameType, ValueType } from 'recharts/types/component/DefaultTooltipContent'; + +import type { GetMonthlyActiveUsersResult } from '@documenso/lib/server-only/admin/get-users-stats'; +import { cn } from '@documenso/ui/lib/utils'; + +export type MonthlyActiveUsersChartProps = { + className?: string; + title: string; + data: GetMonthlyActiveUsersResult; + tooltip?: string; +}; + +const CustomTooltip = ({ + active, + payload, + label, + tooltip, +}: TooltipProps & { tooltip?: string }) => { + if (active && payload && payload.length) { + return ( +
+

{label}

+

+ {`${tooltip} : `} + {payload[0].value} +

+
+ ); + } + + return null; +}; + +export const MonthlyActiveUsersChart = ({ + className, + title, + data, + tooltip, +}: MonthlyActiveUsersChartProps) => { + const formattedData = (data: GetMonthlyActiveUsersResult) => { + return [...data].reverse().map(({ month, count }) => ({ + month: DateTime.fromFormat(month, 'yyyy-MM').toFormat('LLL'), + count: Number(count), + })); + }; + + return ( +
+
+

{title}

+
+ +
+ + + + + + } + labelStyle={{ + color: 'hsl(var(--primary-foreground))', + }} + cursor={{ fill: 'hsl(var(--primary) / 10%)' }} + /> + + + + +
+
+ ); +}; diff --git a/apps/web/src/app/(dashboard)/admin/stats/page.tsx b/apps/web/src/app/(dashboard)/admin/stats/page.tsx index 51b243cc6..9384da065 100644 --- a/apps/web/src/app/(dashboard)/admin/stats/page.tsx +++ b/apps/web/src/app/(dashboard)/admin/stats/page.tsx @@ -19,6 +19,7 @@ import { setupI18nSSR } from '@documenso/lib/client-only/providers/i18n.server'; import { getDocumentStats } from '@documenso/lib/server-only/admin/get-documents-stats'; import { getRecipientsStats } from '@documenso/lib/server-only/admin/get-recipients-stats'; import { + getMonthlyActiveUsers, getUserWithSignedDocumentMonthlyGrowth, getUsersCount, getUsersWithLastSignedInCount, @@ -28,6 +29,7 @@ import { getSignerConversionMonthly } from '@documenso/lib/server-only/user/get- import { CardMetric } from '~/components/(dashboard)/metric-card/metric-card'; +import { MonthlyActiveUsersChart } from './monthly-active-users-chart'; import { SignerConversionChart } from './signer-conversion-chart'; import { UserWithDocumentChart } from './user-with-document'; @@ -46,6 +48,7 @@ export default async function AdminStatsPage() { // userWithAtLeastOneDocumentSignedPerMonth, MONTHLY_USERS_SIGNED, usersWithLastSignedInCount, + monthlyActiveUsers, ] = await Promise.all([ getUsersCount(), getUsersWithSubscriptionsCount(), @@ -56,6 +59,7 @@ export default async function AdminStatsPage() { // getUserWithAtLeastOneDocumentSignedPerMonth(), getUserWithSignedDocumentMonthlyGrowth(), getUsersWithLastSignedInCount(), + getMonthlyActiveUsers(), ]); return ( @@ -140,6 +144,11 @@ export default async function AdminStatsPage() { Charts
+ { signed_count: Number(row.signed_count), })); }; + +export type GetMonthlyActiveUsersResult = Array<{ + month: string; + count: number; +}>; + +type GetMonthlyActiveUsersQueryResult = Array<{ + month: Date; + count: bigint; +}>; + +export const getMonthlyActiveUsers = async () => { + const result = await prisma.$queryRaw` + SELECT + DATE_TRUNC('month', "lastSignedIn") AS "month", + COUNT(DISTINCT "id") as "count" + FROM "User" + WHERE "lastSignedIn" >= NOW() - INTERVAL '1 year' + GROUP BY "month" + ORDER BY "month" DESC + LIMIT 12 + `; + + return result.map((row) => ({ + month: DateTime.fromJSDate(row.month).toFormat('yyyy-MM'), + count: Number(row.count), + })); +};