feat: add chat to admin dashboard

This commit is contained in:
Ephraim Atta-Duncan
2024-04-05 17:49:32 +00:00
parent cbe6270494
commit 7615c9d2fa
5 changed files with 264 additions and 6 deletions

View File

@ -14,18 +14,35 @@ 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';
import { UserWithDocumentCummulativeChart } from './user-with-document-cummulative';
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 +60,30 @@ 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"> {/* 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-2 gap-4">
<CardMetric
icon={File}
title="Users with document in the last month"
value={userWithAtLeastOneDocumentPerMonth}
/>
<CardMetric
icon={File}
title="Users with signed document in the last month"
value={userWithAtLeastOneDocumentSignedPerMonth}
/>
</div>
</div>
<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-2 gap-4">
<CardMetric icon={File} title="Total Documents" value={docStats.ALL} /> {/* <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 +93,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-2 gap-4">
<CardMetric <CardMetric
icon={UserSquare2} icon={UserSquare2}
title="Total Recipients" title="Total Recipients"
@ -70,6 +105,20 @@ export default async function AdminStatsPage() {
</div> </div>
</div> </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"
/>
</div>
</div> </div>
); );
} }

View File

@ -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()
.map(({ month, cume_count: count, cume_signed_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%)' }}
/>
<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>
);
};

View File

@ -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%)' }}
/>
<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>
);
};

View File

@ -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,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)),
},
},
},
},
});
};
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)),
},
},
},
},
});
};
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),
}));
};

View File

@ -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%;
} }
} }