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,