feat: admin activation metrics (#1080)

![CleanShot 2024-05-21 at 09 42
42@2x](https://github.com/documenso/documenso/assets/55143799/8d9d972b-6ebd-4e42-adba-62edba188460)


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

- **New Features**
- Introduced user statistics display including metrics on document
creation and signing in the admin dashboard.
- Added a bar chart visualizing monthly growth data of users with
documents.

- **Enhancements**
- Updated dashboard stats page to provide more detailed user-related
metrics and charts.
- Added new CSS variable `--gold` for consistent theming across light
and dark modes.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
Timur Ercan
2024-06-25 17:37:52 +02:00
committed by GitHub
4 changed files with 201 additions and 6 deletions

View File

@ -14,18 +14,34 @@ import {
import { getDocumentStats } from '@documenso/lib/server-only/admin/get-documents-stats';
import { getRecipientsStats } from '@documenso/lib/server-only/admin/get-recipients-stats';
import {
getUserWithAtLeastOneDocumentPerMonth,
getUserWithAtLeastOneDocumentSignedPerMonth,
getUserWithSignedDocumentMonthlyGrowth,
getUsersCount,
getUsersWithSubscriptionsCount,
} from '@documenso/lib/server-only/admin/get-users-stats';
import { CardMetric } from '~/components/(dashboard)/metric-card/metric-card';
import { UserWithDocumentChart } from './user-with-document';
export default async function AdminStatsPage() {
const [usersCount, usersWithSubscriptionsCount, docStats, recipientStats] = await Promise.all([
const [
usersCount,
usersWithSubscriptionsCount,
docStats,
recipientStats,
userWithAtLeastOneDocumentPerMonth,
userWithAtLeastOneDocumentSignedPerMonth,
MONTHLY_USERS_SIGNED,
] = await Promise.all([
getUsersCount(),
getUsersWithSubscriptionsCount(),
getDocumentStats(),
getRecipientsStats(),
getUserWithAtLeastOneDocumentPerMonth(),
getUserWithAtLeastOneDocumentSignedPerMonth(),
getUserWithSignedDocumentMonthlyGrowth(),
]);
return (
@ -43,12 +59,11 @@ export default async function AdminStatsPage() {
<CardMetric icon={UserPlus2} title="App Version" value={`v${process.env.APP_VERSION}`} />
</div>
<div className="mt-16 grid grid-cols-1 gap-8 md:grid-cols-1 lg:grid-cols-2">
<div className="mt-16 gap-8">
<div>
<h3 className="text-3xl font-semibold">Document metrics</h3>
<div className="mt-8 grid flex-1 grid-cols-2 gap-4">
<CardMetric icon={File} title="Total Documents" value={docStats.ALL} />
<div className="mb-8 mt-4 grid flex-1 grid-cols-1 gap-4 md:grid-cols-2">
<CardMetric icon={FileEdit} title="Drafted Documents" value={docStats.DRAFT} />
<CardMetric icon={FileClock} title="Pending Documents" value={docStats.PENDING} />
<CardMetric icon={FileCheck} title="Completed Documents" value={docStats.COMPLETED} />
@ -58,7 +73,7 @@ export default async function AdminStatsPage() {
<div>
<h3 className="text-3xl font-semibold">Recipients metrics</h3>
<div className="mt-8 grid flex-1 grid-cols-2 gap-4">
<div className="mb-8 mt-4 grid flex-1 grid-cols-1 gap-4 md:grid-cols-2">
<CardMetric
icon={UserSquare2}
title="Total Recipients"
@ -70,6 +85,23 @@ export default async function AdminStatsPage() {
</div>
</div>
</div>
<div className="mt-16">
<h3 className="text-3xl font-semibold">Charts</h3>
<div className="mt-5 grid grid-cols-2 gap-10">
<UserWithDocumentChart
data={MONTHLY_USERS_SIGNED}
title="MAU (created document)"
tooltip="Monthly Active Users: Users that created at least one Document"
/>
<UserWithDocumentChart
data={MONTHLY_USERS_SIGNED}
completed
title="MAU (had document completed)"
tooltip="Monthly Active Users: Users that had at least one of their documents completed"
/>
</div>
</div>
</div>
);
}

View File

@ -0,0 +1,95 @@
'use client';
import { DateTime } from 'luxon';
import { Bar, BarChart, ResponsiveContainer, Tooltip, XAxis, YAxis } from 'recharts';
import type { TooltipProps } from 'recharts';
import type { NameType, ValueType } from 'recharts/types/component/DefaultTooltipContent';
import type { GetUserWithDocumentMonthlyGrowth } from '@documenso/lib/server-only/admin/get-users-stats';
export type UserWithDocumentChartProps = {
className?: string;
title: string;
data: GetUserWithDocumentMonthlyGrowth;
completed?: boolean;
tooltip?: string;
};
const CustomTooltip = ({
active,
payload,
label,
tooltip,
}: TooltipProps<ValueType, NameType> & { tooltip?: string }) => {
if (active && payload && payload.length) {
return (
<div className="z-100 w-60 space-y-1 rounded-md border border-solid bg-white p-2 px-3">
<p className="">{label}</p>
<p className="text-documenso">
{`${tooltip} : `}
<span className="text-black">{payload[0].value}</span>
</p>
</div>
);
}
return null;
};
export const UserWithDocumentChart = ({
className,
data,
title,
completed = false,
tooltip,
}: UserWithDocumentChartProps) => {
const formattedData = (data: GetUserWithDocumentMonthlyGrowth, completed: boolean) => {
return [...data].reverse().map(({ month, count, signed_count }) => {
const formattedMonth = DateTime.fromFormat(month, 'yyyy-MM').toFormat('LLL');
if (completed) {
return {
month: formattedMonth,
count: Number(signed_count),
};
} else {
return {
month: formattedMonth,
count: Number(count),
};
}
});
};
return (
<div className={className}>
<div className="border-border flex flex-1 flex-col justify-center rounded-2xl border p-6 pl-2">
<div className="mb-6 flex h-12 px-4">
<h3 className="text-lg font-semibold">{title}</h3>
</div>
<ResponsiveContainer width="100%" height={400}>
<BarChart className="bg-white" data={formattedData(data, completed)}>
<XAxis dataKey="month" />
<YAxis />
<Tooltip
content={<CustomTooltip tooltip={tooltip} />}
labelStyle={{
color: 'hsl(var(--primary-foreground))',
}}
cursor={{ fill: 'hsl(var(--primary) / 10%)' }}
/>
<Bar
dataKey="count"
fill="hsl(var(--primary))"
radius={[4, 4, 0, 0]}
maxBarSize={60}
label={tooltip}
/>
</BarChart>
</ResponsiveContainer>
</div>
</div>
);
};

View File

@ -1,5 +1,7 @@
import { DateTime } from 'luxon';
import { prisma } from '@documenso/prisma';
import { SubscriptionStatus } from '@documenso/prisma/client';
import { DocumentStatus, SubscriptionStatus } from '@documenso/prisma/client';
export const getUsersCount = async () => {
return await prisma.user.count();
@ -16,3 +18,65 @@ export const getUsersWithSubscriptionsCount = async () => {
},
});
};
export const getUserWithAtLeastOneDocumentPerMonth = async () => {
return await prisma.user.count({
where: {
Document: {
some: {
createdAt: {
gte: DateTime.now().minus({ months: 1 }).toJSDate(),
},
},
},
},
});
};
export const getUserWithAtLeastOneDocumentSignedPerMonth = async () => {
return await prisma.user.count({
where: {
Document: {
some: {
status: {
equals: DocumentStatus.COMPLETED,
},
completedAt: {
gte: DateTime.now().minus({ months: 1 }).toJSDate(),
},
},
},
},
});
};
export type GetUserWithDocumentMonthlyGrowth = Array<{
month: string;
count: number;
signed_count: number;
}>;
type GetUserWithDocumentMonthlyGrowthQueryResult = Array<{
month: Date;
count: bigint;
signed_count: bigint;
}>;
export const getUserWithSignedDocumentMonthlyGrowth = async () => {
const result = await prisma.$queryRaw<GetUserWithDocumentMonthlyGrowthQueryResult>`
SELECT
DATE_TRUNC('month', "Document"."createdAt") AS "month",
COUNT(DISTINCT "Document"."userId") as "count",
COUNT(DISTINCT CASE WHEN "Document"."status" = 'COMPLETED' THEN "Document"."userId" END) as "signed_count"
FROM "Document"
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),
signed_count: Number(row.signed_count),
}));
};

View File

@ -44,6 +44,8 @@
--radius: 0.5rem;
--warning: 54 96% 45%;
--gold: 47.9 95.8% 53.1%;
}
.dark {
@ -83,6 +85,8 @@
--radius: 0.5rem;
--warning: 54 96% 45%;
--gold: 47.9 95.8% 53.1%;
}
}