diff --git a/apps/web/src/app/(dashboard)/admin/layout.tsx b/apps/web/src/app/(dashboard)/admin/layout.tsx index a221d92ba..a04c7b693 100644 --- a/apps/web/src/app/(dashboard)/admin/layout.tsx +++ b/apps/web/src/app/(dashboard)/admin/layout.tsx @@ -1,23 +1,19 @@ -import { redirect } from 'next/navigation'; +import React from 'react'; -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 AdminLayoutProps = { +export type AdminSectionLayoutProps = { children: React.ReactNode; }; -export default async function AdminLayout({ children }: AdminLayoutProps) { - const user = await getRequiredServerComponentSession(); - const isUserAdmin = isAdmin(user); +export default function AdminSectionLayout({ children }: AdminSectionLayoutProps) { + return ( +
+
+ - if (!user) { - redirect('/signin'); - } - - if (!isUserAdmin) { - redirect('/dashboard'); - } - - return
{children}
; +
{children}
+
+
+ ); } diff --git a/apps/web/src/app/(dashboard)/admin/nav.tsx b/apps/web/src/app/(dashboard)/admin/nav.tsx new file mode 100644 index 000000000..3b87a9b13 --- /dev/null +++ b/apps/web/src/app/(dashboard)/admin/nav.tsx @@ -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; + +export const AdminNav = ({ className, ...props }: AdminNavProps) => { + const pathname = usePathname(); + + return ( +
+ + + +
+ ); +}; diff --git a/apps/web/src/app/(dashboard)/admin/page.tsx b/apps/web/src/app/(dashboard)/admin/page.tsx index 073056478..5fe030685 100644 --- a/apps/web/src/app/(dashboard)/admin/page.tsx +++ b/apps/web/src/app/(dashboard)/admin/page.tsx @@ -1,114 +1,5 @@ -import { - Archive, - File, - FileX2, - LucideIcon, - Mail, - MailOpen, - PenTool, - Send, - User as UserIcon, - UserPlus2, - UserSquare2, -} from 'lucide-react'; +import { redirect } from 'next/navigation'; -import { getDocsCount } 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 { - ReadStatus as InternalReadStatus, - SendStatus as InternalSendStatus, - SigningStatus as InternalSigningStatus, -} from '@documenso/prisma/client'; - -import { CardMetric } from '~/components/(dashboard)/metric-card/metric-card'; - -type CardData = { - icon: LucideIcon; - title: string; - status: - | 'TOTAL_RECIPIENTS' - | 'OPENED' - | 'NOT_OPENED' - | 'SIGNED' - | 'NOT_SIGNED' - | 'SENT' - | 'NOT_SENT'; -}; - -const CARD_DATA: CardData[] = [ - { - icon: UserSquare2, - title: 'Recipients in the database', - status: 'TOTAL_RECIPIENTS', - }, - { - icon: MailOpen, - title: 'Opened documents', - status: InternalReadStatus.OPENED, - }, - { - icon: Mail, - title: 'Unopened documents', - status: InternalReadStatus.NOT_OPENED, - }, - { - icon: Send, - title: 'Sent documents', - status: InternalSendStatus.SENT, - }, - { - icon: Archive, - title: 'Unsent documents', - status: InternalSendStatus.NOT_SENT, - }, - { - icon: PenTool, - title: 'Signed documents', - status: InternalSigningStatus.SIGNED, - }, - { - icon: FileX2, - title: 'Unsigned documents', - status: InternalSigningStatus.NOT_SIGNED, - }, -]; - -export default async function Admin() { - const [usersCount, usersWithSubscriptionsCount, docsCount, recipientsStats] = await Promise.all([ - getUsersCount(), - getUsersWithSubscriptionsCount(), - getDocsCount(), - getRecipientsStats(), - ]); - - return ( -
-

Instance version: {process.env.APP_VERSION}

-
- - -
-

Document metrics

-
- -
- -

Recipients metrics

-
- {CARD_DATA.map((card) => ( -
- -
- ))} -
-
- ); +export default function Admin() { + redirect('/admin/stats'); } diff --git a/apps/web/src/app/(dashboard)/admin/stats/page.tsx b/apps/web/src/app/(dashboard)/admin/stats/page.tsx new file mode 100644 index 000000000..b93af5a03 --- /dev/null +++ b/apps/web/src/app/(dashboard)/admin/stats/page.tsx @@ -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 ( +
+

Instance Stats

+ +
+ + + + +
+ +
+
+

Document metrics

+ +
+ + + + +
+
+ +
+

Recipients metrics

+ +
+ + + + +
+
+
+
+ ); +} diff --git a/apps/web/src/components/(dashboard)/layout/profile-dropdown.tsx b/apps/web/src/components/(dashboard)/layout/profile-dropdown.tsx index e3fd4c6d6..3f7a02e60 100644 --- a/apps/web/src/components/(dashboard)/layout/profile-dropdown.tsx +++ b/apps/web/src/components/(dashboard)/layout/profile-dropdown.tsx @@ -62,6 +62,19 @@ export const ProfileDropdown = ({ user }: ProfileDropdownProps) => { Account + {isUserAdmin && ( + <> + + + + Admin + + + + + + )} + @@ -69,15 +82,6 @@ export const ProfileDropdown = ({ user }: ProfileDropdownProps) => { - {isUserAdmin && ( - - - - Admin - - - )} - diff --git a/apps/web/src/components/(dashboard)/metric-card/metric-card.tsx b/apps/web/src/components/(dashboard)/metric-card/metric-card.tsx index f59d42096..a2248ccdc 100644 --- a/apps/web/src/components/(dashboard)/metric-card/metric-card.tsx +++ b/apps/web/src/components/(dashboard)/metric-card/metric-card.tsx @@ -18,10 +18,10 @@ export const CardMetric = ({ icon: Icon, title, value, className }: CardMetricPr )} >
-
- {Icon && } +
+ {Icon && } -

{title}

+

{title}

diff --git a/packages/lib/server-only/admin/get-documents-stats.ts b/packages/lib/server-only/admin/get-documents-stats.ts index 9100a886c..e0d53373f 100644 --- a/packages/lib/server-only/admin/get-documents-stats.ts +++ b/packages/lib/server-only/admin/get-documents-stats.ts @@ -1,5 +1,26 @@ import { prisma } from '@documenso/prisma'; +import { ExtendedDocumentStatus } from '@documenso/prisma/types/extended-document-status'; -export const getDocsCount = async () => { - return await prisma.document.count(); +export const getDocumentStats = async () => { + const counts = await prisma.document.groupBy({ + by: ['status'], + _count: { + _all: true, + }, + }); + + const stats: Record, number> = { + [ExtendedDocumentStatus.DRAFT]: 0, + [ExtendedDocumentStatus.PENDING]: 0, + [ExtendedDocumentStatus.COMPLETED]: 0, + [ExtendedDocumentStatus.ALL]: 0, + }; + + counts.forEach((stat) => { + stats[stat.status] = stat._count._all; + + stats.ALL += stat._count._all; + }); + + return stats; };