mirror of
https://github.com/documenso/documenso.git
synced 2025-11-18 02:32:00 +10:00
Merge branch 'feat/refresh' into feat/universal-upload
This commit is contained in:
30
apps/web/src/app/(dashboard)/admin/layout.tsx
Normal file
30
apps/web/src/app/(dashboard)/admin/layout.tsx
Normal file
@ -0,0 +1,30 @@
|
||||
import React from 'react';
|
||||
|
||||
import { redirect } from 'next/navigation';
|
||||
|
||||
import { getRequiredServerComponentSession } from '@documenso/lib/next-auth/get-server-session';
|
||||
import { isAdmin } from '@documenso/lib/next-auth/guards/is-admin';
|
||||
|
||||
import { AdminNav } from './nav';
|
||||
|
||||
export type AdminSectionLayoutProps = {
|
||||
children: React.ReactNode;
|
||||
};
|
||||
|
||||
export default async function AdminSectionLayout({ children }: AdminSectionLayoutProps) {
|
||||
const user = await getRequiredServerComponentSession();
|
||||
|
||||
if (!isAdmin(user)) {
|
||||
redirect('/documents');
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mx-auto mt-16 w-full max-w-screen-xl px-4 md:px-8">
|
||||
<div className="grid grid-cols-12 gap-x-8 md:mt-8">
|
||||
<AdminNav className="col-span-12 md:col-span-3 md:flex" />
|
||||
|
||||
<div className="col-span-12 mt-12 md:col-span-9 md:mt-0">{children}</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
47
apps/web/src/app/(dashboard)/admin/nav.tsx
Normal file
47
apps/web/src/app/(dashboard)/admin/nav.tsx
Normal file
@ -0,0 +1,47 @@
|
||||
'use client';
|
||||
|
||||
import { HTMLAttributes } from 'react';
|
||||
|
||||
import Link from 'next/link';
|
||||
import { usePathname } from 'next/navigation';
|
||||
|
||||
import { BarChart3, User2 } from 'lucide-react';
|
||||
|
||||
import { cn } from '@documenso/ui/lib/utils';
|
||||
import { Button } from '@documenso/ui/primitives/button';
|
||||
|
||||
export type AdminNavProps = HTMLAttributes<HTMLDivElement>;
|
||||
|
||||
export const AdminNav = ({ className, ...props }: AdminNavProps) => {
|
||||
const pathname = usePathname();
|
||||
|
||||
return (
|
||||
<div className={cn('flex gap-x-2.5 gap-y-2 md:flex-col', className)} {...props}>
|
||||
<Button
|
||||
variant="ghost"
|
||||
className={cn(
|
||||
'justify-start md:w-full',
|
||||
pathname?.startsWith('/admin/stats') && 'bg-secondary',
|
||||
)}
|
||||
asChild
|
||||
>
|
||||
<Link href="/admin/stats">
|
||||
<BarChart3 className="mr-2 h-5 w-5" />
|
||||
Stats
|
||||
</Link>
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
variant="ghost"
|
||||
className={cn(
|
||||
'justify-start md:w-full',
|
||||
pathname?.startsWith('/admin/users') && 'bg-secondary',
|
||||
)}
|
||||
disabled
|
||||
>
|
||||
<User2 className="mr-2 h-5 w-5" />
|
||||
Users (Coming Soon)
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
5
apps/web/src/app/(dashboard)/admin/page.tsx
Normal file
5
apps/web/src/app/(dashboard)/admin/page.tsx
Normal file
@ -0,0 +1,5 @@
|
||||
import { redirect } from 'next/navigation';
|
||||
|
||||
export default function Admin() {
|
||||
redirect('/admin/stats');
|
||||
}
|
||||
75
apps/web/src/app/(dashboard)/admin/stats/page.tsx
Normal file
75
apps/web/src/app/(dashboard)/admin/stats/page.tsx
Normal file
@ -0,0 +1,75 @@
|
||||
import {
|
||||
File,
|
||||
FileCheck,
|
||||
FileClock,
|
||||
FileEdit,
|
||||
Mail,
|
||||
MailOpen,
|
||||
PenTool,
|
||||
User as UserIcon,
|
||||
UserPlus2,
|
||||
UserSquare2,
|
||||
} from 'lucide-react';
|
||||
|
||||
import { getDocumentStats } from '@documenso/lib/server-only/admin/get-documents-stats';
|
||||
import { getRecipientsStats } from '@documenso/lib/server-only/admin/get-recipients-stats';
|
||||
import {
|
||||
getUsersCount,
|
||||
getUsersWithSubscriptionsCount,
|
||||
} from '@documenso/lib/server-only/admin/get-users-stats';
|
||||
|
||||
import { CardMetric } from '~/components/(dashboard)/metric-card/metric-card';
|
||||
|
||||
export default async function AdminStatsPage() {
|
||||
const [usersCount, usersWithSubscriptionsCount, docStats, recipientStats] = await Promise.all([
|
||||
getUsersCount(),
|
||||
getUsersWithSubscriptionsCount(),
|
||||
getDocumentStats(),
|
||||
getRecipientsStats(),
|
||||
]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h2 className="text-4xl font-semibold">Instance Stats</h2>
|
||||
|
||||
<div className="mt-8 grid flex-1 grid-cols-1 gap-4 md:grid-cols-4">
|
||||
<CardMetric icon={UserIcon} title="Total Users" value={usersCount} />
|
||||
<CardMetric icon={File} title="Total Documents" value={docStats.ALL} />
|
||||
<CardMetric
|
||||
icon={UserPlus2}
|
||||
title="Active Subscriptions"
|
||||
value={usersWithSubscriptionsCount}
|
||||
/>
|
||||
<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-2">
|
||||
<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} />
|
||||
<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} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 className="text-3xl font-semibold">Recipients metrics</h3>
|
||||
|
||||
<div className="mt-8 grid flex-1 grid-cols-2 gap-4">
|
||||
<CardMetric
|
||||
icon={UserSquare2}
|
||||
title="Total Recipients"
|
||||
value={recipientStats.TOTAL_RECIPIENTS}
|
||||
/>
|
||||
<CardMetric icon={Mail} title="Documents Received" value={recipientStats.SENT} />
|
||||
<CardMetric icon={MailOpen} title="Documents Viewed" value={recipientStats.OPENED} />
|
||||
<CardMetric icon={PenTool} title="Signatures Collected" value={recipientStats.SIGNED} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -21,16 +21,18 @@ export default async function BillingSettingsPage() {
|
||||
redirect('/settings/profile');
|
||||
}
|
||||
|
||||
let subscription = await getSubscriptionByUserId({ userId: user.id });
|
||||
const subscription = await getSubscriptionByUserId({ userId: user.id }).then(async (sub) => {
|
||||
if (sub) {
|
||||
return sub;
|
||||
}
|
||||
|
||||
// If we don't have a customer record, create one as well as an empty subscription.
|
||||
if (!subscription?.customerId) {
|
||||
subscription = await createCustomer({ user });
|
||||
}
|
||||
// If we don't have a customer record, create one as well as an empty subscription.
|
||||
return createCustomer({ user });
|
||||
});
|
||||
|
||||
let billingPortalUrl = '';
|
||||
|
||||
if (subscription?.customerId) {
|
||||
if (subscription.customerId) {
|
||||
billingPortalUrl = await getPortalSession({
|
||||
customerId: subscription.customerId,
|
||||
returnUrl: `${process.env.NEXT_PUBLIC_SITE_URL}/settings/billing`,
|
||||
|
||||
Reference in New Issue
Block a user