mirror of
https://github.com/documenso/documenso.git
synced 2025-11-14 00:32:43 +10:00
feat: onepage inbox
This commit is contained in:
@ -4,13 +4,32 @@ import { useTransition } from 'react';
|
|||||||
|
|
||||||
import Link from 'next/link';
|
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 { useUpdateSearchParams } from '@documenso/lib/client-only/hooks/use-update-search-params';
|
||||||
import { FindResultSet } from '@documenso/lib/types/find-result-set';
|
import { FindResultSet } from '@documenso/lib/types/find-result-set';
|
||||||
import { DocumentWithReciepient } from '@documenso/prisma/types/document-with-recipient';
|
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 { DataTable } from '@documenso/ui/primitives/data-table';
|
||||||
import { DataTablePagination } from '@documenso/ui/primitives/data-table-pagination';
|
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 { StackAvatarsWithTooltip } from '~/components/(dashboard)/avatar/stack-avatars-with-tooltip';
|
||||||
import { DocumentStatus } from '~/components/formatter/document-status';
|
import { DocumentStatus } from '~/components/formatter/document-status';
|
||||||
@ -67,6 +86,57 @@ export const DocumentsDataTable = ({ results }: DocumentsDataTableProps) => {
|
|||||||
accessorKey: 'created',
|
accessorKey: 'created',
|
||||||
cell: ({ row }) => <LocaleDate date={row.getValue('created')} />,
|
cell: ({ row }) => <LocaleDate date={row.getValue('created')} />,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
header: 'Actions',
|
||||||
|
cell: ({ row: _row }) => (
|
||||||
|
<div className="flex items-center gap-x-4">
|
||||||
|
<Button>Action</Button>
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger>
|
||||||
|
<MoreHorizontal className="h-5 w-5 text-gray-500" />
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
|
||||||
|
<DropdownMenuContent className="w-52" align="start" forceMount>
|
||||||
|
<DropdownMenuLabel>Action</DropdownMenuLabel>
|
||||||
|
<DropdownMenuItem disabled>
|
||||||
|
<Pencil className="mr-2 h-4 w-4" />
|
||||||
|
Sign
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem disabled>
|
||||||
|
<Edit className="mr-2 h-4 w-4" />
|
||||||
|
Edit
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem disabled>
|
||||||
|
<Download className="mr-2 h-4 w-4" />
|
||||||
|
Download
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem disabled>
|
||||||
|
<Copy className="mr-2 h-4 w-4" />
|
||||||
|
Duplicate
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem disabled>
|
||||||
|
<XCircle className="mr-2 h-4 w-4" />
|
||||||
|
Void
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem disabled>
|
||||||
|
<Trash2 className="mr-2 h-4 w-4" />
|
||||||
|
Delete
|
||||||
|
</DropdownMenuItem>
|
||||||
|
|
||||||
|
<DropdownMenuLabel>Share</DropdownMenuLabel>
|
||||||
|
<DropdownMenuItem disabled>
|
||||||
|
<History className="mr-2 h-4 w-4" />
|
||||||
|
Resend
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem disabled>
|
||||||
|
<Share className="mr-2 h-4 w-4" />
|
||||||
|
Share
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
},
|
||||||
]}
|
]}
|
||||||
data={results.data}
|
data={results.data}
|
||||||
perPage={results.perPage}
|
perPage={results.perPage}
|
||||||
|
|||||||
@ -3,8 +3,8 @@ import Link from 'next/link';
|
|||||||
import { getRequiredServerComponentSession } from '@documenso/lib/next-auth/get-server-session';
|
import { getRequiredServerComponentSession } from '@documenso/lib/next-auth/get-server-session';
|
||||||
import { findDocuments } from '@documenso/lib/server-only/document/find-documents';
|
import { findDocuments } from '@documenso/lib/server-only/document/find-documents';
|
||||||
import { getStats } from '@documenso/lib/server-only/document/get-stats';
|
import { getStats } from '@documenso/lib/server-only/document/get-stats';
|
||||||
import { isDocumentStatus } from '@documenso/lib/types/is-document-status';
|
import { isExtendedDocumentStatus } from '@documenso/prisma/guards/is-extended-document-status';
|
||||||
import { DocumentStatus as InternalDocumentStatus } from '@documenso/prisma/client';
|
import { ExtendedDocumentStatus } from '@documenso/prisma/types/extended-document-status';
|
||||||
import { Tabs, TabsList, TabsTrigger } from '@documenso/ui/primitives/tabs';
|
import { Tabs, TabsList, TabsTrigger } from '@documenso/ui/primitives/tabs';
|
||||||
|
|
||||||
import { PeriodSelector } from '~/components/(dashboard)/period-selector/period-selector';
|
import { PeriodSelector } from '~/components/(dashboard)/period-selector/period-selector';
|
||||||
@ -16,7 +16,7 @@ import { DocumentsDataTable } from './data-table';
|
|||||||
|
|
||||||
export type DocumentsPageProps = {
|
export type DocumentsPageProps = {
|
||||||
searchParams?: {
|
searchParams?: {
|
||||||
status?: InternalDocumentStatus | 'ALL';
|
status?: ExtendedDocumentStatus;
|
||||||
period?: PeriodSelectorValue;
|
period?: PeriodSelectorValue;
|
||||||
page?: string;
|
page?: string;
|
||||||
perPage?: string;
|
perPage?: string;
|
||||||
@ -24,22 +24,20 @@ export type DocumentsPageProps = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export default async function DocumentsPage({ searchParams = {} }: DocumentsPageProps) {
|
export default async function DocumentsPage({ searchParams = {} }: DocumentsPageProps) {
|
||||||
const session = await getRequiredServerComponentSession();
|
const user = await getRequiredServerComponentSession();
|
||||||
|
|
||||||
const stats = await getStats({
|
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 period = isPeriodSelectorValue(searchParams.period) ? searchParams.period : '';
|
||||||
const page = Number(searchParams.page) || 1;
|
const page = Number(searchParams.page) || 1;
|
||||||
const perPage = Number(searchParams.perPage) || 20;
|
const perPage = Number(searchParams.perPage) || 20;
|
||||||
|
|
||||||
const shouldDefaultToPending = status === 'ALL' && stats.PENDING > 0;
|
|
||||||
|
|
||||||
const results = await findDocuments({
|
const results = await findDocuments({
|
||||||
userId: session.id,
|
userId: user.id,
|
||||||
status: status === 'ALL' ? undefined : status,
|
status,
|
||||||
orderBy: {
|
orderBy: {
|
||||||
column: 'created',
|
column: 'created',
|
||||||
direction: 'desc',
|
direction: 'desc',
|
||||||
@ -57,10 +55,6 @@ export default async function DocumentsPage({ searchParams = {} }: DocumentsPage
|
|||||||
params.delete('page');
|
params.delete('page');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (value === 'ALL') {
|
|
||||||
params.delete('status');
|
|
||||||
}
|
|
||||||
|
|
||||||
return `/documents?${params.toString()}`;
|
return `/documents?${params.toString()}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -71,46 +65,27 @@ export default async function DocumentsPage({ searchParams = {} }: DocumentsPage
|
|||||||
<h1 className="mt-12 text-4xl font-semibold">Documents</h1>
|
<h1 className="mt-12 text-4xl font-semibold">Documents</h1>
|
||||||
|
|
||||||
<div className="mt-8 flex flex-wrap gap-x-4 gap-y-6">
|
<div className="mt-8 flex flex-wrap gap-x-4 gap-y-6">
|
||||||
<Tabs
|
<Tabs defaultValue={status} className="overflow-x-auto">
|
||||||
className="overflow-x-auto"
|
|
||||||
defaultValue={shouldDefaultToPending ? InternalDocumentStatus.PENDING : status}
|
|
||||||
>
|
|
||||||
<TabsList>
|
<TabsList>
|
||||||
<TabsTrigger className="min-w-[60px]" value={InternalDocumentStatus.PENDING} asChild>
|
{[
|
||||||
<Link href={getTabHref(InternalDocumentStatus.PENDING)} scroll={false}>
|
ExtendedDocumentStatus.INBOX,
|
||||||
<DocumentStatus status={InternalDocumentStatus.PENDING} />
|
ExtendedDocumentStatus.PENDING,
|
||||||
|
ExtendedDocumentStatus.COMPLETED,
|
||||||
|
ExtendedDocumentStatus.DRAFT,
|
||||||
|
ExtendedDocumentStatus.ALL,
|
||||||
|
].map((value) => (
|
||||||
|
<TabsTrigger key={value} className="min-w-[60px]" value={value} asChild>
|
||||||
|
<Link href={getTabHref(value)} scroll={false}>
|
||||||
|
<DocumentStatus status={value} />
|
||||||
|
|
||||||
|
{value !== ExtendedDocumentStatus.ALL && (
|
||||||
<span className="ml-1 hidden opacity-50 md:inline-block">
|
<span className="ml-1 hidden opacity-50 md:inline-block">
|
||||||
{Math.min(stats.PENDING, 99)}
|
{Math.min(stats[value], 99)}
|
||||||
</span>
|
</span>
|
||||||
|
)}
|
||||||
</Link>
|
</Link>
|
||||||
</TabsTrigger>
|
</TabsTrigger>
|
||||||
|
))}
|
||||||
<TabsTrigger className="min-w-[60px]" value={InternalDocumentStatus.COMPLETED} asChild>
|
|
||||||
<Link href={getTabHref(InternalDocumentStatus.COMPLETED)} scroll={false}>
|
|
||||||
<DocumentStatus status={InternalDocumentStatus.COMPLETED} />
|
|
||||||
|
|
||||||
<span className="ml-1 hidden opacity-50 md:inline-block">
|
|
||||||
{Math.min(stats.COMPLETED, 99)}
|
|
||||||
</span>
|
|
||||||
</Link>
|
|
||||||
</TabsTrigger>
|
|
||||||
|
|
||||||
<TabsTrigger className="min-w-[60px]" value={InternalDocumentStatus.DRAFT} asChild>
|
|
||||||
<Link href={getTabHref(InternalDocumentStatus.DRAFT)} scroll={false}>
|
|
||||||
<DocumentStatus status={InternalDocumentStatus.DRAFT} />
|
|
||||||
|
|
||||||
<span className="ml-1 hidden opacity-50 md:inline-block">
|
|
||||||
{Math.min(stats.DRAFT, 99)}
|
|
||||||
</span>
|
|
||||||
</Link>
|
|
||||||
</TabsTrigger>
|
|
||||||
|
|
||||||
<TabsTrigger className="min-w-[60px]" value="ALL" asChild>
|
|
||||||
<Link href={getTabHref('ALL')} scroll={false}>
|
|
||||||
All
|
|
||||||
</Link>
|
|
||||||
</TabsTrigger>
|
|
||||||
</TabsList>
|
</TabsList>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
|
|
||||||
|
|||||||
@ -3,16 +3,17 @@ import { HTMLAttributes } from 'react';
|
|||||||
import { CheckCircle2, Clock, File } from 'lucide-react';
|
import { CheckCircle2, Clock, File } from 'lucide-react';
|
||||||
import type { LucideIcon } from 'lucide-react/dist/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';
|
import { cn } from '@documenso/ui/lib/utils';
|
||||||
|
|
||||||
type FriendlyStatus = {
|
type FriendlyStatus = {
|
||||||
label: string;
|
label: string;
|
||||||
icon: LucideIcon;
|
icon?: LucideIcon;
|
||||||
color: string;
|
color: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
const FRIENDLY_STATUS_MAP: Record<InternalDocumentStatus, FriendlyStatus> = {
|
const FRIENDLY_STATUS_MAP: Record<ExtendedDocumentStatus, FriendlyStatus> = {
|
||||||
PENDING: {
|
PENDING: {
|
||||||
label: 'Pending',
|
label: 'Pending',
|
||||||
icon: Clock,
|
icon: Clock,
|
||||||
@ -28,10 +29,19 @@ const FRIENDLY_STATUS_MAP: Record<InternalDocumentStatus, FriendlyStatus> = {
|
|||||||
icon: File,
|
icon: File,
|
||||||
color: 'text-yellow-500',
|
color: 'text-yellow-500',
|
||||||
},
|
},
|
||||||
|
INBOX: {
|
||||||
|
label: 'Inbox',
|
||||||
|
icon: SignatureIcon,
|
||||||
|
color: 'text-muted-foreground',
|
||||||
|
},
|
||||||
|
ALL: {
|
||||||
|
label: 'All',
|
||||||
|
color: 'text-muted-foreground',
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export type DocumentStatusProps = HTMLAttributes<HTMLSpanElement> & {
|
export type DocumentStatusProps = HTMLAttributes<HTMLSpanElement> & {
|
||||||
status: InternalDocumentStatus;
|
status: ExtendedDocumentStatus;
|
||||||
inheritColor?: boolean;
|
inheritColor?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -45,11 +55,13 @@ export const DocumentStatus = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<span className={cn('flex items-center', className)} {...props}>
|
<span className={cn('flex items-center', className)} {...props}>
|
||||||
|
{Icon && (
|
||||||
<Icon
|
<Icon
|
||||||
className={cn('mr-2 inline-block h-4 w-4', {
|
className={cn('mr-2 inline-block h-4 w-4', {
|
||||||
[color]: !inheritColor,
|
[color]: !inheritColor,
|
||||||
})}
|
})}
|
||||||
/>
|
/>
|
||||||
|
)}
|
||||||
{label}
|
{label}
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -1,14 +1,17 @@
|
|||||||
|
import { match } from 'ts-pattern';
|
||||||
|
|
||||||
import { prisma } from '@documenso/prisma';
|
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 { DocumentWithRecipientAndSender } from '@documenso/prisma/types/document';
|
||||||
import { DocumentWithReciepient } from '@documenso/prisma/types/document-with-recipient';
|
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';
|
import { FindResultSet } from '../../types/find-result-set';
|
||||||
|
|
||||||
export interface FindDocumentsOptions {
|
export interface FindDocumentsOptions {
|
||||||
userId: number;
|
userId: number;
|
||||||
term?: string;
|
term?: string;
|
||||||
status?: DocumentStatus;
|
status?: ExtendedDocumentStatus;
|
||||||
page?: number;
|
page?: number;
|
||||||
perPage?: number;
|
perPage?: number;
|
||||||
orderBy?: {
|
orderBy?: {
|
||||||
@ -20,29 +23,102 @@ export interface FindDocumentsOptions {
|
|||||||
export const findDocuments = async ({
|
export const findDocuments = async ({
|
||||||
userId,
|
userId,
|
||||||
term,
|
term,
|
||||||
status,
|
status = ExtendedDocumentStatus.ALL,
|
||||||
page = 1,
|
page = 1,
|
||||||
perPage = 10,
|
perPage = 10,
|
||||||
orderBy,
|
orderBy,
|
||||||
}: FindDocumentsOptions): Promise<FindResultSet<DocumentWithReciepient>> => {
|
}: FindDocumentsOptions): Promise<FindResultSet<DocumentWithReciepient>> => {
|
||||||
|
const user = await prisma.user.findFirstOrThrow({
|
||||||
|
where: {
|
||||||
|
id: userId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
const orderByColumn = orderBy?.column ?? 'created';
|
const orderByColumn = orderBy?.column ?? 'created';
|
||||||
const orderByDirection = orderBy?.direction ?? 'desc';
|
const orderByDirection = orderBy?.direction ?? 'desc';
|
||||||
|
|
||||||
const filters: Prisma.DocumentWhereInput = {
|
const termFilters = !term
|
||||||
status,
|
? undefined
|
||||||
userId,
|
: ({
|
||||||
};
|
title: {
|
||||||
|
|
||||||
if (term) {
|
|
||||||
filters.title = {
|
|
||||||
contains: term,
|
contains: term,
|
||||||
mode: 'insensitive',
|
mode: 'insensitive',
|
||||||
};
|
},
|
||||||
}
|
} as const);
|
||||||
|
|
||||||
|
const filters = match<ExtendedDocumentStatus, Prisma.DocumentWhereInput>(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([
|
const [data, count] = await Promise.all([
|
||||||
prisma.document.findMany({
|
prisma.document.findMany({
|
||||||
where: {
|
where: {
|
||||||
|
...termFilters,
|
||||||
...filters,
|
...filters,
|
||||||
},
|
},
|
||||||
skip: Math.max(page - 1, 0) * perPage,
|
skip: Math.max(page - 1, 0) * perPage,
|
||||||
@ -51,11 +127,19 @@ export const findDocuments = async ({
|
|||||||
[orderByColumn]: orderByDirection,
|
[orderByColumn]: orderByDirection,
|
||||||
},
|
},
|
||||||
include: {
|
include: {
|
||||||
|
User: {
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
name: true,
|
||||||
|
email: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
Recipient: true,
|
Recipient: true,
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
prisma.document.count({
|
prisma.document.count({
|
||||||
where: {
|
where: {
|
||||||
|
...termFilters,
|
||||||
...filters,
|
...filters,
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
|
|||||||
@ -1,30 +1,88 @@
|
|||||||
import { prisma } from '@documenso/prisma';
|
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 = {
|
export type GetStatsInput = {
|
||||||
userId: number;
|
user: User;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getStats = async ({ userId }: GetStatsInput) => {
|
export const getStats = async ({ user }: GetStatsInput) => {
|
||||||
const result = await prisma.document.groupBy({
|
const [ownerCounts, notSignedCounts, hasSignedCounts] = await Promise.all([
|
||||||
|
prisma.document.groupBy({
|
||||||
by: ['status'],
|
by: ['status'],
|
||||||
_count: {
|
_count: {
|
||||||
_all: true,
|
_all: true,
|
||||||
},
|
},
|
||||||
where: {
|
where: {
|
||||||
userId,
|
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, number> = {
|
const stats: Record<ExtendedDocumentStatus, number> = {
|
||||||
[DocumentStatus.DRAFT]: 0,
|
[ExtendedDocumentStatus.DRAFT]: 0,
|
||||||
[DocumentStatus.PENDING]: 0,
|
[ExtendedDocumentStatus.PENDING]: 0,
|
||||||
[DocumentStatus.COMPLETED]: 0,
|
[ExtendedDocumentStatus.COMPLETED]: 0,
|
||||||
|
[ExtendedDocumentStatus.INBOX]: 0,
|
||||||
|
[ExtendedDocumentStatus.ALL]: 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
result.forEach((stat) => {
|
ownerCounts.forEach((stat) => {
|
||||||
stats[stat.status] = stat._count._all;
|
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;
|
return stats;
|
||||||
};
|
};
|
||||||
|
|||||||
11
packages/prisma/guards/is-extended-document-status.ts
Normal file
11
packages/prisma/guards/is-extended-document-status.ts
Normal file
@ -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);
|
||||||
|
};
|
||||||
10
packages/prisma/types/extended-document-status.ts
Normal file
10
packages/prisma/types/extended-document-status.ts
Normal file
@ -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];
|
||||||
28
packages/ui/icons/signature.tsx
Normal file
28
packages/ui/icons/signature.tsx
Normal file
@ -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 (
|
||||||
|
<svg
|
||||||
|
width={size}
|
||||||
|
height={size}
|
||||||
|
viewBox="0 0 16 16"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M1.5 11H14.5M1.5 14C1.5 14 8.72 2 4.86938 2H4.875C2.01 2 1.97437 14.0694 8 6.51188V6.5C8 6.5 9 11.3631 11.5 7.52375V7.5C11.5 7.5 11.5 9 14.5 9"
|
||||||
|
stroke={color}
|
||||||
|
strokeWidth={absoluteStrokeWidth ? (Number(strokeWidth) * 24) / Number(size) : strokeWidth}
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user