mirror of
https://github.com/documenso/documenso.git
synced 2025-11-10 04:22:32 +10:00
feat: admin activation metrics (#1080)
 <!-- 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:
@ -14,18 +14,34 @@ import {
|
|||||||
import { getDocumentStats } from '@documenso/lib/server-only/admin/get-documents-stats';
|
import { getDocumentStats } from '@documenso/lib/server-only/admin/get-documents-stats';
|
||||||
import { getRecipientsStats } from '@documenso/lib/server-only/admin/get-recipients-stats';
|
import { getRecipientsStats } from '@documenso/lib/server-only/admin/get-recipients-stats';
|
||||||
import {
|
import {
|
||||||
|
getUserWithAtLeastOneDocumentPerMonth,
|
||||||
|
getUserWithAtLeastOneDocumentSignedPerMonth,
|
||||||
|
getUserWithSignedDocumentMonthlyGrowth,
|
||||||
getUsersCount,
|
getUsersCount,
|
||||||
getUsersWithSubscriptionsCount,
|
getUsersWithSubscriptionsCount,
|
||||||
} from '@documenso/lib/server-only/admin/get-users-stats';
|
} from '@documenso/lib/server-only/admin/get-users-stats';
|
||||||
|
|
||||||
import { CardMetric } from '~/components/(dashboard)/metric-card/metric-card';
|
import { CardMetric } from '~/components/(dashboard)/metric-card/metric-card';
|
||||||
|
|
||||||
|
import { UserWithDocumentChart } from './user-with-document';
|
||||||
|
|
||||||
export default async function AdminStatsPage() {
|
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(),
|
getUsersCount(),
|
||||||
getUsersWithSubscriptionsCount(),
|
getUsersWithSubscriptionsCount(),
|
||||||
getDocumentStats(),
|
getDocumentStats(),
|
||||||
getRecipientsStats(),
|
getRecipientsStats(),
|
||||||
|
getUserWithAtLeastOneDocumentPerMonth(),
|
||||||
|
getUserWithAtLeastOneDocumentSignedPerMonth(),
|
||||||
|
getUserWithSignedDocumentMonthlyGrowth(),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -43,12 +59,11 @@ export default async function AdminStatsPage() {
|
|||||||
<CardMetric icon={UserPlus2} title="App Version" value={`v${process.env.APP_VERSION}`} />
|
<CardMetric icon={UserPlus2} title="App Version" value={`v${process.env.APP_VERSION}`} />
|
||||||
</div>
|
</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>
|
<div>
|
||||||
<h3 className="text-3xl font-semibold">Document metrics</h3>
|
<h3 className="text-3xl font-semibold">Document 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={File} title="Total Documents" value={docStats.ALL} />
|
|
||||||
<CardMetric icon={FileEdit} title="Drafted Documents" value={docStats.DRAFT} />
|
<CardMetric icon={FileEdit} title="Drafted Documents" value={docStats.DRAFT} />
|
||||||
<CardMetric icon={FileClock} title="Pending Documents" value={docStats.PENDING} />
|
<CardMetric icon={FileClock} title="Pending Documents" value={docStats.PENDING} />
|
||||||
<CardMetric icon={FileCheck} title="Completed Documents" value={docStats.COMPLETED} />
|
<CardMetric icon={FileCheck} title="Completed Documents" value={docStats.COMPLETED} />
|
||||||
@ -58,7 +73,7 @@ export default async function AdminStatsPage() {
|
|||||||
<div>
|
<div>
|
||||||
<h3 className="text-3xl font-semibold">Recipients metrics</h3>
|
<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
|
<CardMetric
|
||||||
icon={UserSquare2}
|
icon={UserSquare2}
|
||||||
title="Total Recipients"
|
title="Total Recipients"
|
||||||
@ -70,6 +85,23 @@ export default async function AdminStatsPage() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -1,5 +1,7 @@
|
|||||||
|
import { DateTime } from 'luxon';
|
||||||
|
|
||||||
import { prisma } from '@documenso/prisma';
|
import { prisma } from '@documenso/prisma';
|
||||||
import { SubscriptionStatus } from '@documenso/prisma/client';
|
import { DocumentStatus, SubscriptionStatus } from '@documenso/prisma/client';
|
||||||
|
|
||||||
export const getUsersCount = async () => {
|
export const getUsersCount = async () => {
|
||||||
return await prisma.user.count();
|
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),
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|||||||
@ -44,6 +44,8 @@
|
|||||||
--radius: 0.5rem;
|
--radius: 0.5rem;
|
||||||
|
|
||||||
--warning: 54 96% 45%;
|
--warning: 54 96% 45%;
|
||||||
|
|
||||||
|
--gold: 47.9 95.8% 53.1%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark {
|
.dark {
|
||||||
@ -83,6 +85,8 @@
|
|||||||
--radius: 0.5rem;
|
--radius: 0.5rem;
|
||||||
|
|
||||||
--warning: 54 96% 45%;
|
--warning: 54 96% 45%;
|
||||||
|
|
||||||
|
--gold: 47.9 95.8% 53.1%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user