diff --git a/apps/web/src/app/(dashboard)/documents/data-table.tsx b/apps/web/src/app/(dashboard)/documents/data-table.tsx
index 35fdfb4b1..012038f4e 100644
--- a/apps/web/src/app/(dashboard)/documents/data-table.tsx
+++ b/apps/web/src/app/(dashboard)/documents/data-table.tsx
@@ -4,13 +4,32 @@ import { useTransition } from 'react';
import Link from 'next/link';
-import { Loader } from 'lucide-react';
+import {
+ Copy,
+ Download,
+ Edit,
+ History,
+ Loader,
+ MoreHorizontal,
+ Pencil,
+ Share,
+ Trash2,
+ XCircle,
+} from 'lucide-react';
import { useUpdateSearchParams } from '@documenso/lib/client-only/hooks/use-update-search-params';
import { FindResultSet } from '@documenso/lib/types/find-result-set';
import { DocumentWithReciepient } from '@documenso/prisma/types/document-with-recipient';
+import { Button } from '@documenso/ui/primitives/button';
import { DataTable } from '@documenso/ui/primitives/data-table';
import { DataTablePagination } from '@documenso/ui/primitives/data-table-pagination';
+import {
+ DropdownMenu,
+ DropdownMenuContent,
+ DropdownMenuItem,
+ DropdownMenuLabel,
+ DropdownMenuTrigger,
+} from '@documenso/ui/primitives/dropdown-menu';
import { StackAvatarsWithTooltip } from '~/components/(dashboard)/avatar/stack-avatars-with-tooltip';
import { DocumentStatus } from '~/components/formatter/document-status';
@@ -67,6 +86,57 @@ export const DocumentsDataTable = ({ results }: DocumentsDataTableProps) => {
accessorKey: 'created',
cell: ({ row }) => ,
},
+ {
+ header: 'Actions',
+ cell: ({ row: _row }) => (
+
+
+
+
+
+
+
+
+ Action
+
+
+ Sign
+
+
+
+ Edit
+
+
+
+ Download
+
+
+
+ Duplicate
+
+
+
+ Void
+
+
+
+ Delete
+
+
+ Share
+
+
+ Resend
+
+
+
+ Share
+
+
+
+
+ ),
+ },
]}
data={results.data}
perPage={results.perPage}
diff --git a/apps/web/src/app/(dashboard)/documents/page.tsx b/apps/web/src/app/(dashboard)/documents/page.tsx
index 76675f573..4ea55936b 100644
--- a/apps/web/src/app/(dashboard)/documents/page.tsx
+++ b/apps/web/src/app/(dashboard)/documents/page.tsx
@@ -3,8 +3,8 @@ import Link from 'next/link';
import { getRequiredServerComponentSession } from '@documenso/lib/next-auth/get-server-session';
import { findDocuments } from '@documenso/lib/server-only/document/find-documents';
import { getStats } from '@documenso/lib/server-only/document/get-stats';
-import { isDocumentStatus } from '@documenso/lib/types/is-document-status';
-import { DocumentStatus as InternalDocumentStatus } from '@documenso/prisma/client';
+import { isExtendedDocumentStatus } from '@documenso/prisma/guards/is-extended-document-status';
+import { ExtendedDocumentStatus } from '@documenso/prisma/types/extended-document-status';
import { Tabs, TabsList, TabsTrigger } from '@documenso/ui/primitives/tabs';
import { PeriodSelector } from '~/components/(dashboard)/period-selector/period-selector';
@@ -16,7 +16,7 @@ import { DocumentsDataTable } from './data-table';
export type DocumentsPageProps = {
searchParams?: {
- status?: InternalDocumentStatus | 'ALL';
+ status?: ExtendedDocumentStatus;
period?: PeriodSelectorValue;
page?: string;
perPage?: string;
@@ -24,22 +24,20 @@ export type DocumentsPageProps = {
};
export default async function DocumentsPage({ searchParams = {} }: DocumentsPageProps) {
- const session = await getRequiredServerComponentSession();
+ const user = await getRequiredServerComponentSession();
const stats = await getStats({
- userId: session.id,
+ user,
});
- const status = isDocumentStatus(searchParams.status) ? searchParams.status : 'ALL';
+ const status = isExtendedDocumentStatus(searchParams.status) ? searchParams.status : 'ALL';
// const period = isPeriodSelectorValue(searchParams.period) ? searchParams.period : '';
const page = Number(searchParams.page) || 1;
const perPage = Number(searchParams.perPage) || 20;
- const shouldDefaultToPending = status === 'ALL' && stats.PENDING > 0;
-
const results = await findDocuments({
- userId: session.id,
- status: status === 'ALL' ? undefined : status,
+ userId: user.id,
+ status,
orderBy: {
column: 'created',
direction: 'desc',
@@ -57,10 +55,6 @@ export default async function DocumentsPage({ searchParams = {} }: DocumentsPage
params.delete('page');
}
- if (value === 'ALL') {
- params.delete('status');
- }
-
return `/documents?${params.toString()}`;
};
@@ -70,47 +64,28 @@ export default async function DocumentsPage({ searchParams = {} }: DocumentsPage
Documents
-
-
+
+
-
-
-
+ {[
+ ExtendedDocumentStatus.INBOX,
+ ExtendedDocumentStatus.PENDING,
+ ExtendedDocumentStatus.COMPLETED,
+ ExtendedDocumentStatus.DRAFT,
+ ExtendedDocumentStatus.ALL,
+ ].map((value) => (
+
+
+
-
- {Math.min(stats.PENDING, 99)}
-
-
-
-
-
-
-
-
-
- {Math.min(stats.COMPLETED, 99)}
-
-
-
-
-
-
-
-
-
- {Math.min(stats.DRAFT, 99)}
-
-
-
-
-
-
- All
-
-
+ {value !== ExtendedDocumentStatus.ALL && (
+
+ {Math.min(stats[value], 99)}
+
+ )}
+
+
+ ))}
diff --git a/apps/web/src/components/formatter/document-status.tsx b/apps/web/src/components/formatter/document-status.tsx
index 4e1ccf742..126a52f4f 100644
--- a/apps/web/src/components/formatter/document-status.tsx
+++ b/apps/web/src/components/formatter/document-status.tsx
@@ -3,16 +3,17 @@ import { HTMLAttributes } from 'react';
import { CheckCircle2, Clock, File } from 'lucide-react';
import type { LucideIcon } from 'lucide-react/dist/lucide-react';
-import { DocumentStatus as InternalDocumentStatus } from '@documenso/prisma/client';
+import { ExtendedDocumentStatus } from '@documenso/prisma/types/extended-document-status';
+import { SignatureIcon } from '@documenso/ui/icons/signature';
import { cn } from '@documenso/ui/lib/utils';
type FriendlyStatus = {
label: string;
- icon: LucideIcon;
+ icon?: LucideIcon;
color: string;
};
-const FRIENDLY_STATUS_MAP: Record
= {
+const FRIENDLY_STATUS_MAP: Record = {
PENDING: {
label: 'Pending',
icon: Clock,
@@ -28,10 +29,19 @@ const FRIENDLY_STATUS_MAP: Record = {
icon: File,
color: 'text-yellow-500',
},
+ INBOX: {
+ label: 'Inbox',
+ icon: SignatureIcon,
+ color: 'text-muted-foreground',
+ },
+ ALL: {
+ label: 'All',
+ color: 'text-muted-foreground',
+ },
};
export type DocumentStatusProps = HTMLAttributes & {
- status: InternalDocumentStatus;
+ status: ExtendedDocumentStatus;
inheritColor?: boolean;
};
@@ -45,11 +55,13 @@ export const DocumentStatus = ({
return (
-
+ {Icon && (
+
+ )}
{label}
);
diff --git a/packages/lib/server-only/document/find-documents.ts b/packages/lib/server-only/document/find-documents.ts
index 4b209f611..93b1e2089 100644
--- a/packages/lib/server-only/document/find-documents.ts
+++ b/packages/lib/server-only/document/find-documents.ts
@@ -1,14 +1,17 @@
+import { match } from 'ts-pattern';
+
import { prisma } from '@documenso/prisma';
-import { Document, DocumentStatus, Prisma, SigningStatus } from '@documenso/prisma/client';
+import { Document, Prisma, SigningStatus } from '@documenso/prisma/client';
import { DocumentWithRecipientAndSender } from '@documenso/prisma/types/document';
import { DocumentWithReciepient } from '@documenso/prisma/types/document-with-recipient';
+import { ExtendedDocumentStatus } from '@documenso/prisma/types/extended-document-status';
import { FindResultSet } from '../../types/find-result-set';
export interface FindDocumentsOptions {
userId: number;
term?: string;
- status?: DocumentStatus;
+ status?: ExtendedDocumentStatus;
page?: number;
perPage?: number;
orderBy?: {
@@ -20,29 +23,102 @@ export interface FindDocumentsOptions {
export const findDocuments = async ({
userId,
term,
- status,
+ status = ExtendedDocumentStatus.ALL,
page = 1,
perPage = 10,
orderBy,
}: FindDocumentsOptions): Promise> => {
+ const user = await prisma.user.findFirstOrThrow({
+ where: {
+ id: userId,
+ },
+ });
+
const orderByColumn = orderBy?.column ?? 'created';
const orderByDirection = orderBy?.direction ?? 'desc';
- const filters: Prisma.DocumentWhereInput = {
- status,
- userId,
- };
+ const termFilters = !term
+ ? undefined
+ : ({
+ title: {
+ contains: term,
+ mode: 'insensitive',
+ },
+ } as const);
- if (term) {
- filters.title = {
- contains: term,
- mode: 'insensitive',
- };
- }
+ const filters = match(status)
+ .with(ExtendedDocumentStatus.ALL, () => ({
+ OR: [
+ {
+ userId,
+ },
+ {
+ status: {
+ not: ExtendedDocumentStatus.DRAFT,
+ },
+ Recipient: {
+ some: {
+ email: user.email,
+ },
+ },
+ },
+ ],
+ }))
+ .with(ExtendedDocumentStatus.INBOX, () => ({
+ status: {
+ not: ExtendedDocumentStatus.DRAFT,
+ },
+ Recipient: {
+ some: {
+ email: user.email,
+ signingStatus: SigningStatus.NOT_SIGNED,
+ },
+ },
+ }))
+ .with(ExtendedDocumentStatus.DRAFT, () => ({
+ userId,
+ status: ExtendedDocumentStatus.DRAFT,
+ }))
+ .with(ExtendedDocumentStatus.PENDING, () => ({
+ OR: [
+ {
+ userId,
+ status: ExtendedDocumentStatus.PENDING,
+ },
+ {
+ status: ExtendedDocumentStatus.PENDING,
+
+ Recipient: {
+ some: {
+ email: user.email,
+ signingStatus: SigningStatus.SIGNED,
+ },
+ },
+ },
+ ],
+ }))
+ .with(ExtendedDocumentStatus.COMPLETED, () => ({
+ OR: [
+ {
+ userId,
+ status: ExtendedDocumentStatus.COMPLETED,
+ },
+ {
+ status: ExtendedDocumentStatus.COMPLETED,
+ Recipient: {
+ some: {
+ email: user.email,
+ },
+ },
+ },
+ ],
+ }))
+ .exhaustive();
const [data, count] = await Promise.all([
prisma.document.findMany({
where: {
+ ...termFilters,
...filters,
},
skip: Math.max(page - 1, 0) * perPage,
@@ -51,11 +127,19 @@ export const findDocuments = async ({
[orderByColumn]: orderByDirection,
},
include: {
+ User: {
+ select: {
+ id: true,
+ name: true,
+ email: true,
+ },
+ },
Recipient: true,
},
}),
prisma.document.count({
where: {
+ ...termFilters,
...filters,
},
}),
diff --git a/packages/lib/server-only/document/get-stats.ts b/packages/lib/server-only/document/get-stats.ts
index 25754d7bc..6e875f9be 100644
--- a/packages/lib/server-only/document/get-stats.ts
+++ b/packages/lib/server-only/document/get-stats.ts
@@ -1,30 +1,88 @@
import { prisma } from '@documenso/prisma';
-import { DocumentStatus } from '@documenso/prisma/client';
+import { SigningStatus, User } from '@documenso/prisma/client';
+import { isExtendedDocumentStatus } from '@documenso/prisma/guards/is-extended-document-status';
+import { ExtendedDocumentStatus } from '@documenso/prisma/types/extended-document-status';
export type GetStatsInput = {
- userId: number;
+ user: User;
};
-export const getStats = async ({ userId }: GetStatsInput) => {
- const result = await prisma.document.groupBy({
- by: ['status'],
- _count: {
- _all: true,
- },
- where: {
- userId,
- },
- });
+export const getStats = async ({ user }: GetStatsInput) => {
+ const [ownerCounts, notSignedCounts, hasSignedCounts] = await Promise.all([
+ prisma.document.groupBy({
+ by: ['status'],
+ _count: {
+ _all: true,
+ },
+ where: {
+ userId: user.id,
+ },
+ }),
+ prisma.document.groupBy({
+ by: ['status'],
+ _count: {
+ _all: true,
+ },
+ where: {
+ status: ExtendedDocumentStatus.PENDING,
+ Recipient: {
+ some: {
+ email: user.email,
+ signingStatus: SigningStatus.NOT_SIGNED,
+ },
+ },
+ },
+ }),
+ prisma.document.groupBy({
+ by: ['status'],
+ _count: {
+ _all: true,
+ },
+ where: {
+ status: {
+ not: ExtendedDocumentStatus.DRAFT,
+ },
+ Recipient: {
+ some: {
+ email: user.email,
+ signingStatus: SigningStatus.SIGNED,
+ },
+ },
+ },
+ }),
+ ]);
- const stats: Record = {
- [DocumentStatus.DRAFT]: 0,
- [DocumentStatus.PENDING]: 0,
- [DocumentStatus.COMPLETED]: 0,
+ const stats: Record = {
+ [ExtendedDocumentStatus.DRAFT]: 0,
+ [ExtendedDocumentStatus.PENDING]: 0,
+ [ExtendedDocumentStatus.COMPLETED]: 0,
+ [ExtendedDocumentStatus.INBOX]: 0,
+ [ExtendedDocumentStatus.ALL]: 0,
};
- result.forEach((stat) => {
+ ownerCounts.forEach((stat) => {
stats[stat.status] = stat._count._all;
});
+ notSignedCounts.forEach((stat) => {
+ stats[ExtendedDocumentStatus.INBOX] += stat._count._all;
+ });
+
+ hasSignedCounts.forEach((stat) => {
+ if (stat.status === ExtendedDocumentStatus.COMPLETED) {
+ stats[ExtendedDocumentStatus.COMPLETED] += stat._count._all;
+ }
+
+ if (stat.status === ExtendedDocumentStatus.PENDING) {
+ stats[ExtendedDocumentStatus.PENDING] += stat._count._all;
+ }
+ });
+
+ Object.keys(stats).forEach((key) => {
+ if (key !== ExtendedDocumentStatus.ALL && isExtendedDocumentStatus(key)) {
+ stats[ExtendedDocumentStatus.ALL] += stats[key];
+ }
+ });
+
return stats;
};
diff --git a/packages/prisma/guards/is-extended-document-status.ts b/packages/prisma/guards/is-extended-document-status.ts
new file mode 100644
index 000000000..43be73f64
--- /dev/null
+++ b/packages/prisma/guards/is-extended-document-status.ts
@@ -0,0 +1,11 @@
+import { ExtendedDocumentStatus } from '../types/extended-document-status';
+
+export const isExtendedDocumentStatus = (value: unknown): value is ExtendedDocumentStatus => {
+ if (typeof value !== 'string') {
+ return false;
+ }
+
+ // We're using the assertion for a type-guard so it's safe to ignore the eslint warning
+ // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
+ return Object.values(ExtendedDocumentStatus).includes(value as ExtendedDocumentStatus);
+};
diff --git a/packages/prisma/types/extended-document-status.ts b/packages/prisma/types/extended-document-status.ts
new file mode 100644
index 000000000..a3576750d
--- /dev/null
+++ b/packages/prisma/types/extended-document-status.ts
@@ -0,0 +1,10 @@
+import { DocumentStatus } from '@prisma/client';
+
+export const ExtendedDocumentStatus = {
+ ...DocumentStatus,
+ INBOX: 'INBOX',
+ ALL: 'ALL',
+} as const;
+
+export type ExtendedDocumentStatus =
+ (typeof ExtendedDocumentStatus)[keyof typeof ExtendedDocumentStatus];
diff --git a/packages/ui/icons/signature.tsx b/packages/ui/icons/signature.tsx
new file mode 100644
index 000000000..b91998bb5
--- /dev/null
+++ b/packages/ui/icons/signature.tsx
@@ -0,0 +1,28 @@
+import type { LucideIcon } from 'lucide-react/dist/lucide-react';
+
+export const SignatureIcon: LucideIcon = ({
+ size = 24,
+ color = 'currentColor',
+ strokeWidth = 1.33,
+ absoluteStrokeWidth,
+ ...props
+}) => {
+ return (
+
+ );
+};