diff --git a/apps/web/src/app/(dashboard)/dashboard/page.tsx b/apps/web/src/app/(dashboard)/dashboard/page.tsx index 30ee90ed9..a9d650eb6 100644 --- a/apps/web/src/app/(dashboard)/dashboard/page.tsx +++ b/apps/web/src/app/(dashboard)/dashboard/page.tsx @@ -14,6 +14,7 @@ import { TableRow, } from '@documenso/ui/primitives/table'; +import { StackAvatarsWithTooltip } from '~/components/(dashboard)/avatar/stack-avatars-with-tooltip'; import { CardMetric } from '~/components/(dashboard)/metric-card/metric-card'; import { DocumentStatus } from '~/components/formatter/document-status'; import { LocaleDate } from '~/components/formatter/locale-date'; @@ -60,30 +61,38 @@ export default async function DashboardPage() { ID Title + Reciepient Status Created - {results.data.map((document) => ( - - {document.id} - - - {document.title} - - - - - - - - - - ))} + {results.data.map((document) => { + return ( + + {document.id} + + + {document.title} + + + + + + + + + + + + + + + ); + })} {results.data.length === 0 && ( diff --git a/apps/web/src/app/(dashboard)/documents/data-table.tsx b/apps/web/src/app/(dashboard)/documents/data-table.tsx index 58b6eb1ac..35fdfb4b1 100644 --- a/apps/web/src/app/(dashboard)/documents/data-table.tsx +++ b/apps/web/src/app/(dashboard)/documents/data-table.tsx @@ -8,15 +8,16 @@ import { Loader } 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 { Document } from '@documenso/prisma/client'; +import { DocumentWithReciepient } from '@documenso/prisma/types/document-with-recipient'; import { DataTable } from '@documenso/ui/primitives/data-table'; import { DataTablePagination } from '@documenso/ui/primitives/data-table-pagination'; +import { StackAvatarsWithTooltip } from '~/components/(dashboard)/avatar/stack-avatars-with-tooltip'; import { DocumentStatus } from '~/components/formatter/document-status'; import { LocaleDate } from '~/components/formatter/locale-date'; export type DocumentsDataTableProps = { - results: FindResultSet; + results: FindResultSet; }; export const DocumentsDataTable = ({ results }: DocumentsDataTableProps) => { @@ -49,6 +50,13 @@ export const DocumentsDataTable = ({ results }: DocumentsDataTableProps) => { ), }, + { + header: 'Recipient', + accessorKey: 'recipient', + cell: ({ row }) => { + return ; + }, + }, { header: 'Status', accessorKey: 'status', diff --git a/apps/web/src/components/(dashboard)/avatar/stack-avatar.tsx b/apps/web/src/components/(dashboard)/avatar/stack-avatar.tsx new file mode 100644 index 000000000..e79a2e71b --- /dev/null +++ b/apps/web/src/components/(dashboard)/avatar/stack-avatar.tsx @@ -0,0 +1,51 @@ +import { Avatar, AvatarFallback } from '@documenso/ui/primitives/avatar'; + +const ZIndexes: { [key: string]: string } = { + '10': 'z-10', + '20': 'z-20', + '30': 'z-30', + '40': 'z-40', + '50': 'z-50', +}; + +export type StackAvatarProps = { + first?: boolean; + zIndex?: string; + fallbackText?: string; + type: 'unsigned' | 'waiting' | 'completed'; +}; + +export const StackAvatar = ({ first, zIndex, fallbackText, type }: StackAvatarProps) => { + let classes = ''; + let zIndexClass = ''; + const firstClass = first ? '' : '-ml-3'; + + if (zIndex) { + zIndexClass = ZIndexes[zIndex] ?? ''; + } + + switch (type) { + case 'unsigned': + classes = 'bg-dawn-200 text-dawn-900'; + break; + case 'waiting': + classes = 'bg-water text-water-700'; + break; + case 'completed': + classes = 'bg-documenso-200 text-documenso-800'; + break; + default: + break; + } + + return ( + + {fallbackText ?? 'UK'} + + ); +}; diff --git a/apps/web/src/components/(dashboard)/avatar/stack-avatars-with-tooltip.tsx b/apps/web/src/components/(dashboard)/avatar/stack-avatars-with-tooltip.tsx new file mode 100644 index 000000000..dbd1dc712 --- /dev/null +++ b/apps/web/src/components/(dashboard)/avatar/stack-avatars-with-tooltip.tsx @@ -0,0 +1,90 @@ +import { initials } from '@documenso/lib/client-only/recipient-initials'; +import { getRecipientType } from '@documenso/lib/client-only/recipient-type'; +import { Recipient } from '@documenso/prisma/client'; +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from '@documenso/ui/primitives/tooltip'; + +import { StackAvatar } from './stack-avatar'; +import { StackAvatars } from './stack-avatars'; + +export const StackAvatarsWithTooltip = ({ recipients }: { recipients: Recipient[] }) => { + const waitingRecipients = recipients.filter( + (recipient) => recipient.sendStatus === 'SENT' && recipient.signingStatus === 'NOT_SIGNED', + ); + + const completedRecipients = recipients.filter( + (recipient) => recipient.sendStatus === 'SENT' && recipient.signingStatus === 'SIGNED', + ); + + const uncompletedRecipients = recipients.filter( + (recipient) => recipient.sendStatus === 'NOT_SENT' && recipient.signingStatus === 'NOT_SIGNED', + ); + + return ( + + + + + + +
+ {completedRecipients.length > 0 && ( +
+

Completed

+ {completedRecipients.map((recipient: Recipient) => ( +
+ + {recipient.email} +
+ ))} +
+ )} + + {waitingRecipients.length > 0 && ( +
+

Waiting

+ {waitingRecipients.map((recipient: Recipient) => ( +
+ + {recipient.email} +
+ ))} +
+ )} + + {uncompletedRecipients.length > 0 && ( +
+

Uncompleted

+ {uncompletedRecipients.map((recipient: Recipient) => ( +
+ + {recipient.email} +
+ ))} +
+ )} +
+
+
+
+ ); +}; diff --git a/apps/web/src/components/(dashboard)/avatar/stack-avatars.tsx b/apps/web/src/components/(dashboard)/avatar/stack-avatars.tsx new file mode 100644 index 000000000..97af9dc9e --- /dev/null +++ b/apps/web/src/components/(dashboard)/avatar/stack-avatars.tsx @@ -0,0 +1,36 @@ +import React from 'react'; + +import { initials } from '@documenso/lib/client-only/recipient-initials'; +import { getRecipientType } from '@documenso/lib/client-only/recipient-type'; +import { Recipient } from '@documenso/prisma/client'; + +import { StackAvatar } from './stack-avatar'; + +export function StackAvatars({ recipients }: { recipients: Recipient[] }) { + const renderStackAvatars = (recipients: Recipient[]) => { + const zIndex = 50; + const itemsToRender = recipients.slice(0, 5); + const remainingItems = recipients.length - itemsToRender.length; + + return itemsToRender.map((recipient: Recipient, index: number) => { + const first = index === 0 ? true : false; + + const lastItemText = + index === itemsToRender.length - 1 && remainingItems > 0 + ? `+${remainingItems + 1}` + : undefined; + + return ( + + ); + }); + }; + + return <>{renderStackAvatars(recipients)}; +} diff --git a/packages/lib/client-only/recipient-initials.ts b/packages/lib/client-only/recipient-initials.ts new file mode 100644 index 000000000..0712ccd7d --- /dev/null +++ b/packages/lib/client-only/recipient-initials.ts @@ -0,0 +1,6 @@ +export const initials = (text: string) => + text + ?.split(' ') + .map((name: string) => name.slice(0, 1).toUpperCase()) + .slice(0, 2) + .join('') ?? 'UK'; diff --git a/packages/lib/client-only/recipient-type.ts b/packages/lib/client-only/recipient-type.ts new file mode 100644 index 000000000..8250eb707 --- /dev/null +++ b/packages/lib/client-only/recipient-type.ts @@ -0,0 +1,13 @@ +import { Recipient } from '@documenso/prisma/client'; + +export const getRecipientType = (recipient: Recipient) => { + if (recipient.sendStatus === 'SENT' && recipient.signingStatus === 'SIGNED') { + return 'completed'; + } + + if (recipient.sendStatus === 'SENT' && recipient.signingStatus === 'NOT_SIGNED') { + return 'waiting'; + } + + return 'unsigned'; +}; diff --git a/packages/lib/server-only/document/find-documents.ts b/packages/lib/server-only/document/find-documents.ts index 627ceee8b..41e9c858a 100644 --- a/packages/lib/server-only/document/find-documents.ts +++ b/packages/lib/server-only/document/find-documents.ts @@ -1,5 +1,6 @@ import { prisma } from '@documenso/prisma'; import { Document, DocumentStatus, Prisma } from '@documenso/prisma/client'; +import { DocumentWithReciepient } from '@documenso/prisma/types/document-with-recipient'; import { FindResultSet } from '../../types/find-result-set'; @@ -22,7 +23,7 @@ export const findDocuments = async ({ page = 1, perPage = 10, orderBy, -}: FindDocumentsOptions): Promise> => { +}: FindDocumentsOptions): Promise> => { const orderByColumn = orderBy?.column ?? 'created'; const orderByDirection = orderBy?.direction ?? 'desc'; @@ -48,6 +49,9 @@ export const findDocuments = async ({ orderBy: { [orderByColumn]: orderByDirection, }, + include: { + Recipient: true, + }, }), prisma.document.count({ where: { diff --git a/packages/prisma/schema.prisma b/packages/prisma/schema.prisma index 21320cc69..7657a04e5 100644 --- a/packages/prisma/schema.prisma +++ b/packages/prisma/schema.prisma @@ -143,10 +143,10 @@ model Field { recipientId Int? type FieldType page Int - positionX Decimal @default(0) - positionY Decimal @default(0) - width Decimal @default(-1) - height Decimal @default(-1) + positionX Decimal @default(0) + positionY Decimal @default(0) + width Decimal @default(-1) + height Decimal @default(-1) customText String inserted Boolean Document Document @relation(fields: [documentId], references: [id], onDelete: Cascade) diff --git a/packages/prisma/types/document-with-recipient.ts b/packages/prisma/types/document-with-recipient.ts new file mode 100644 index 000000000..208fb2b68 --- /dev/null +++ b/packages/prisma/types/document-with-recipient.ts @@ -0,0 +1,5 @@ +import { Document, Recipient } from '@documenso/prisma/client'; + +export type DocumentWithReciepient = Document & { + Recipient: Recipient[]; +}; diff --git a/packages/tailwind-config/index.cjs b/packages/tailwind-config/index.cjs index 3f414588d..d841e2711 100644 --- a/packages/tailwind-config/index.cjs +++ b/packages/tailwind-config/index.cjs @@ -76,6 +76,20 @@ module.exports = { 900: '#52514a', 950: '#2a2925', }, + water: { + DEFAULT: '#d7e4f3', + 50: '#f3f6fb', + 100: '#e3ebf6', + 200: '#d7e4f3', + 300: '#abc7e5', + 400: '#82abd8', + 500: '#658ecc', + 600: '#5175bf', + 700: '#4764ae', + 800: '#3e538f', + 900: '#364772', + 950: '#252d46', + }, }, backgroundImage: { 'gradient-radial': 'radial-gradient(var(--tw-gradient-stops))',