mirror of
https://github.com/documenso/documenso.git
synced 2025-11-11 13:02:31 +10:00
Compare commits
1 Commits
v2.0.2
...
feat/docum
| Author | SHA1 | Date | |
|---|---|---|---|
| 5f17288af6 |
@ -0,0 +1,54 @@
|
|||||||
|
import { useEffect } from 'react';
|
||||||
|
|
||||||
|
import type { DocumentStatus, RecipientRole, SigningStatus } from '@prisma/client';
|
||||||
|
import { useRevalidator } from 'react-router';
|
||||||
|
|
||||||
|
import { isDocumentBeingProcessed } from '@documenso/lib/utils/document';
|
||||||
|
|
||||||
|
type DocumentType = {
|
||||||
|
id: number;
|
||||||
|
status: DocumentStatus;
|
||||||
|
deletedAt: Date | null;
|
||||||
|
recipients: Array<{
|
||||||
|
role: RecipientRole;
|
||||||
|
signingStatus: SigningStatus;
|
||||||
|
}>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type DocumentProcessingPollProps = {
|
||||||
|
documents?: DocumentType[] | DocumentType;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const DocumentProcessingPoll = ({ documents }: DocumentProcessingPollProps) => {
|
||||||
|
const { revalidate } = useRevalidator();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!documents) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const documentArray = Array.isArray(documents) ? documents : [documents];
|
||||||
|
|
||||||
|
if (documentArray.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const hasProcessingDocuments = documentArray.some((document) =>
|
||||||
|
isDocumentBeingProcessed(document),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!hasProcessingDocuments) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const interval = setInterval(() => {
|
||||||
|
if (window.document.hasFocus()) {
|
||||||
|
void revalidate();
|
||||||
|
}
|
||||||
|
}, 3000);
|
||||||
|
|
||||||
|
return () => clearInterval(interval);
|
||||||
|
}, [documents, revalidate]);
|
||||||
|
|
||||||
|
return null;
|
||||||
|
};
|
||||||
@ -3,7 +3,7 @@ import type { HTMLAttributes } from 'react';
|
|||||||
import type { MessageDescriptor } from '@lingui/core';
|
import type { MessageDescriptor } from '@lingui/core';
|
||||||
import { msg } from '@lingui/core/macro';
|
import { msg } from '@lingui/core/macro';
|
||||||
import { useLingui } from '@lingui/react';
|
import { useLingui } from '@lingui/react';
|
||||||
import { CheckCircle2, Clock, File, XCircle } from 'lucide-react';
|
import { CheckCircle2, Clock, File, Loader2, XCircle } from 'lucide-react';
|
||||||
import type { LucideIcon } from 'lucide-react/dist/lucide-react';
|
import type { LucideIcon } from 'lucide-react/dist/lucide-react';
|
||||||
|
|
||||||
import type { ExtendedDocumentStatus } from '@documenso/prisma/types/extended-document-status';
|
import type { ExtendedDocumentStatus } from '@documenso/prisma/types/extended-document-status';
|
||||||
@ -15,6 +15,7 @@ type FriendlyStatus = {
|
|||||||
labelExtended: MessageDescriptor;
|
labelExtended: MessageDescriptor;
|
||||||
icon?: LucideIcon;
|
icon?: LucideIcon;
|
||||||
color: string;
|
color: string;
|
||||||
|
animate?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const FRIENDLY_STATUS_MAP: Record<ExtendedDocumentStatus, FriendlyStatus> = {
|
export const FRIENDLY_STATUS_MAP: Record<ExtendedDocumentStatus, FriendlyStatus> = {
|
||||||
@ -55,20 +56,31 @@ export const FRIENDLY_STATUS_MAP: Record<ExtendedDocumentStatus, FriendlyStatus>
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const PROCESSING_STATUS: FriendlyStatus = {
|
||||||
|
label: msg`Processing`,
|
||||||
|
labelExtended: msg`Document processing`,
|
||||||
|
icon: Loader2,
|
||||||
|
color: 'text-blue-600 dark:text-blue-300',
|
||||||
|
animate: true,
|
||||||
|
};
|
||||||
|
|
||||||
export type DocumentStatusProps = HTMLAttributes<HTMLSpanElement> & {
|
export type DocumentStatusProps = HTMLAttributes<HTMLSpanElement> & {
|
||||||
status: ExtendedDocumentStatus;
|
status: ExtendedDocumentStatus;
|
||||||
inheritColor?: boolean;
|
inheritColor?: boolean;
|
||||||
|
isProcessing?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const DocumentStatus = ({
|
export const DocumentStatus = ({
|
||||||
className,
|
className,
|
||||||
status,
|
status,
|
||||||
inheritColor,
|
inheritColor,
|
||||||
|
isProcessing,
|
||||||
...props
|
...props
|
||||||
}: DocumentStatusProps) => {
|
}: DocumentStatusProps) => {
|
||||||
const { _ } = useLingui();
|
const { _ } = useLingui();
|
||||||
|
|
||||||
const { label, icon: Icon, color } = FRIENDLY_STATUS_MAP[status];
|
const statusConfig = isProcessing ? PROCESSING_STATUS : FRIENDLY_STATUS_MAP[status];
|
||||||
|
const { label, icon: Icon, color, animate } = statusConfig;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<span className={cn('flex items-center', className)} {...props}>
|
<span className={cn('flex items-center', className)} {...props}>
|
||||||
@ -76,6 +88,7 @@ export const DocumentStatus = ({
|
|||||||
<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,
|
||||||
|
'animate-spin': animate,
|
||||||
})}
|
})}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -9,7 +9,7 @@ import { match } from 'ts-pattern';
|
|||||||
|
|
||||||
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 { useSession } from '@documenso/lib/client-only/providers/session';
|
import { useSession } from '@documenso/lib/client-only/providers/session';
|
||||||
import { isDocumentCompleted } from '@documenso/lib/utils/document';
|
import { isDocumentBeingProcessed, isDocumentCompleted } from '@documenso/lib/utils/document';
|
||||||
import { formatDocumentsPath } from '@documenso/lib/utils/teams';
|
import { formatDocumentsPath } from '@documenso/lib/utils/teams';
|
||||||
import type { TFindDocumentsResponse } from '@documenso/trpc/server/document-router/schema';
|
import type { TFindDocumentsResponse } from '@documenso/trpc/server/document-router/schema';
|
||||||
import type { DataTableColumnDef } from '@documenso/ui/primitives/data-table';
|
import type { DataTableColumnDef } from '@documenso/ui/primitives/data-table';
|
||||||
@ -77,7 +77,12 @@ export const DocumentsTable = ({
|
|||||||
{
|
{
|
||||||
header: _(msg`Status`),
|
header: _(msg`Status`),
|
||||||
accessorKey: 'status',
|
accessorKey: 'status',
|
||||||
cell: ({ row }) => <DocumentStatus status={row.original.status} />,
|
cell: ({ row }) => (
|
||||||
|
<DocumentStatus
|
||||||
|
status={row.original.status}
|
||||||
|
isProcessing={isDocumentBeingProcessed(row.original)}
|
||||||
|
/>
|
||||||
|
),
|
||||||
size: 140,
|
size: 140,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@ -6,6 +6,7 @@ import { DateTime } from 'luxon';
|
|||||||
import { Link, redirect } from 'react-router';
|
import { Link, redirect } from 'react-router';
|
||||||
|
|
||||||
import { getEntireDocument } from '@documenso/lib/server-only/admin/get-entire-document';
|
import { getEntireDocument } from '@documenso/lib/server-only/admin/get-entire-document';
|
||||||
|
import { isDocumentBeingProcessed } from '@documenso/lib/utils/document';
|
||||||
import { trpc } from '@documenso/trpc/react';
|
import { trpc } from '@documenso/trpc/react';
|
||||||
import {
|
import {
|
||||||
Accordion,
|
Accordion,
|
||||||
@ -24,6 +25,7 @@ import {
|
|||||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||||
|
|
||||||
import { AdminDocumentDeleteDialog } from '~/components/dialogs/admin-document-delete-dialog';
|
import { AdminDocumentDeleteDialog } from '~/components/dialogs/admin-document-delete-dialog';
|
||||||
|
import { DocumentProcessingPoll } from '~/components/general/document/document-processing-poll';
|
||||||
import { DocumentStatus } from '~/components/general/document/document-status';
|
import { DocumentStatus } from '~/components/general/document/document-status';
|
||||||
import { AdminDocumentRecipientItemTable } from '~/components/tables/admin-document-recipient-item-table';
|
import { AdminDocumentRecipientItemTable } from '~/components/tables/admin-document-recipient-item-table';
|
||||||
|
|
||||||
@ -69,7 +71,10 @@ export default function AdminDocumentDetailsPage({ loaderData }: Route.Component
|
|||||||
<div className="flex items-start justify-between">
|
<div className="flex items-start justify-between">
|
||||||
<div className="flex items-center gap-x-4">
|
<div className="flex items-center gap-x-4">
|
||||||
<h1 className="text-2xl font-semibold">{document.title}</h1>
|
<h1 className="text-2xl font-semibold">{document.title}</h1>
|
||||||
<DocumentStatus status={document.status} />
|
<DocumentStatus
|
||||||
|
status={document.status}
|
||||||
|
isProcessing={isDocumentBeingProcessed(document)}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{document.deletedAt && (
|
{document.deletedAt && (
|
||||||
@ -162,6 +167,8 @@ export default function AdminDocumentDetailsPage({ loaderData }: Route.Component
|
|||||||
<hr className="my-4" />
|
<hr className="my-4" />
|
||||||
|
|
||||||
{document && <AdminDocumentDeleteDialog document={document} />}
|
{document && <AdminDocumentDeleteDialog document={document} />}
|
||||||
|
|
||||||
|
<DocumentProcessingPoll documents={document} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -8,6 +8,7 @@ import { Link, useSearchParams } from 'react-router';
|
|||||||
|
|
||||||
import { useDebouncedValue } from '@documenso/lib/client-only/hooks/use-debounced-value';
|
import { useDebouncedValue } from '@documenso/lib/client-only/hooks/use-debounced-value';
|
||||||
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 { isDocumentBeingProcessed } from '@documenso/lib/utils/document';
|
||||||
import { extractInitials } from '@documenso/lib/utils/recipient-formatter';
|
import { extractInitials } from '@documenso/lib/utils/recipient-formatter';
|
||||||
import { trpc } from '@documenso/trpc/react';
|
import { trpc } from '@documenso/trpc/react';
|
||||||
import { Avatar, AvatarFallback } from '@documenso/ui/primitives/avatar';
|
import { Avatar, AvatarFallback } from '@documenso/ui/primitives/avatar';
|
||||||
@ -75,7 +76,12 @@ export default function AdminDocumentsPage() {
|
|||||||
{
|
{
|
||||||
header: _(msg`Status`),
|
header: _(msg`Status`),
|
||||||
accessorKey: 'status',
|
accessorKey: 'status',
|
||||||
cell: ({ row }) => <DocumentStatus status={row.original.status} />,
|
cell: ({ row }) => (
|
||||||
|
<DocumentStatus
|
||||||
|
status={row.original.status}
|
||||||
|
isProcessing={isDocumentBeingProcessed(row.original)}
|
||||||
|
/>
|
||||||
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
header: _(msg`Owner`),
|
header: _(msg`Owner`),
|
||||||
|
|||||||
@ -10,6 +10,7 @@ import { useSession } from '@documenso/lib/client-only/providers/session';
|
|||||||
import { getDocumentWithDetailsById } from '@documenso/lib/server-only/document/get-document-with-details-by-id';
|
import { getDocumentWithDetailsById } from '@documenso/lib/server-only/document/get-document-with-details-by-id';
|
||||||
import { getTeamByUrl } from '@documenso/lib/server-only/team/get-team';
|
import { getTeamByUrl } from '@documenso/lib/server-only/team/get-team';
|
||||||
import { DocumentVisibility } from '@documenso/lib/types/document-visibility';
|
import { DocumentVisibility } from '@documenso/lib/types/document-visibility';
|
||||||
|
import { isDocumentBeingProcessed } from '@documenso/lib/utils/document';
|
||||||
import { formatDocumentsPath } from '@documenso/lib/utils/teams';
|
import { formatDocumentsPath } from '@documenso/lib/utils/teams';
|
||||||
import { DocumentReadOnlyFields } from '@documenso/ui/components/document/document-read-only-fields';
|
import { DocumentReadOnlyFields } from '@documenso/ui/components/document/document-read-only-fields';
|
||||||
import { Badge } from '@documenso/ui/primitives/badge';
|
import { Badge } from '@documenso/ui/primitives/badge';
|
||||||
@ -23,6 +24,7 @@ import { DocumentPageViewDropdown } from '~/components/general/document/document
|
|||||||
import { DocumentPageViewInformation } from '~/components/general/document/document-page-view-information';
|
import { DocumentPageViewInformation } from '~/components/general/document/document-page-view-information';
|
||||||
import { DocumentPageViewRecentActivity } from '~/components/general/document/document-page-view-recent-activity';
|
import { DocumentPageViewRecentActivity } from '~/components/general/document/document-page-view-recent-activity';
|
||||||
import { DocumentPageViewRecipients } from '~/components/general/document/document-page-view-recipients';
|
import { DocumentPageViewRecipients } from '~/components/general/document/document-page-view-recipients';
|
||||||
|
import { DocumentProcessingPoll } from '~/components/general/document/document-processing-poll';
|
||||||
import { DocumentRecipientLinkCopyDialog } from '~/components/general/document/document-recipient-link-copy-dialog';
|
import { DocumentRecipientLinkCopyDialog } from '~/components/general/document/document-recipient-link-copy-dialog';
|
||||||
import {
|
import {
|
||||||
DocumentStatus as DocumentStatusComponent,
|
DocumentStatus as DocumentStatusComponent,
|
||||||
@ -128,6 +130,7 @@ export default function DocumentPage() {
|
|||||||
<DocumentStatusComponent
|
<DocumentStatusComponent
|
||||||
inheritColor
|
inheritColor
|
||||||
status={document.status}
|
status={document.status}
|
||||||
|
isProcessing={isDocumentBeingProcessed(document)}
|
||||||
className="text-muted-foreground"
|
className="text-muted-foreground"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@ -241,6 +244,8 @@ export default function DocumentPage() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<DocumentProcessingPoll documents={document} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,8 +3,7 @@ import { useEffect, useMemo, useState } from 'react';
|
|||||||
import { Trans } from '@lingui/react/macro';
|
import { Trans } from '@lingui/react/macro';
|
||||||
import { OrganisationType } from '@prisma/client';
|
import { OrganisationType } from '@prisma/client';
|
||||||
import { FolderIcon, HomeIcon, Loader2 } from 'lucide-react';
|
import { FolderIcon, HomeIcon, Loader2 } from 'lucide-react';
|
||||||
import { useNavigate, useSearchParams } from 'react-router';
|
import { Link, useNavigate, useSearchParams } from 'react-router';
|
||||||
import { Link } from 'react-router';
|
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
import { useCurrentOrganisation } from '@documenso/lib/client-only/providers/organisation';
|
import { useCurrentOrganisation } from '@documenso/lib/client-only/providers/organisation';
|
||||||
@ -29,6 +28,7 @@ import { FolderDeleteDialog } from '~/components/dialogs/folder-delete-dialog';
|
|||||||
import { FolderMoveDialog } from '~/components/dialogs/folder-move-dialog';
|
import { FolderMoveDialog } from '~/components/dialogs/folder-move-dialog';
|
||||||
import { FolderSettingsDialog } from '~/components/dialogs/folder-settings-dialog';
|
import { FolderSettingsDialog } from '~/components/dialogs/folder-settings-dialog';
|
||||||
import { DocumentDropZoneWrapper } from '~/components/general/document/document-drop-zone-wrapper';
|
import { DocumentDropZoneWrapper } from '~/components/general/document/document-drop-zone-wrapper';
|
||||||
|
import { DocumentProcessingPoll } from '~/components/general/document/document-processing-poll';
|
||||||
import { DocumentSearch } from '~/components/general/document/document-search';
|
import { DocumentSearch } from '~/components/general/document/document-search';
|
||||||
import { DocumentStatus } from '~/components/general/document/document-status';
|
import { DocumentStatus } from '~/components/general/document/document-status';
|
||||||
import { DocumentUploadDropzone } from '~/components/general/document/document-upload';
|
import { DocumentUploadDropzone } from '~/components/general/document/document-upload';
|
||||||
@ -346,6 +346,8 @@ export default function DocumentsPage() {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
<DocumentProcessingPoll documents={data?.data} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@ -26,6 +26,7 @@ import { FolderDeleteDialog } from '~/components/dialogs/folder-delete-dialog';
|
|||||||
import { FolderMoveDialog } from '~/components/dialogs/folder-move-dialog';
|
import { FolderMoveDialog } from '~/components/dialogs/folder-move-dialog';
|
||||||
import { FolderSettingsDialog } from '~/components/dialogs/folder-settings-dialog';
|
import { FolderSettingsDialog } from '~/components/dialogs/folder-settings-dialog';
|
||||||
import { DocumentDropZoneWrapper } from '~/components/general/document/document-drop-zone-wrapper';
|
import { DocumentDropZoneWrapper } from '~/components/general/document/document-drop-zone-wrapper';
|
||||||
|
import { DocumentProcessingPoll } from '~/components/general/document/document-processing-poll';
|
||||||
import { DocumentSearch } from '~/components/general/document/document-search';
|
import { DocumentSearch } from '~/components/general/document/document-search';
|
||||||
import { DocumentStatus } from '~/components/general/document/document-status';
|
import { DocumentStatus } from '~/components/general/document/document-status';
|
||||||
import { DocumentUploadDropzone } from '~/components/general/document/document-upload';
|
import { DocumentUploadDropzone } from '~/components/general/document/document-upload';
|
||||||
@ -314,6 +315,8 @@ export default function DocumentsPage() {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
<DocumentProcessingPoll documents={data?.data} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@ -3,8 +3,14 @@ import { useEffect } from 'react';
|
|||||||
import { msg } from '@lingui/core/macro';
|
import { msg } from '@lingui/core/macro';
|
||||||
import { useLingui } from '@lingui/react';
|
import { useLingui } from '@lingui/react';
|
||||||
import { Trans } from '@lingui/react/macro';
|
import { Trans } from '@lingui/react/macro';
|
||||||
import { type Document, DocumentStatus, FieldType, RecipientRole } from '@prisma/client';
|
import {
|
||||||
import { CheckCircle2, Clock8, FileSearch } from 'lucide-react';
|
type Document,
|
||||||
|
DocumentStatus,
|
||||||
|
FieldType,
|
||||||
|
RecipientRole,
|
||||||
|
SigningStatus,
|
||||||
|
} from '@prisma/client';
|
||||||
|
import { CheckCircle2, Clock8, FileSearch, Loader2 } from 'lucide-react';
|
||||||
import { Link, useRevalidator } from 'react-router';
|
import { Link, useRevalidator } from 'react-router';
|
||||||
import { match } from 'ts-pattern';
|
import { match } from 'ts-pattern';
|
||||||
|
|
||||||
@ -16,10 +22,11 @@ import { isRecipientAuthorized } from '@documenso/lib/server-only/document/is-re
|
|||||||
import { getFieldsForToken } from '@documenso/lib/server-only/field/get-fields-for-token';
|
import { getFieldsForToken } from '@documenso/lib/server-only/field/get-fields-for-token';
|
||||||
import { getRecipientByToken } from '@documenso/lib/server-only/recipient/get-recipient-by-token';
|
import { getRecipientByToken } from '@documenso/lib/server-only/recipient/get-recipient-by-token';
|
||||||
import { getRecipientSignatures } from '@documenso/lib/server-only/recipient/get-recipient-signatures';
|
import { getRecipientSignatures } from '@documenso/lib/server-only/recipient/get-recipient-signatures';
|
||||||
|
import { getAllRecipientsByDocumentId } from '@documenso/lib/server-only/recipient/get-recipients-for-document';
|
||||||
import { getUserByEmail } from '@documenso/lib/server-only/user/get-user-by-email';
|
import { getUserByEmail } from '@documenso/lib/server-only/user/get-user-by-email';
|
||||||
import { isDocumentCompleted } from '@documenso/lib/utils/document';
|
import { isDocumentCompleted } from '@documenso/lib/utils/document';
|
||||||
import { env } from '@documenso/lib/utils/env';
|
import { env } from '@documenso/lib/utils/env';
|
||||||
import DocumentDialog from '@documenso/ui/components/document/document-dialog';
|
import { DocumentDialog } from '@documenso/ui/components/document/document-dialog';
|
||||||
import { DocumentDownloadButton } from '@documenso/ui/components/document/document-download-button';
|
import { DocumentDownloadButton } from '@documenso/ui/components/document/document-download-button';
|
||||||
import { DocumentShareButton } from '@documenso/ui/components/document/document-share-button';
|
import { DocumentShareButton } from '@documenso/ui/components/document/document-share-button';
|
||||||
import { SigningCard3D } from '@documenso/ui/components/signing-card';
|
import { SigningCard3D } from '@documenso/ui/components/signing-card';
|
||||||
@ -50,9 +57,10 @@ export async function loader({ params, request }: Route.LoaderArgs) {
|
|||||||
throw new Response('Not Found', { status: 404 });
|
throw new Response('Not Found', { status: 404 });
|
||||||
}
|
}
|
||||||
|
|
||||||
const [fields, recipient] = await Promise.all([
|
const [fields, recipient, allRecipients] = await Promise.all([
|
||||||
getFieldsForToken({ token }),
|
getFieldsForToken({ token }),
|
||||||
getRecipientByToken({ token }).catch(() => null),
|
getRecipientByToken({ token }).catch(() => null),
|
||||||
|
getAllRecipientsByDocumentId({ documentId: document.id }),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
if (!recipient) {
|
if (!recipient) {
|
||||||
@ -66,17 +74,26 @@ export async function loader({ params, request }: Route.LoaderArgs) {
|
|||||||
userId: user?.id,
|
userId: user?.id,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const isDocumentWaitingForSignatureFromOthers =
|
||||||
|
allRecipients.length > 1 &&
|
||||||
|
allRecipients.some(
|
||||||
|
(r) => r.role !== RecipientRole.CC && r.signingStatus !== SigningStatus.SIGNED,
|
||||||
|
);
|
||||||
|
|
||||||
if (!isDocumentAccessValid) {
|
if (!isDocumentAccessValid) {
|
||||||
return {
|
return {
|
||||||
isDocumentAccessValid: false,
|
isDocumentAccessValid: false,
|
||||||
recipientEmail: recipient.email,
|
recipientEmail: recipient.email,
|
||||||
|
isDocumentWaitingForSignatureFromOthers,
|
||||||
} as const;
|
} as const;
|
||||||
}
|
}
|
||||||
|
|
||||||
const signatures = await getRecipientSignatures({ recipientId: recipient.id });
|
const signatures = await getRecipientSignatures({ recipientId: recipient.id });
|
||||||
const isExistingUser = await getUserByEmail({ email: recipient.email })
|
const isExistingUser = recipient.email
|
||||||
.then((u) => !!u)
|
? await getUserByEmail({ email: recipient.email })
|
||||||
.catch(() => false);
|
.then((u) => !!u)
|
||||||
|
.catch(() => false)
|
||||||
|
: false;
|
||||||
|
|
||||||
const recipientName =
|
const recipientName =
|
||||||
recipient.name ||
|
recipient.name ||
|
||||||
@ -93,6 +110,7 @@ export async function loader({ params, request }: Route.LoaderArgs) {
|
|||||||
signatures,
|
signatures,
|
||||||
document,
|
document,
|
||||||
recipient,
|
recipient,
|
||||||
|
isDocumentWaitingForSignatureFromOthers,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -110,10 +128,11 @@ export default function CompletedSigningPage({ loaderData }: Route.ComponentProp
|
|||||||
document,
|
document,
|
||||||
recipient,
|
recipient,
|
||||||
recipientEmail,
|
recipientEmail,
|
||||||
|
isDocumentWaitingForSignatureFromOthers,
|
||||||
} = loaderData;
|
} = loaderData;
|
||||||
|
|
||||||
if (!isDocumentAccessValid) {
|
if (!isDocumentAccessValid) {
|
||||||
return <DocumentSigningAuthPageView email={recipientEmail} />;
|
return <DocumentSigningAuthPageView email={recipientEmail ?? ''} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -142,7 +161,7 @@ export default function CompletedSigningPage({ loaderData }: Route.ComponentProp
|
|||||||
|
|
||||||
{/* Card with recipient */}
|
{/* Card with recipient */}
|
||||||
<SigningCard3D
|
<SigningCard3D
|
||||||
name={recipientName}
|
name={recipientName ?? ''}
|
||||||
signature={signatures.at(0)}
|
signature={signatures.at(0)}
|
||||||
signingCelebrationImage={signingCelebration}
|
signingCelebrationImage={signingCelebration}
|
||||||
/>
|
/>
|
||||||
@ -153,7 +172,11 @@ export default function CompletedSigningPage({ loaderData }: Route.ComponentProp
|
|||||||
{recipient.role === RecipientRole.APPROVER && <Trans>Document Approved</Trans>}
|
{recipient.role === RecipientRole.APPROVER && <Trans>Document Approved</Trans>}
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
{match({ status: document.status, deletedAt: document.deletedAt })
|
{match({
|
||||||
|
status: document.status,
|
||||||
|
deletedAt: document.deletedAt,
|
||||||
|
waitingForOthers: isDocumentWaitingForSignatureFromOthers,
|
||||||
|
})
|
||||||
.with({ status: DocumentStatus.COMPLETED }, () => (
|
.with({ status: DocumentStatus.COMPLETED }, () => (
|
||||||
<div className="text-documenso-700 mt-4 flex items-center text-center">
|
<div className="text-documenso-700 mt-4 flex items-center text-center">
|
||||||
<CheckCircle2 className="mr-2 h-5 w-5" />
|
<CheckCircle2 className="mr-2 h-5 w-5" />
|
||||||
@ -162,7 +185,15 @@ export default function CompletedSigningPage({ loaderData }: Route.ComponentProp
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
))
|
))
|
||||||
.with({ deletedAt: null }, () => (
|
.with({ deletedAt: null, waitingForOthers: false }, () => (
|
||||||
|
<div className="mt-4 flex items-center text-center text-blue-600">
|
||||||
|
<Loader2 className="mr-2 h-5 w-5 animate-spin" />
|
||||||
|
<span className="text-sm">
|
||||||
|
<Trans>Processing document...</Trans>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
.with({ deletedAt: null, waitingForOthers: true }, () => (
|
||||||
<div className="mt-4 flex items-center text-center text-blue-600">
|
<div className="mt-4 flex items-center text-center text-blue-600">
|
||||||
<Clock8 className="mr-2 h-5 w-5" />
|
<Clock8 className="mr-2 h-5 w-5" />
|
||||||
<span className="text-sm">
|
<span className="text-sm">
|
||||||
@ -179,7 +210,11 @@ export default function CompletedSigningPage({ loaderData }: Route.ComponentProp
|
|||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
{match({ status: document.status, deletedAt: document.deletedAt })
|
{match({
|
||||||
|
status: document.status,
|
||||||
|
deletedAt: document.deletedAt,
|
||||||
|
waitingForOthers: isDocumentWaitingForSignatureFromOthers,
|
||||||
|
})
|
||||||
.with({ status: DocumentStatus.COMPLETED }, () => (
|
.with({ status: DocumentStatus.COMPLETED }, () => (
|
||||||
<p className="text-muted-foreground/60 mt-2.5 max-w-[60ch] text-center text-sm font-medium md:text-base">
|
<p className="text-muted-foreground/60 mt-2.5 max-w-[60ch] text-center text-sm font-medium md:text-base">
|
||||||
<Trans>
|
<Trans>
|
||||||
@ -187,7 +222,15 @@ export default function CompletedSigningPage({ loaderData }: Route.ComponentProp
|
|||||||
</Trans>
|
</Trans>
|
||||||
</p>
|
</p>
|
||||||
))
|
))
|
||||||
.with({ deletedAt: null }, () => (
|
.with({ deletedAt: null, waitingForOthers: false }, () => (
|
||||||
|
<p className="text-muted-foreground/60 mt-2.5 max-w-[60ch] text-center text-sm font-medium md:text-base">
|
||||||
|
<Trans>
|
||||||
|
All parties have completed their actions. The document is now being finalized. You
|
||||||
|
will receive an Email copy once it is ready.
|
||||||
|
</Trans>
|
||||||
|
</p>
|
||||||
|
))
|
||||||
|
.with({ deletedAt: null, waitingForOthers: true }, () => (
|
||||||
<p className="text-muted-foreground/60 mt-2.5 max-w-[60ch] text-center text-sm font-medium md:text-base">
|
<p className="text-muted-foreground/60 mt-2.5 max-w-[60ch] text-center text-sm font-medium md:text-base">
|
||||||
<Trans>
|
<Trans>
|
||||||
You will receive an Email copy of the signed document once everyone has signed.
|
You will receive an Email copy of the signed document once everyone has signed.
|
||||||
@ -244,7 +287,10 @@ export default function CompletedSigningPage({ loaderData }: Route.ComponentProp
|
|||||||
</Trans>
|
</Trans>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<ClaimAccount defaultName={recipientName} defaultEmail={recipient.email} />
|
<ClaimAccount
|
||||||
|
defaultName={recipientName ?? ''}
|
||||||
|
defaultEmail={recipient.email ?? ''}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@ -30,3 +30,26 @@ export const getRecipientsForDocument = async ({
|
|||||||
|
|
||||||
return recipients;
|
return recipients;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export interface GetAllRecipientsByDocumentIdOptions {
|
||||||
|
documentId: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getAllRecipientsByDocumentId = async ({
|
||||||
|
documentId,
|
||||||
|
}: GetAllRecipientsByDocumentIdOptions) => {
|
||||||
|
const recipients = await prisma.recipient.findMany({
|
||||||
|
where: {
|
||||||
|
documentId,
|
||||||
|
},
|
||||||
|
select: {
|
||||||
|
role: true,
|
||||||
|
signingStatus: true,
|
||||||
|
},
|
||||||
|
orderBy: {
|
||||||
|
id: 'asc',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return recipients;
|
||||||
|
};
|
||||||
|
|||||||
@ -1,8 +1,29 @@
|
|||||||
import type { Document } from '@prisma/client';
|
import type { Document } from '@prisma/client';
|
||||||
import { DocumentStatus } from '@prisma/client';
|
import { DocumentStatus, RecipientRole, SigningStatus } from '@prisma/client';
|
||||||
|
|
||||||
export const isDocumentCompleted = (document: Pick<Document, 'status'> | DocumentStatus) => {
|
export const isDocumentCompleted = (document: Pick<Document, 'status'> | DocumentStatus) => {
|
||||||
const status = typeof document === 'string' ? document : document.status;
|
const status = typeof document === 'string' ? document : document.status;
|
||||||
|
|
||||||
return status === DocumentStatus.COMPLETED || status === DocumentStatus.REJECTED;
|
return status === DocumentStatus.COMPLETED || status === DocumentStatus.REJECTED;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const isDocumentBeingProcessed = (document: {
|
||||||
|
status: DocumentStatus;
|
||||||
|
deletedAt: Date | null;
|
||||||
|
recipients: Array<{
|
||||||
|
role: RecipientRole;
|
||||||
|
signingStatus: SigningStatus;
|
||||||
|
}>;
|
||||||
|
}) => {
|
||||||
|
if (document.status !== DocumentStatus.PENDING || document.deletedAt !== null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const recipients = document.recipients.filter((r) => r.role !== RecipientRole.CC);
|
||||||
|
|
||||||
|
if (recipients.length === 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return recipients.every((r) => r.signingStatus === SigningStatus.SIGNED);
|
||||||
|
};
|
||||||
|
|||||||
@ -16,7 +16,7 @@ export type DocumentDialogProps = {
|
|||||||
/**
|
/**
|
||||||
* A dialog which renders the provided document.
|
* A dialog which renders the provided document.
|
||||||
*/
|
*/
|
||||||
export default function DocumentDialog({ trigger, documentData, ...props }: DocumentDialogProps) {
|
export const DocumentDialog = ({ trigger, documentData, ...props }: DocumentDialogProps) => {
|
||||||
const [documentLoaded, setDocumentLoaded] = useState(false);
|
const [documentLoaded, setDocumentLoaded] = useState(false);
|
||||||
|
|
||||||
const onDocumentLoad = () => {
|
const onDocumentLoad = () => {
|
||||||
@ -58,4 +58,4 @@ export default function DocumentDialog({ trigger, documentData, ...props }: Docu
|
|||||||
</DialogPortal>
|
</DialogPortal>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user