diff --git a/apps/web/src/app/(dashboard)/documents/[id]/document-page-view-information.tsx b/apps/web/src/app/(dashboard)/documents/[id]/document-page-view-information.tsx index af0bc8644..24a85bacc 100644 --- a/apps/web/src/app/(dashboard)/documents/[id]/document-page-view-information.tsx +++ b/apps/web/src/app/(dashboard)/documents/[id]/document-page-view-information.tsx @@ -21,6 +21,7 @@ export const DocumentPageViewInformation = ({ userId, }: DocumentPageViewInformationProps) => { const isMounted = useIsMounted(); + const { locale } = useLocale(); const documentInformation = useMemo(() => { diff --git a/apps/web/src/app/(dashboard)/documents/[id]/logs/document-logs-data-table.tsx b/apps/web/src/app/(dashboard)/documents/[id]/logs/document-logs-data-table.tsx new file mode 100644 index 000000000..bdfdc8658 --- /dev/null +++ b/apps/web/src/app/(dashboard)/documents/[id]/logs/document-logs-data-table.tsx @@ -0,0 +1,165 @@ +'use client'; + +import { useSearchParams } from 'next/navigation'; + +import { DateTime } from 'luxon'; +import type { DateTimeFormatOptions } from 'luxon'; +import { UAParser } from 'ua-parser-js'; + +import { useUpdateSearchParams } from '@documenso/lib/client-only/hooks/use-update-search-params'; +import { ZBaseTableSearchParamsSchema } from '@documenso/lib/types/search-params'; +import { formatDocumentAuditLogAction } from '@documenso/lib/utils/document-audit-logs'; +import { trpc } from '@documenso/trpc/react'; +import { DataTable } from '@documenso/ui/primitives/data-table'; +import { DataTablePagination } from '@documenso/ui/primitives/data-table-pagination'; +import { Skeleton } from '@documenso/ui/primitives/skeleton'; +import { TableCell } from '@documenso/ui/primitives/table'; + +import { LocaleDate } from '~/components/formatter/locale-date'; + +export type DocumentLogsDataTableProps = { + documentId: number; +}; + +const dateFormat: DateTimeFormatOptions = { + ...DateTime.DATETIME_SHORT, + hourCycle: 'h12', +}; + +export const DocumentLogsDataTable = ({ documentId }: DocumentLogsDataTableProps) => { + const parser = new UAParser(); + + const searchParams = useSearchParams(); + const updateSearchParams = useUpdateSearchParams(); + + const parsedSearchParams = ZBaseTableSearchParamsSchema.parse( + Object.fromEntries(searchParams ?? []), + ); + + const { data, isLoading, isInitialLoading, isLoadingError } = + trpc.document.findDocumentAuditLogs.useQuery( + { + documentId, + page: parsedSearchParams.page, + perPage: parsedSearchParams.perPage, + }, + { + keepPreviousData: true, + }, + ); + + const onPaginationChange = (page: number, perPage: number) => { + updateSearchParams({ + page, + perPage, + }); + }; + + const uppercaseFistLetter = (text: string) => { + return text.charAt(0).toUpperCase() + text.slice(1); + }; + + const results = data ?? { + data: [], + perPage: 10, + currentPage: 1, + totalPages: 1, + }; + + return ( + , + }, + { + header: 'User', + accessorKey: 'name', + cell: ({ row }) => + row.original.name || row.original.email ? ( +
+ {row.original.name && ( +

+ {row.original.name} +

+ )} + + {row.original.email && ( +

+ {row.original.email} +

+ )} +
+ ) : ( +

N/A

+ ), + }, + { + header: 'Action', + accessorKey: 'type', + cell: ({ row }) => ( + + {uppercaseFistLetter(formatDocumentAuditLogAction(row.original).description)} + + ), + }, + { + header: 'IP Address', + accessorKey: 'ipAddress', + }, + { + header: 'Browser', + cell: ({ row }) => { + if (!row.original.userAgent) { + return 'N/A'; + } + + parser.setUA(row.original.userAgent); + + const result = parser.getResult(); + + return result.browser.name ?? 'N/A'; + }, + }, + ]} + data={results.data} + perPage={results.perPage} + currentPage={results.currentPage} + totalPages={results.totalPages} + onPaginationChange={onPaginationChange} + error={{ + enable: isLoadingError, + }} + skeleton={{ + enable: isLoading && isInitialLoading, + rows: 3, + component: ( + <> + + + + +
+ + +
+
+ + + + + + + + + + + ), + }} + > + {(table) => } +
+ ); +}; diff --git a/apps/web/src/app/(dashboard)/documents/[id]/logs/document-logs-page-view.tsx b/apps/web/src/app/(dashboard)/documents/[id]/logs/document-logs-page-view.tsx new file mode 100644 index 000000000..e9627d2c7 --- /dev/null +++ b/apps/web/src/app/(dashboard)/documents/[id]/logs/document-logs-page-view.tsx @@ -0,0 +1,150 @@ +import Link from 'next/link'; +import { redirect } from 'next/navigation'; + +import { ChevronLeft, DownloadIcon } from 'lucide-react'; + +import { getRequiredServerComponentSession } from '@documenso/lib/next-auth/get-server-component-session'; +import { getDocumentById } from '@documenso/lib/server-only/document/get-document-by-id'; +import { getRecipientsForDocument } from '@documenso/lib/server-only/recipient/get-recipients-for-document'; +import { formatDocumentsPath } from '@documenso/lib/utils/teams'; +import type { Recipient, Team } from '@documenso/prisma/client'; +import { Button } from '@documenso/ui/primitives/button'; +import { Card } from '@documenso/ui/primitives/card'; + +import { FRIENDLY_STATUS_MAP } from '~/components/formatter/document-status'; + +import { DocumentLogsDataTable } from './document-logs-data-table'; + +export type DocumentLogsPageViewProps = { + params: { + id: string; + }; + team?: Team; +}; + +export const DocumentLogsPageView = async ({ params, team }: DocumentLogsPageViewProps) => { + const { id } = params; + + const documentId = Number(id); + + const documentRootPath = formatDocumentsPath(team?.url); + + if (!documentId || Number.isNaN(documentId)) { + redirect(documentRootPath); + } + + const { user } = await getRequiredServerComponentSession(); + + const [document, recipients] = await Promise.all([ + getDocumentById({ + id: documentId, + userId: user.id, + teamId: team?.id, + }).catch(() => null), + getRecipientsForDocument({ + documentId, + userId: user.id, + }), + ]); + + if (!document || !document.documentData) { + redirect(documentRootPath); + } + + const documentInformation: { description: string; value: string }[] = [ + { + description: 'Document title', + value: document.title, + }, + { + description: 'Document ID', + value: document.id.toString(), + }, + { + description: 'Document status', + value: FRIENDLY_STATUS_MAP[document.status].label, + }, + { + description: 'Created by', + value: document.User.name ?? document.User.email, + }, + { + description: 'Date created', + value: document.createdAt.toISOString(), + }, + { + description: 'Last updated', + value: document.updatedAt.toISOString(), + }, + { + description: 'Time zone', + value: document.documentMeta?.timezone ?? 'N/A', + }, + ]; + + const formatRecipientText = (recipient: Recipient) => { + let text = recipient.email; + + if (recipient.name) { + text = `${recipient.name} (${recipient.email})`; + } + + return `${text} - ${recipient.role}`; + }; + + return ( +
+ + + Document + + +
+

+ {document.title} +

+ +
+ + + +
+
+ +
+ + {documentInformation.map((info, i) => ( +
+

{info.description}

+

{info.value}

+
+ ))} + +
+

Recipients

+
    + {recipients.map((recipient) => ( +
  • + {formatRecipientText(recipient)} +
  • + ))} +
+
+
+
+ +
+ +
+
+ ); +}; diff --git a/apps/web/src/app/(dashboard)/documents/[id]/logs/page.tsx b/apps/web/src/app/(dashboard)/documents/[id]/logs/page.tsx new file mode 100644 index 000000000..e21f8459b --- /dev/null +++ b/apps/web/src/app/(dashboard)/documents/[id]/logs/page.tsx @@ -0,0 +1,11 @@ +import { DocumentLogsPageView } from './document-logs-page-view'; + +export type DocumentsLogsPageProps = { + params: { + id: string; + }; +}; + +export default function DocumentsLogsPage({ params }: DocumentsLogsPageProps) { + return ; +} diff --git a/apps/web/src/app/(teams)/t/[teamUrl]/documents/[id]/logs/page.tsx b/apps/web/src/app/(teams)/t/[teamUrl]/documents/[id]/logs/page.tsx new file mode 100644 index 000000000..4f514dd56 --- /dev/null +++ b/apps/web/src/app/(teams)/t/[teamUrl]/documents/[id]/logs/page.tsx @@ -0,0 +1,20 @@ +import { getRequiredServerComponentSession } from '@documenso/lib/next-auth/get-server-component-session'; +import { getTeamByUrl } from '@documenso/lib/server-only/team/get-team'; + +import { DocumentLogsPageView } from '~/app/(dashboard)/documents/[id]/logs/document-logs-page-view'; + +export type TeamDocumentsLogsPageProps = { + params: { + id: string; + teamUrl: string; + }; +}; + +export default async function TeamsDocumentsLogsPage({ params }: TeamDocumentsLogsPageProps) { + const { teamUrl } = params; + + const { user } = await getRequiredServerComponentSession(); + const team = await getTeamByUrl({ userId: user.id, teamUrl }); + + return ; +} diff --git a/packages/trpc/server/document-router/router.ts b/packages/trpc/server/document-router/router.ts index cd9491fd6..eb833684a 100644 --- a/packages/trpc/server/document-router/router.ts +++ b/packages/trpc/server/document-router/router.ts @@ -133,9 +133,10 @@ export const documentRouter = router({ .input(ZFindDocumentAuditLogsQuerySchema) .query(async ({ input, ctx }) => { try { - const { perPage, documentId, cursor, filterForRecentActivity, orderBy } = input; + const { page, perPage, documentId, cursor, filterForRecentActivity, orderBy } = input; return await findDocumentAuditLogs({ + page, perPage, documentId, cursor,