Merge branch 'main' into feat/public-profiles

This commit is contained in:
Lucas Smith
2024-06-27 12:10:45 +10:00
committed by GitHub
26 changed files with 463 additions and 130 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

@ -8,7 +8,7 @@ import { DOCUMENSO_ENCRYPTION_KEY } from '@documenso/lib/constants/crypto';
import { getRequiredServerComponentSession } from '@documenso/lib/next-auth/get-server-component-session';
import { getDocumentById } from '@documenso/lib/server-only/document/get-document-by-id';
import { getServerComponentFlag } from '@documenso/lib/server-only/feature-flags/get-server-component-feature-flag';
import { getCompletedFieldsForDocument } from '@documenso/lib/server-only/field/get-completed-fields-for-document';
import { getFieldsForDocument } from '@documenso/lib/server-only/field/get-fields-for-document';
import { getRecipientsForDocument } from '@documenso/lib/server-only/recipient/get-recipients-for-document';
import { symmetricDecrypt } from '@documenso/lib/universal/crypto';
import { formatDocumentsPath } from '@documenso/lib/utils/teams';
@ -86,14 +86,15 @@ export const DocumentPageView = async ({ params, team }: DocumentPageViewProps)
documentMeta.password = securePassword;
}
const [recipients, completedFields] = await Promise.all([
const [recipients, fields] = await Promise.all([
getRecipientsForDocument({
documentId,
teamId: team?.id,
userId: user.id,
}),
getCompletedFieldsForDocument({
getFieldsForDocument({
documentId,
userId: user.id,
}),
]);
@ -163,10 +164,7 @@ export const DocumentPageView = async ({ params, team }: DocumentPageViewProps)
</Card>
{document.status === DocumentStatus.PENDING && (
<DocumentReadOnlyFields
fields={completedFields}
documentMeta={document.documentMeta || undefined}
/>
<DocumentReadOnlyFields fields={fields} documentMeta={documentMeta || undefined} />
)}
<div className="col-span-12 lg:col-span-6 xl:col-span-5">

View File

@ -383,7 +383,7 @@ export const TemplateDirectLinkDialog = ({
</div>
</div>
<DialogFooter className='mt-4'>
<DialogFooter className="mt-4">
<Button
type="button"
variant="destructive"

View File

@ -10,6 +10,7 @@ import { getCompletedFieldsForToken } from '@documenso/lib/server-only/field/get
import { getFieldsForToken } from '@documenso/lib/server-only/field/get-fields-for-token';
import { getRecipientByToken } from '@documenso/lib/server-only/recipient/get-recipient-by-token';
import { getRecipientSignatures } from '@documenso/lib/server-only/recipient/get-recipient-signatures';
import { getUserByEmail } from '@documenso/lib/server-only/user/get-user-by-email';
import { symmetricDecrypt } from '@documenso/lib/universal/crypto';
import { extractNextHeaderRequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
import { extractDocumentAuthMethods } from '@documenso/lib/utils/document-auth';
@ -70,8 +71,14 @@ export default async function SigningPage({ params: { token } }: SigningPageProp
userId: user?.id,
});
let recipientHasAccount: boolean | null = null;
if (!isDocumentAccessValid) {
return <SigningAuthPageView email={recipient.email} />;
recipientHasAccount = await getUserByEmail({ email: recipient?.email })
.then((user) => !!user)
.catch(() => false);
return <SigningAuthPageView email={recipient.email} emailHasAccount={!!recipientHasAccount} />;
}
await viewedDocument({

View File

@ -11,9 +11,10 @@ import { useToast } from '@documenso/ui/primitives/use-toast';
export type SigningAuthPageViewProps = {
email: string;
emailHasAccount?: boolean;
};
export const SigningAuthPageView = ({ email }: SigningAuthPageViewProps) => {
export const SigningAuthPageView = ({ email, emailHasAccount }: SigningAuthPageViewProps) => {
const { toast } = useToast();
const [isSigningOut, setIsSigningOut] = useState(false);
@ -30,7 +31,9 @@ export const SigningAuthPageView = ({ email }: SigningAuthPageViewProps) => {
});
await signOut({
callbackUrl: `/signin?email=${encodeURIComponent(encryptedEmail)}`,
callbackUrl: emailHasAccount
? `/signin?email=${encodeURIComponent(encryptedEmail)}`
: `/signup?email=${encodeURIComponent(encryptedEmail)}`,
});
} catch {
toast({
@ -59,7 +62,7 @@ export const SigningAuthPageView = ({ email }: SigningAuthPageViewProps) => {
onClick={async () => handleChangeAccount(email)}
loading={isSigningOut}
>
Login
{emailHasAccount ? 'Login' : 'Sign up'}
</Button>
</div>
</div>