@@ -58,7 +73,7 @@ export default async function AdminStatsPage() {
);
}
diff --git a/apps/web/src/app/(dashboard)/admin/stats/user-with-document.tsx b/apps/web/src/app/(dashboard)/admin/stats/user-with-document.tsx
new file mode 100644
index 000000000..cf9f11e23
--- /dev/null
+++ b/apps/web/src/app/(dashboard)/admin/stats/user-with-document.tsx
@@ -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
& { tooltip?: string }) => {
+ if (active && payload && payload.length) {
+ return (
+
+
{label}
+
+ {`${tooltip} : `}
+ {payload[0].value}
+
+
+ );
+ }
+
+ 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 (
+
+
+
+
{title}
+
+
+
+
+
+
+
+ }
+ labelStyle={{
+ color: 'hsl(var(--primary-foreground))',
+ }}
+ cursor={{ fill: 'hsl(var(--primary) / 10%)' }}
+ />
+
+
+
+
+
+
+ );
+};
diff --git a/packages/lib/server-only/admin/get-users-stats.ts b/packages/lib/server-only/admin/get-users-stats.ts
index 09892171a..0f4a2f0b4 100644
--- a/packages/lib/server-only/admin/get-users-stats.ts
+++ b/packages/lib/server-only/admin/get-users-stats.ts
@@ -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`
+ 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),
+ }));
+};
diff --git a/packages/ui/styles/theme.css b/packages/ui/styles/theme.css
index fa9231e5d..0b5bc587f 100644
--- a/packages/ui/styles/theme.css
+++ b/packages/ui/styles/theme.css
@@ -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%;
}
}