Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: admin activation metrics #1080

Merged
merged 14 commits into from
Jun 25, 2024
59 changes: 54 additions & 5 deletions apps/web/src/app/(dashboard)/admin/stats/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,18 +14,35 @@ 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';
import { UserWithDocumentCummulativeChart } from './user-with-document-cummulative';

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 (
Expand All @@ -43,12 +60,30 @@ 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">
{/* TODO: remove grid and see something */}
<div className="mt-16 gap-8">
<div>
<h3 className="text-3xl font-semibold">User metrics</h3>

<div className="mb-8 mt-4 grid flex-1 grid-cols-1 gap-4 md:grid-cols-2">
<CardMetric
icon={File}
title="Users with document in the last month"
ephraimduncan marked this conversation as resolved.
Show resolved Hide resolved
value={userWithAtLeastOneDocumentPerMonth}
/>
<CardMetric
icon={File}
title="Users with signed document in the last month"
ephraimduncan marked this conversation as resolved.
Show resolved Hide resolved
value={userWithAtLeastOneDocumentSignedPerMonth}
/>
</div>
</div>

<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={File} title="Total Documents" value={docStats.ALL} /> */}
ephraimduncan marked this conversation as resolved.
Show resolved Hide resolved
<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} />
Expand All @@ -58,7 +93,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"
Expand All @@ -70,6 +105,20 @@ export default async function AdminStatsPage() {
</div>
</div>
</div>

<div className="mt-16">
<h3 className="text-3xl font-semibold">User Charts</h3>

<UserWithDocumentChart
data={MONTHLY_USERS_SIGNED}
className="col-span-12 mb-8 mt-4 lg:col-span-6"
/>

<UserWithDocumentCummulativeChart
data={MONTHLY_USERS_SIGNED}
className="col-span-12 mb-8 mt-4 lg:col-span-6"
ephraimduncan marked this conversation as resolved.
Show resolved Hide resolved
/>
</div>
</div>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
'use client';

import { DateTime } from 'luxon';
import { Bar, BarChart, ResponsiveContainer, Tooltip, XAxis, YAxis } from 'recharts';

import type { GetUserWithDocumentMonthlyGrowth } from '@documenso/lib/server-only/admin/get-users-stats';

export type UserWithDocumentCummulativeChartProps = {
className?: string;
data: GetUserWithDocumentMonthlyGrowth;
};

export const UserWithDocumentCummulativeChart = ({
className,
data,
}: UserWithDocumentCummulativeChartProps) => {
const formattedData = [...data]
.reverse()
ephraimduncan marked this conversation as resolved.
Show resolved Hide resolved
.map(({ month, cume_count: count, cume_signed_count: signed_count }) => {
return {
month: DateTime.fromFormat(month, 'yyyy-MM').toFormat('LLLL'),
ephraimduncan marked this conversation as resolved.
Show resolved Hide resolved
count: Number(count),
signed_count: Number(signed_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 px-4">
<h3 className="text-lg font-semibold">Total Activity (Cummulative)</h3>
</div>

<ResponsiveContainer width="100%" height={400}>
<BarChart data={formattedData}>
<XAxis dataKey="month" />
<YAxis />

<Tooltip
labelStyle={{
color: 'hsl(var(--primary-foreground))',
}}
formatter={(value, name) => [
Number(value).toLocaleString('en-US'),
name === 'count' ? 'User with document' : 'Users with signed document',
]}
cursor={{ fill: 'hsl(var(--primary) / 10%)' }}
/>

<Bar
dataKey="signed_count"
fill="hsl(var(--gold))"
radius={[4, 4, 0, 0]}
maxBarSize={60}
label="Documents Added"
/>
<Bar
dataKey="count"
fill="hsl(var(--primary))"
radius={[4, 4, 0, 0]}
maxBarSize={60}
label="Documents Signed"
/>
</BarChart>
</ResponsiveContainer>
</div>
</div>
);
};
64 changes: 64 additions & 0 deletions apps/web/src/app/(dashboard)/admin/stats/user-with-document.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
'use client';

import { DateTime } from 'luxon';
import { Bar, BarChart, ResponsiveContainer, Tooltip, XAxis, YAxis } from 'recharts';

import type { GetUserWithDocumentMonthlyGrowth } from '@documenso/lib/server-only/admin/get-users-stats';

export type UserWithDocumentChartProps = {
className?: string;
data: GetUserWithDocumentMonthlyGrowth;
};

export const UserWithDocumentChart = ({ className, data }: UserWithDocumentChartProps) => {
const formattedData = [...data].reverse().map(({ month, count, signed_count }) => {
return {
month: DateTime.fromFormat(month, 'yyyy-MM').toFormat('LLLL'),
count: Number(count),
signed_count: Number(signed_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 px-4">
<h3 className="text-lg font-semibold">Total Activity</h3>
</div>

<ResponsiveContainer width="100%" height={400}>
<BarChart data={formattedData}>
<XAxis dataKey="month" />
<YAxis />

<Tooltip
labelStyle={{
color: 'hsl(var(--primary-foreground))',
}}
formatter={(value, name) => [
Number(value).toLocaleString('en-US'),
name === 'count' ? 'User with document' : 'Users with signed document',
]}
cursor={{ fill: 'hsl(var(--primary) / 10%)' }}
ephraimduncan marked this conversation as resolved.
Show resolved Hide resolved
/>

<Bar
dataKey="signed_count"
fill="hsl(var(--gold))"
radius={[4, 4, 0, 0]}
maxBarSize={60}
label="Documents Added"
/>
<Bar
dataKey="count"
fill="hsl(var(--primary))"
radius={[4, 4, 0, 0]}
maxBarSize={60}
label="Documents Signed"
/>
ephraimduncan marked this conversation as resolved.
Show resolved Hide resolved
</BarChart>
</ResponsiveContainer>
</div>
</div>
);
};
74 changes: 73 additions & 1 deletion packages/lib/server-only/admin/get-users-stats.ts
Original file line number Diff line number Diff line change
@@ -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();
Expand All @@ -16,3 +18,73 @@ export const getUsersWithSubscriptionsCount = async () => {
},
});
};

export const getUserWithAtLeastOneDocumentPerMonth = async () => {
return await prisma.user.count({
where: {
Document: {
some: {
createdAt: {
gte: new Date(new Date().setMonth(new Date().getMonth() - 1)),
},
},
},
},
});
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ensure correct date calculation for fetching documents.

The date calculation in line 28 might lead to incorrect results due to direct manipulation of the date object. Consider using a more robust method with luxon to handle date calculations.

- gte: new Date(new Date().setMonth(new Date().getMonth() - 1)),
+ gte: DateTime.now().minus({ months: 1 }).toJSDate(),
Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export const getUserWithAtLeastOneDocumentPerMonth = async () => {
return await prisma.user.count({
where: {
Document: {
some: {
createdAt: {
gte: new Date(new Date().setMonth(new Date().getMonth() - 1)),
},
},
},
},
});
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,
},
createdAt: {
gte: new Date(new Date().setMonth(new Date().getMonth() - 1)),
},
},
},
},
});
};
ephraimduncan marked this conversation as resolved.
Show resolved Hide resolved

export type GetUserWithDocumentMonthlyGrowth = Array<{
month: string;
count: number;
cume_count: number;
signed_count: number;
cume_signed_count: number;
}>;

type GetUserWithDocumentMonthlyGrowthQueryResult = Array<{
month: Date;
count: bigint;
cume_count: bigint;
signed_count: bigint;
cume_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",
SUM(COUNT(DISTINCT "Document"."userId")) OVER (ORDER BY DATE_TRUNC('month', "Document"."createdAt")) as "cume_count",
COUNT(DISTINCT CASE WHEN "Document"."status" = 'COMPLETED' THEN "Document"."userId" END) as "signed_count",
SUM(COUNT(DISTINCT CASE WHEN "Document"."status" = 'COMPLETED' THEN "Document"."userId" END)) OVER (ORDER BY DATE_TRUNC('month', "Document"."createdAt")) as "cume_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),
cume_count: Number(row.cume_count),
signed_count: Number(row.signed_count),
cume_signed_count: Number(row.cume_signed_count),
}));
};
4 changes: 4 additions & 0 deletions packages/ui/styles/theme.css
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@
--radius: 0.5rem;

--warning: 54 96% 45%;

--gold: 47.9 95.8% 53.1%;
}

.dark {
Expand Down Expand Up @@ -83,6 +85,8 @@
--radius: 0.5rem;

--warning: 54 96% 45%;

--gold: 47.9 95.8% 53.1%;
}
}

Expand Down
Loading