mirror of
https://github.com/documenso/documenso.git
synced 2025-11-21 04:01:45 +10:00
Compare commits
1 Commits
release
...
perf/insig
| Author | SHA1 | Date | |
|---|---|---|---|
| e182d29f99 |
@ -23,10 +23,6 @@ NEXT_PRIVATE_OIDC_CLIENT_ID=""
|
|||||||
NEXT_PRIVATE_OIDC_CLIENT_SECRET=""
|
NEXT_PRIVATE_OIDC_CLIENT_SECRET=""
|
||||||
NEXT_PRIVATE_OIDC_PROVIDER_LABEL="OIDC"
|
NEXT_PRIVATE_OIDC_PROVIDER_LABEL="OIDC"
|
||||||
NEXT_PRIVATE_OIDC_SKIP_VERIFY=""
|
NEXT_PRIVATE_OIDC_SKIP_VERIFY=""
|
||||||
# Specifies the prompt to use for OIDC signin, explicitly setting
|
|
||||||
# an empty string will omit the prompt parameter.
|
|
||||||
# See: https://www.cerberauth.com/blog/openid-connect-oauth2-prompts/
|
|
||||||
NEXT_PRIVATE_OIDC_PROMPT="login"
|
|
||||||
|
|
||||||
# [[URLS]]
|
# [[URLS]]
|
||||||
NEXT_PUBLIC_WEBAPP_URL="http://localhost:3000"
|
NEXT_PUBLIC_WEBAPP_URL="http://localhost:3000"
|
||||||
|
|||||||
@ -2,7 +2,7 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|||||||
|
|
||||||
import { useLingui } from '@lingui/react';
|
import { useLingui } from '@lingui/react';
|
||||||
import { Trans } from '@lingui/react/macro';
|
import { Trans } from '@lingui/react/macro';
|
||||||
import type { EnvelopeItem, FieldType } from '@prisma/client';
|
import type { DocumentData, FieldType } from '@prisma/client';
|
||||||
import { ReadStatus, type Recipient, SendStatus, SigningStatus } from '@prisma/client';
|
import { ReadStatus, type Recipient, SendStatus, SigningStatus } from '@prisma/client';
|
||||||
import { base64 } from '@scure/base';
|
import { base64 } from '@scure/base';
|
||||||
import { ChevronsUpDown } from 'lucide-react';
|
import { ChevronsUpDown } from 'lucide-react';
|
||||||
@ -40,8 +40,7 @@ const DEFAULT_WIDTH_PX = MIN_WIDTH_PX * 2.5;
|
|||||||
|
|
||||||
export type ConfigureFieldsViewProps = {
|
export type ConfigureFieldsViewProps = {
|
||||||
configData: TConfigureEmbedFormSchema;
|
configData: TConfigureEmbedFormSchema;
|
||||||
presignToken?: string | undefined;
|
documentData?: DocumentData;
|
||||||
envelopeItem?: Pick<EnvelopeItem, 'id' | 'envelopeId'>;
|
|
||||||
defaultValues?: Partial<TConfigureFieldsFormSchema>;
|
defaultValues?: Partial<TConfigureFieldsFormSchema>;
|
||||||
onBack?: (data: TConfigureFieldsFormSchema) => void;
|
onBack?: (data: TConfigureFieldsFormSchema) => void;
|
||||||
onSubmit: (data: TConfigureFieldsFormSchema) => void;
|
onSubmit: (data: TConfigureFieldsFormSchema) => void;
|
||||||
@ -49,8 +48,7 @@ export type ConfigureFieldsViewProps = {
|
|||||||
|
|
||||||
export const ConfigureFieldsView = ({
|
export const ConfigureFieldsView = ({
|
||||||
configData,
|
configData,
|
||||||
presignToken,
|
documentData,
|
||||||
envelopeItem,
|
|
||||||
defaultValues,
|
defaultValues,
|
||||||
onBack,
|
onBack,
|
||||||
onSubmit,
|
onSubmit,
|
||||||
@ -84,25 +82,17 @@ export const ConfigureFieldsView = ({
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const normalizedDocumentData = useMemo(() => {
|
const normalizedDocumentData = useMemo(() => {
|
||||||
if (envelopeItem) {
|
if (documentData) {
|
||||||
return undefined;
|
return documentData.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!configData.documentData) {
|
if (!configData.documentData) {
|
||||||
return undefined;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return base64.encode(configData.documentData.data);
|
return base64.encode(configData.documentData.data);
|
||||||
}, [configData.documentData]);
|
}, [configData.documentData]);
|
||||||
|
|
||||||
const normalizedEnvelopeItem = useMemo(() => {
|
|
||||||
if (envelopeItem) {
|
|
||||||
return envelopeItem;
|
|
||||||
}
|
|
||||||
|
|
||||||
return { id: '', envelopeId: '' };
|
|
||||||
}, [envelopeItem]);
|
|
||||||
|
|
||||||
const recipients = useMemo(() => {
|
const recipients = useMemo(() => {
|
||||||
return configData.signers.map<Recipient>((signer, index) => ({
|
return configData.signers.map<Recipient>((signer, index) => ({
|
||||||
id: signer.nativeId || index,
|
id: signer.nativeId || index,
|
||||||
@ -544,50 +534,56 @@ export const ConfigureFieldsView = ({
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<Form {...form}>
|
<Form {...form}>
|
||||||
<div>
|
{normalizedDocumentData && (
|
||||||
<PDFViewer
|
<div>
|
||||||
presignToken={presignToken}
|
<PDFViewer
|
||||||
overrideData={normalizedDocumentData}
|
overrideData={normalizedDocumentData}
|
||||||
envelopeItem={normalizedEnvelopeItem}
|
envelopeItem={{
|
||||||
token={undefined}
|
id: '',
|
||||||
version="signed"
|
envelopeId: '',
|
||||||
/>
|
}}
|
||||||
|
token={undefined}
|
||||||
|
version="signed"
|
||||||
|
/>
|
||||||
|
|
||||||
<ElementVisible
|
<ElementVisible
|
||||||
target={`${PDF_VIEWER_PAGE_SELECTOR}[data-page-number="${highestPageNumber}"]`}
|
target={`${PDF_VIEWER_PAGE_SELECTOR}[data-page-number="${highestPageNumber}"]`}
|
||||||
>
|
>
|
||||||
{localFields.map((field, index) => {
|
{localFields.map((field, index) => {
|
||||||
const recipientIndex = recipients.findIndex((r) => r.id === field.recipientId);
|
const recipientIndex = recipients.findIndex(
|
||||||
|
(r) => r.id === field.recipientId,
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FieldItem
|
<FieldItem
|
||||||
key={field.formId}
|
key={field.formId}
|
||||||
field={field}
|
field={field}
|
||||||
minHeight={MIN_HEIGHT_PX}
|
minHeight={MIN_HEIGHT_PX}
|
||||||
minWidth={MIN_WIDTH_PX}
|
minWidth={MIN_WIDTH_PX}
|
||||||
defaultHeight={DEFAULT_HEIGHT_PX}
|
defaultHeight={DEFAULT_HEIGHT_PX}
|
||||||
defaultWidth={DEFAULT_WIDTH_PX}
|
defaultWidth={DEFAULT_WIDTH_PX}
|
||||||
onResize={(node) => onFieldResize(node, index)}
|
onResize={(node) => onFieldResize(node, index)}
|
||||||
onMove={(node) => onFieldMove(node, index)}
|
onMove={(node) => onFieldMove(node, index)}
|
||||||
onRemove={() => remove(index)}
|
onRemove={() => remove(index)}
|
||||||
onDuplicate={() => onFieldCopy(null, { duplicate: true })}
|
onDuplicate={() => onFieldCopy(null, { duplicate: true })}
|
||||||
onDuplicateAllPages={() => onFieldCopy(null, { duplicateAll: true })}
|
onDuplicateAllPages={() => onFieldCopy(null, { duplicateAll: true })}
|
||||||
onFocus={() => setLastActiveField(field)}
|
onFocus={() => setLastActiveField(field)}
|
||||||
onBlur={() => setLastActiveField(null)}
|
onBlur={() => setLastActiveField(null)}
|
||||||
onAdvancedSettings={() => {
|
onAdvancedSettings={() => {
|
||||||
setCurrentField(field);
|
setCurrentField(field);
|
||||||
setShowAdvancedSettings(true);
|
setShowAdvancedSettings(true);
|
||||||
}}
|
}}
|
||||||
recipientIndex={recipientIndex}
|
recipientIndex={recipientIndex}
|
||||||
active={activeFieldId === field.formId}
|
active={activeFieldId === field.formId}
|
||||||
onFieldActivate={() => setActiveFieldId(field.formId)}
|
onFieldActivate={() => setActiveFieldId(field.formId)}
|
||||||
onFieldDeactivate={() => setActiveFieldId(null)}
|
onFieldDeactivate={() => setActiveFieldId(null)}
|
||||||
disabled={selectedRecipient?.id !== field.recipientId}
|
disabled={selectedRecipient?.id !== field.recipientId}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</ElementVisible>
|
</ElementVisible>
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
</Form>
|
</Form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -1,8 +1,10 @@
|
|||||||
|
import { useEffect } from 'react';
|
||||||
|
|
||||||
import { useLingui } from '@lingui/react';
|
import { useLingui } from '@lingui/react';
|
||||||
import { Trans } from '@lingui/react/macro';
|
import { Trans } from '@lingui/react/macro';
|
||||||
import { DocumentStatus, FieldType, RecipientRole } from '@prisma/client';
|
import { DocumentStatus, FieldType, RecipientRole } from '@prisma/client';
|
||||||
import { CheckCircle2, Clock8, DownloadIcon, Loader2 } from 'lucide-react';
|
import { CheckCircle2, Clock8, DownloadIcon } from 'lucide-react';
|
||||||
import { Link } from 'react-router';
|
import { Link, useRevalidator } from 'react-router';
|
||||||
import { match } from 'ts-pattern';
|
import { match } from 'ts-pattern';
|
||||||
|
|
||||||
import signingCelebration from '@documenso/assets/images/signing-celebration.png';
|
import signingCelebration from '@documenso/assets/images/signing-celebration.png';
|
||||||
@ -16,7 +18,7 @@ import { getRecipientSignatures } from '@documenso/lib/server-only/recipient/get
|
|||||||
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 { trpc } from '@documenso/trpc/react';
|
import type { Document } from '@documenso/prisma/types/document-legacy-schema';
|
||||||
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';
|
||||||
import { cn } from '@documenso/ui/lib/utils';
|
import { cn } from '@documenso/ui/lib/utils';
|
||||||
@ -82,13 +84,6 @@ export async function loader({ params, request }: Route.LoaderArgs) {
|
|||||||
|
|
||||||
const canSignUp = !isExistingUser && env('NEXT_PUBLIC_DISABLE_SIGNUP') !== 'true';
|
const canSignUp = !isExistingUser && env('NEXT_PUBLIC_DISABLE_SIGNUP') !== 'true';
|
||||||
|
|
||||||
const canRedirectToFolder =
|
|
||||||
user && document.userId === user.id && document.folderId && document.team?.url;
|
|
||||||
|
|
||||||
const returnToHomePath = canRedirectToFolder
|
|
||||||
? `/t/${document.team.url}/documents/f/${document.folderId}`
|
|
||||||
: '/';
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
isDocumentAccessValid: true,
|
isDocumentAccessValid: true,
|
||||||
canSignUp,
|
canSignUp,
|
||||||
@ -97,7 +92,6 @@ export async function loader({ params, request }: Route.LoaderArgs) {
|
|||||||
signatures,
|
signatures,
|
||||||
document,
|
document,
|
||||||
recipient,
|
recipient,
|
||||||
returnToHomePath,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -115,27 +109,8 @@ export default function CompletedSigningPage({ loaderData }: Route.ComponentProp
|
|||||||
document,
|
document,
|
||||||
recipient,
|
recipient,
|
||||||
recipientEmail,
|
recipientEmail,
|
||||||
returnToHomePath,
|
|
||||||
} = loaderData;
|
} = loaderData;
|
||||||
|
|
||||||
// Poll signing status every few seconds
|
|
||||||
const { data: signingStatusData } = trpc.envelope.signingStatus.useQuery(
|
|
||||||
{
|
|
||||||
token: recipient?.token || '',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
refetchInterval: 3000,
|
|
||||||
initialData: match(document?.status)
|
|
||||||
.with(DocumentStatus.COMPLETED, () => ({ status: 'COMPLETED' }) as const)
|
|
||||||
.with(DocumentStatus.REJECTED, () => ({ status: 'REJECTED' }) as const)
|
|
||||||
.with(DocumentStatus.PENDING, () => ({ status: 'PENDING' }) as const)
|
|
||||||
.otherwise(() => ({ status: 'PENDING' }) as const),
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
// Use signing status from query if available, otherwise fall back to document status
|
|
||||||
const signingStatus = signingStatusData?.status ?? 'PENDING';
|
|
||||||
|
|
||||||
if (!isDocumentAccessValid) {
|
if (!isDocumentAccessValid) {
|
||||||
return <DocumentSigningAuthPageView email={recipientEmail} />;
|
return <DocumentSigningAuthPageView email={recipientEmail} />;
|
||||||
}
|
}
|
||||||
@ -143,7 +118,7 @@ export default function CompletedSigningPage({ loaderData }: Route.ComponentProp
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
'-mx-4 flex flex-col items-center overflow-hidden px-4 pt-16 md:-mx-8 md:px-8 lg:pt-20 xl:pt-28',
|
'-mx-4 flex flex-col items-center overflow-hidden px-4 pt-24 md:-mx-8 md:px-8 lg:pt-36 xl:pt-44',
|
||||||
{ 'pt-0 lg:pt-0 xl:pt-0': canSignUp },
|
{ 'pt-0 lg:pt-0 xl:pt-0': canSignUp },
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
@ -177,8 +152,8 @@ 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: signingStatus, deletedAt: document.deletedAt })
|
{match({ status: document.status, deletedAt: document.deletedAt })
|
||||||
.with({ status: '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" />
|
||||||
<span className="text-sm">
|
<span className="text-sm">
|
||||||
@ -186,14 +161,6 @@ export default function CompletedSigningPage({ loaderData }: Route.ComponentProp
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
))
|
))
|
||||||
.with({ status: 'PROCESSING' }, () => (
|
|
||||||
<div className="mt-4 flex items-center text-center text-orange-600">
|
|
||||||
<Loader2 className="mr-2 h-5 w-5 animate-spin" />
|
|
||||||
<span className="text-sm">
|
|
||||||
<Trans>Processing document</Trans>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
))
|
|
||||||
.with({ deletedAt: null }, () => (
|
.with({ deletedAt: null }, () => (
|
||||||
<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" />
|
||||||
@ -211,22 +178,14 @@ export default function CompletedSigningPage({ loaderData }: Route.ComponentProp
|
|||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
{match({ status: signingStatus, deletedAt: document.deletedAt })
|
{match({ status: document.status, deletedAt: document.deletedAt })
|
||||||
.with({ status: '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>
|
||||||
Everyone has signed! You will receive an Email copy of the signed document.
|
Everyone has signed! You will receive an Email copy of the signed document.
|
||||||
</Trans>
|
</Trans>
|
||||||
</p>
|
</p>
|
||||||
))
|
))
|
||||||
.with({ status: 'PROCESSING' }, () => (
|
|
||||||
<p className="text-muted-foreground/60 mt-2.5 max-w-[60ch] text-center text-sm font-medium md:text-base">
|
|
||||||
<Trans>
|
|
||||||
All recipients have signed. The document is being processed and you will receive
|
|
||||||
an Email copy shortly.
|
|
||||||
</Trans>
|
|
||||||
</p>
|
|
||||||
))
|
|
||||||
.with({ deletedAt: null }, () => (
|
.with({ deletedAt: null }, () => (
|
||||||
<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>
|
||||||
@ -243,35 +202,23 @@ export default function CompletedSigningPage({ loaderData }: Route.ComponentProp
|
|||||||
</p>
|
</p>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
<div className="mt-8 flex w-full max-w-xs flex-col items-stretch gap-4 md:w-auto md:max-w-none md:flex-row md:items-center">
|
<div className="mt-8 flex w-full max-w-sm items-center justify-center gap-4">
|
||||||
<DocumentShareButton
|
<DocumentShareButton documentId={document.id} token={recipient.token} />
|
||||||
documentId={document.id}
|
|
||||||
token={recipient.token}
|
|
||||||
className="w-full max-w-none md:flex-1"
|
|
||||||
/>
|
|
||||||
|
|
||||||
{isDocumentCompleted(document) && (
|
{isDocumentCompleted(document.status) && (
|
||||||
<EnvelopeDownloadDialog
|
<EnvelopeDownloadDialog
|
||||||
envelopeId={document.envelopeId}
|
envelopeId={document.envelopeId}
|
||||||
envelopeStatus={document.status}
|
envelopeStatus={document.status}
|
||||||
envelopeItems={document.envelopeItems}
|
envelopeItems={document.envelopeItems}
|
||||||
token={recipient?.token}
|
token={recipient?.token}
|
||||||
trigger={
|
trigger={
|
||||||
<Button type="button" variant="outline" className="flex-1 md:flex-initial">
|
<Button type="button" variant="outline" className="flex-1">
|
||||||
<DownloadIcon className="mr-2 h-5 w-5" />
|
<DownloadIcon className="mr-2 h-5 w-5" />
|
||||||
<Trans>Download</Trans>
|
<Trans>Download</Trans>
|
||||||
</Button>
|
</Button>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{user && (
|
|
||||||
<Button asChild>
|
|
||||||
<Link to={returnToHomePath}>
|
|
||||||
<Trans>Go Back Home</Trans>
|
|
||||||
</Link>
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -291,8 +238,41 @@ export default function CompletedSigningPage({ loaderData }: Route.ComponentProp
|
|||||||
<ClaimAccount defaultName={recipientName} defaultEmail={recipient.email} />
|
<ClaimAccount defaultName={recipientName} defaultEmail={recipient.email} />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{user && (
|
||||||
|
<Link to="/" className="text-documenso-700 hover:text-documenso-600 mt-2">
|
||||||
|
<Trans>Go Back Home</Trans>
|
||||||
|
</Link>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<PollUntilDocumentCompleted document={document} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type PollUntilDocumentCompletedProps = {
|
||||||
|
document: Pick<Document, 'id' | 'status' | 'deletedAt'>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const PollUntilDocumentCompleted = ({ document }: PollUntilDocumentCompletedProps) => {
|
||||||
|
const { revalidate } = useRevalidator();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isDocumentCompleted(document.status)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const interval = setInterval(() => {
|
||||||
|
if (window.document.hasFocus()) {
|
||||||
|
void revalidate();
|
||||||
|
}
|
||||||
|
}, 5000);
|
||||||
|
|
||||||
|
return () => clearInterval(interval);
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [document.status]);
|
||||||
|
|
||||||
|
return <></>;
|
||||||
|
};
|
||||||
|
|||||||
@ -75,7 +75,6 @@ export const loader = async ({ request, params }: Route.LoaderArgs) => {
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
return {
|
return {
|
||||||
token,
|
|
||||||
document: {
|
document: {
|
||||||
...document,
|
...document,
|
||||||
fields,
|
fields,
|
||||||
@ -87,7 +86,7 @@ export default function EmbeddingAuthoringDocumentEditPage() {
|
|||||||
const { _ } = useLingui();
|
const { _ } = useLingui();
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
|
|
||||||
const { document, token } = useLoaderData<typeof loader>();
|
const { document } = useLoaderData<typeof loader>();
|
||||||
|
|
||||||
const [hasFinishedInit, setHasFinishedInit] = useState(false);
|
const [hasFinishedInit, setHasFinishedInit] = useState(false);
|
||||||
|
|
||||||
@ -322,8 +321,7 @@ export default function EmbeddingAuthoringDocumentEditPage() {
|
|||||||
|
|
||||||
<ConfigureFieldsView
|
<ConfigureFieldsView
|
||||||
configData={configuration!}
|
configData={configuration!}
|
||||||
presignToken={token}
|
documentData={document.documentData}
|
||||||
envelopeItem={document.envelopeItems[0]}
|
|
||||||
defaultValues={fields ?? undefined}
|
defaultValues={fields ?? undefined}
|
||||||
onBack={canGoBack ? handleBackToConfig : undefined}
|
onBack={canGoBack ? handleBackToConfig : undefined}
|
||||||
onSubmit={handleConfigureFieldsSubmit}
|
onSubmit={handleConfigureFieldsSubmit}
|
||||||
|
|||||||
@ -75,7 +75,6 @@ export const loader = async ({ request, params }: Route.LoaderArgs) => {
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
return {
|
return {
|
||||||
token,
|
|
||||||
template: {
|
template: {
|
||||||
...template,
|
...template,
|
||||||
fields,
|
fields,
|
||||||
@ -87,7 +86,7 @@ export default function EmbeddingAuthoringTemplateEditPage() {
|
|||||||
const { _ } = useLingui();
|
const { _ } = useLingui();
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
|
|
||||||
const { template, token } = useLoaderData<typeof loader>();
|
const { template } = useLoaderData<typeof loader>();
|
||||||
|
|
||||||
const [hasFinishedInit, setHasFinishedInit] = useState(false);
|
const [hasFinishedInit, setHasFinishedInit] = useState(false);
|
||||||
|
|
||||||
@ -322,8 +321,7 @@ export default function EmbeddingAuthoringTemplateEditPage() {
|
|||||||
|
|
||||||
<ConfigureFieldsView
|
<ConfigureFieldsView
|
||||||
configData={configuration!}
|
configData={configuration!}
|
||||||
presignToken={token}
|
documentData={template.templateDocumentData}
|
||||||
envelopeItem={template.envelopeItems[0]}
|
|
||||||
defaultValues={fields ?? undefined}
|
defaultValues={fields ?? undefined}
|
||||||
onBack={canGoBack ? handleBackToConfig : undefined}
|
onBack={canGoBack ? handleBackToConfig : undefined}
|
||||||
onSubmit={handleConfigureFieldsSubmit}
|
onSubmit={handleConfigureFieldsSubmit}
|
||||||
|
|||||||
@ -106,5 +106,5 @@
|
|||||||
"vite-plugin-babel-macros": "^1.0.6",
|
"vite-plugin-babel-macros": "^1.0.6",
|
||||||
"vite-tsconfig-paths": "^5.1.4"
|
"vite-tsconfig-paths": "^5.1.4"
|
||||||
},
|
},
|
||||||
"version": "2.0.14"
|
"version": "2.0.13"
|
||||||
}
|
}
|
||||||
|
|||||||
@ -5,7 +5,6 @@ import { Hono } from 'hono';
|
|||||||
import { getOptionalSession } from '@documenso/auth/server/lib/utils/get-session';
|
import { getOptionalSession } from '@documenso/auth/server/lib/utils/get-session';
|
||||||
import { APP_DOCUMENT_UPLOAD_SIZE_LIMIT } from '@documenso/lib/constants/app';
|
import { APP_DOCUMENT_UPLOAD_SIZE_LIMIT } from '@documenso/lib/constants/app';
|
||||||
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
||||||
import { verifyEmbeddingPresignToken } from '@documenso/lib/server-only/embedding-presign/verify-embedding-presign-token';
|
|
||||||
import { getTeamById } from '@documenso/lib/server-only/team/get-team';
|
import { getTeamById } from '@documenso/lib/server-only/team/get-team';
|
||||||
import { putNormalizedPdfFileServerSide } from '@documenso/lib/universal/upload/put-file.server';
|
import { putNormalizedPdfFileServerSide } from '@documenso/lib/universal/upload/put-file.server';
|
||||||
import { getPresignPostUrl } from '@documenso/lib/universal/upload/server-actions';
|
import { getPresignPostUrl } from '@documenso/lib/universal/upload/server-actions';
|
||||||
@ -17,7 +16,6 @@ import {
|
|||||||
type TGetPresignedPostUrlResponse,
|
type TGetPresignedPostUrlResponse,
|
||||||
ZGetEnvelopeItemFileDownloadRequestParamsSchema,
|
ZGetEnvelopeItemFileDownloadRequestParamsSchema,
|
||||||
ZGetEnvelopeItemFileRequestParamsSchema,
|
ZGetEnvelopeItemFileRequestParamsSchema,
|
||||||
ZGetEnvelopeItemFileRequestQuerySchema,
|
|
||||||
ZGetEnvelopeItemFileTokenDownloadRequestParamsSchema,
|
ZGetEnvelopeItemFileTokenDownloadRequestParamsSchema,
|
||||||
ZGetEnvelopeItemFileTokenRequestParamsSchema,
|
ZGetEnvelopeItemFileTokenRequestParamsSchema,
|
||||||
ZGetPresignedPostUrlRequestSchema,
|
ZGetPresignedPostUrlRequestSchema,
|
||||||
@ -70,24 +68,12 @@ export const filesRoute = new Hono<HonoEnv>()
|
|||||||
.get(
|
.get(
|
||||||
'/envelope/:envelopeId/envelopeItem/:envelopeItemId',
|
'/envelope/:envelopeId/envelopeItem/:envelopeItemId',
|
||||||
sValidator('param', ZGetEnvelopeItemFileRequestParamsSchema),
|
sValidator('param', ZGetEnvelopeItemFileRequestParamsSchema),
|
||||||
sValidator('query', ZGetEnvelopeItemFileRequestQuerySchema),
|
|
||||||
async (c) => {
|
async (c) => {
|
||||||
const { envelopeId, envelopeItemId } = c.req.valid('param');
|
const { envelopeId, envelopeItemId } = c.req.valid('param');
|
||||||
const { token } = c.req.query();
|
|
||||||
|
|
||||||
const session = await getOptionalSession(c);
|
const session = await getOptionalSession(c);
|
||||||
|
|
||||||
let userId = session.user?.id;
|
if (!session.user) {
|
||||||
|
|
||||||
if (token) {
|
|
||||||
const presignToken = await verifyEmbeddingPresignToken({
|
|
||||||
token,
|
|
||||||
}).catch(() => undefined);
|
|
||||||
|
|
||||||
userId = presignToken?.userId;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!userId) {
|
|
||||||
return c.json({ error: 'Unauthorized' }, 401);
|
return c.json({ error: 'Unauthorized' }, 401);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -118,7 +104,7 @@ export const filesRoute = new Hono<HonoEnv>()
|
|||||||
}
|
}
|
||||||
|
|
||||||
const team = await getTeamById({
|
const team = await getTeamById({
|
||||||
userId: userId,
|
userId: session.user.id,
|
||||||
teamId: envelope.teamId,
|
teamId: envelope.teamId,
|
||||||
}).catch((error) => {
|
}).catch((error) => {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
|
|||||||
@ -36,14 +36,6 @@ export type TGetEnvelopeItemFileRequestParams = z.infer<
|
|||||||
typeof ZGetEnvelopeItemFileRequestParamsSchema
|
typeof ZGetEnvelopeItemFileRequestParamsSchema
|
||||||
>;
|
>;
|
||||||
|
|
||||||
export const ZGetEnvelopeItemFileRequestQuerySchema = z.object({
|
|
||||||
token: z.string().optional(),
|
|
||||||
});
|
|
||||||
|
|
||||||
export type TGetEnvelopeItemFileRequestQuery = z.infer<
|
|
||||||
typeof ZGetEnvelopeItemFileRequestQuerySchema
|
|
||||||
>;
|
|
||||||
|
|
||||||
export const ZGetEnvelopeItemFileTokenRequestParamsSchema = z.object({
|
export const ZGetEnvelopeItemFileTokenRequestParamsSchema = z.object({
|
||||||
token: z.string().min(1),
|
token: z.string().min(1),
|
||||||
envelopeItemId: z.string().min(1),
|
envelopeItemId: z.string().min(1),
|
||||||
|
|||||||
6
package-lock.json
generated
6
package-lock.json
generated
@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "@documenso/root",
|
"name": "@documenso/root",
|
||||||
"version": "2.0.14",
|
"version": "2.0.13",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "@documenso/root",
|
"name": "@documenso/root",
|
||||||
"version": "2.0.14",
|
"version": "2.0.13",
|
||||||
"workspaces": [
|
"workspaces": [
|
||||||
"apps/*",
|
"apps/*",
|
||||||
"packages/*"
|
"packages/*"
|
||||||
@ -101,7 +101,7 @@
|
|||||||
},
|
},
|
||||||
"apps/remix": {
|
"apps/remix": {
|
||||||
"name": "@documenso/remix",
|
"name": "@documenso/remix",
|
||||||
"version": "2.0.14",
|
"version": "2.0.13",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@cantoo/pdf-lib": "^2.5.2",
|
"@cantoo/pdf-lib": "^2.5.2",
|
||||||
"@documenso/api": "*",
|
"@documenso/api": "*",
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "2.0.14",
|
"version": "2.0.13",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "turbo run build",
|
"build": "turbo run build",
|
||||||
"dev": "turbo run dev --filter=@documenso/remix",
|
"dev": "turbo run dev --filter=@documenso/remix",
|
||||||
|
|||||||
@ -27,13 +27,13 @@ type HandleOAuthAuthorizeUrlOptions = {
|
|||||||
/**
|
/**
|
||||||
* Optional prompt to pass to the authorization endpoint.
|
* Optional prompt to pass to the authorization endpoint.
|
||||||
*/
|
*/
|
||||||
prompt?: 'none' | 'login' | 'consent' | 'select_account';
|
prompt?: 'login' | 'consent' | 'select_account';
|
||||||
};
|
};
|
||||||
|
|
||||||
const oauthCookieMaxAge = 60 * 10; // 10 minutes.
|
const oauthCookieMaxAge = 60 * 10; // 10 minutes.
|
||||||
|
|
||||||
export const handleOAuthAuthorizeUrl = async (options: HandleOAuthAuthorizeUrlOptions) => {
|
export const handleOAuthAuthorizeUrl = async (options: HandleOAuthAuthorizeUrlOptions) => {
|
||||||
const { c, clientOptions, redirectPath } = options;
|
const { c, clientOptions, redirectPath, prompt = 'login' } = options;
|
||||||
|
|
||||||
if (!clientOptions.clientId || !clientOptions.clientSecret) {
|
if (!clientOptions.clientId || !clientOptions.clientSecret) {
|
||||||
throw new AppError(AppErrorCode.NOT_SETUP);
|
throw new AppError(AppErrorCode.NOT_SETUP);
|
||||||
@ -63,11 +63,7 @@ export const handleOAuthAuthorizeUrl = async (options: HandleOAuthAuthorizeUrlOp
|
|||||||
);
|
);
|
||||||
|
|
||||||
// Pass the prompt to the authorization endpoint.
|
// Pass the prompt to the authorization endpoint.
|
||||||
if (process.env.NEXT_PRIVATE_OIDC_PROMPT !== '') {
|
url.searchParams.append('prompt', prompt);
|
||||||
const prompt = process.env.NEXT_PRIVATE_OIDC_PROMPT ?? 'login';
|
|
||||||
|
|
||||||
url.searchParams.append('prompt', prompt);
|
|
||||||
}
|
|
||||||
|
|
||||||
setCookie(c, `${clientOptions.id}_oauth_state`, state, {
|
setCookie(c, `${clientOptions.id}_oauth_state`, state, {
|
||||||
...sessionCookieOptions,
|
...sessionCookieOptions,
|
||||||
|
|||||||
@ -7,7 +7,6 @@ export const SUPPORTED_LANGUAGE_CODES = [
|
|||||||
'es',
|
'es',
|
||||||
'it',
|
'it',
|
||||||
'pl',
|
'pl',
|
||||||
'pt-BR',
|
|
||||||
'ja',
|
'ja',
|
||||||
'ko',
|
'ko',
|
||||||
'zh',
|
'zh',
|
||||||
@ -65,10 +64,6 @@ export const SUPPORTED_LANGUAGES: Record<string, SupportedLanguage> = {
|
|||||||
short: 'pl',
|
short: 'pl',
|
||||||
full: 'Polish',
|
full: 'Polish',
|
||||||
},
|
},
|
||||||
'pt-BR': {
|
|
||||||
short: 'pt-BR',
|
|
||||||
full: 'Portuguese (Brazil)',
|
|
||||||
},
|
|
||||||
ja: {
|
ja: {
|
||||||
short: 'ja',
|
short: 'ja',
|
||||||
full: 'Japanese',
|
full: 'Japanese',
|
||||||
|
|||||||
@ -25,6 +25,7 @@ import { signPdf } from '@documenso/signing';
|
|||||||
|
|
||||||
import { AppError, AppErrorCode } from '../../../errors/app-error';
|
import { AppError, AppErrorCode } from '../../../errors/app-error';
|
||||||
import { sendCompletedEmail } from '../../../server-only/document/send-completed-email';
|
import { sendCompletedEmail } from '../../../server-only/document/send-completed-email';
|
||||||
|
import PostHogServerClient from '../../../server-only/feature-flags/get-post-hog-server-client';
|
||||||
import { getAuditLogsPdf } from '../../../server-only/htmltopdf/get-audit-logs-pdf';
|
import { getAuditLogsPdf } from '../../../server-only/htmltopdf/get-audit-logs-pdf';
|
||||||
import { getCertificatePdf } from '../../../server-only/htmltopdf/get-certificate-pdf';
|
import { getCertificatePdf } from '../../../server-only/htmltopdf/get-certificate-pdf';
|
||||||
import { addRejectionStampToPdf } from '../../../server-only/pdf/add-rejection-stamp-to-pdf';
|
import { addRejectionStampToPdf } from '../../../server-only/pdf/add-rejection-stamp-to-pdf';
|
||||||
@ -61,120 +62,171 @@ export const run = async ({
|
|||||||
}) => {
|
}) => {
|
||||||
const { documentId, sendEmail = true, isResealing = false, requestMetadata } = payload;
|
const { documentId, sendEmail = true, isResealing = false, requestMetadata } = payload;
|
||||||
|
|
||||||
const { envelopeId, envelopeStatus, isRejected } = await io.runTask('seal-document', async () => {
|
const envelope = await prisma.envelope.findFirstOrThrow({
|
||||||
const envelope = await prisma.envelope.findFirstOrThrow({
|
where: {
|
||||||
where: {
|
type: EnvelopeType.DOCUMENT,
|
||||||
type: EnvelopeType.DOCUMENT,
|
secondaryId: mapDocumentIdToSecondaryId(documentId),
|
||||||
secondaryId: mapDocumentIdToSecondaryId(documentId),
|
},
|
||||||
},
|
include: {
|
||||||
include: {
|
documentMeta: true,
|
||||||
documentMeta: true,
|
recipients: true,
|
||||||
recipients: true,
|
envelopeItems: {
|
||||||
envelopeItems: {
|
include: {
|
||||||
include: {
|
documentData: true,
|
||||||
documentData: true,
|
field: {
|
||||||
field: {
|
include: {
|
||||||
include: {
|
signature: true,
|
||||||
signature: true,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (envelope.envelopeItems.length === 0) {
|
||||||
|
throw new Error('At least one envelope item required');
|
||||||
|
}
|
||||||
|
|
||||||
|
const settings = await getTeamSettings({
|
||||||
|
userId: envelope.userId,
|
||||||
|
teamId: envelope.teamId,
|
||||||
|
});
|
||||||
|
|
||||||
|
const isComplete =
|
||||||
|
envelope.recipients.some((recipient) => recipient.signingStatus === SigningStatus.REJECTED) ||
|
||||||
|
envelope.recipients.every((recipient) => recipient.signingStatus === SigningStatus.SIGNED);
|
||||||
|
|
||||||
|
if (!isComplete) {
|
||||||
|
throw new AppError(AppErrorCode.UNKNOWN_ERROR, {
|
||||||
|
message: 'Document is not complete',
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (envelope.envelopeItems.length === 0) {
|
// Seems silly but we need to do this in case the job is re-ran
|
||||||
throw new Error('At least one envelope item required');
|
// after it has already run through the update task further below.
|
||||||
}
|
// eslint-disable-next-line @typescript-eslint/require-await
|
||||||
|
const documentStatus = await io.runTask('get-document-status', async () => {
|
||||||
|
return envelope.status;
|
||||||
|
});
|
||||||
|
|
||||||
const settings = await getTeamSettings({
|
// This is the same case as above.
|
||||||
userId: envelope.userId,
|
let envelopeItems = await io.runTask(
|
||||||
teamId: envelope.teamId,
|
'get-document-data-id',
|
||||||
});
|
// eslint-disable-next-line @typescript-eslint/require-await
|
||||||
|
async () => {
|
||||||
const isComplete =
|
// eslint-disable-next-line unused-imports/no-unused-vars
|
||||||
envelope.recipients.some((recipient) => recipient.signingStatus === SigningStatus.REJECTED) ||
|
return envelope.envelopeItems.map(({ field, ...rest }) => ({
|
||||||
envelope.recipients.every((recipient) => recipient.signingStatus === SigningStatus.SIGNED);
|
...rest,
|
||||||
|
|
||||||
if (!isComplete) {
|
|
||||||
throw new AppError(AppErrorCode.UNKNOWN_ERROR, {
|
|
||||||
message: 'Document is not complete',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
let envelopeItems = envelope.envelopeItems;
|
|
||||||
|
|
||||||
if (envelopeItems.length < 1) {
|
|
||||||
throw new Error(`Document ${envelope.id} has no envelope items`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const recipients = await prisma.recipient.findMany({
|
|
||||||
where: {
|
|
||||||
envelopeId: envelope.id,
|
|
||||||
role: {
|
|
||||||
not: RecipientRole.CC,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// Determine if the document has been rejected by checking if any recipient has rejected it
|
|
||||||
const rejectedRecipient = recipients.find(
|
|
||||||
(recipient) => recipient.signingStatus === SigningStatus.REJECTED,
|
|
||||||
);
|
|
||||||
|
|
||||||
const isRejected = Boolean(rejectedRecipient);
|
|
||||||
|
|
||||||
// Get the rejection reason from the rejected recipient
|
|
||||||
const rejectionReason = rejectedRecipient?.rejectionReason ?? '';
|
|
||||||
|
|
||||||
const fields = await prisma.field.findMany({
|
|
||||||
where: {
|
|
||||||
envelopeId: envelope.id,
|
|
||||||
},
|
|
||||||
include: {
|
|
||||||
signature: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// Skip the field check if the document is rejected
|
|
||||||
if (!isRejected && fieldsContainUnsignedRequiredField(fields)) {
|
|
||||||
throw new Error(`Document ${envelope.id} has unsigned required fields`);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isResealing) {
|
|
||||||
// If we're resealing we want to use the initial data for the document
|
|
||||||
// so we aren't placing fields on top of eachother.
|
|
||||||
envelopeItems = envelopeItems.map((envelopeItem) => ({
|
|
||||||
...envelopeItem,
|
|
||||||
documentData: {
|
|
||||||
...envelopeItem.documentData,
|
|
||||||
data: envelopeItem.documentData.initialData,
|
|
||||||
},
|
|
||||||
}));
|
}));
|
||||||
}
|
},
|
||||||
|
);
|
||||||
|
|
||||||
if (!envelope.qrToken) {
|
if (envelopeItems.length < 1) {
|
||||||
await prisma.envelope.update({
|
throw new Error(`Document ${envelope.id} has no envelope items`);
|
||||||
where: {
|
}
|
||||||
id: envelope.id,
|
|
||||||
},
|
|
||||||
data: {
|
|
||||||
qrToken: prefixedId('qr'),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const legacyDocumentId = mapSecondaryIdToDocumentId(envelope.secondaryId);
|
const recipients = await prisma.recipient.findMany({
|
||||||
|
where: {
|
||||||
|
envelopeId: envelope.id,
|
||||||
|
role: {
|
||||||
|
not: RecipientRole.CC,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
const { certificateData, auditLogData } = await getCertificateAndAuditLogData({
|
// Determine if the document has been rejected by checking if any recipient has rejected it
|
||||||
legacyDocumentId,
|
const rejectedRecipient = recipients.find(
|
||||||
documentMeta: envelope.documentMeta,
|
(recipient) => recipient.signingStatus === SigningStatus.REJECTED,
|
||||||
settings,
|
);
|
||||||
|
|
||||||
|
const isRejected = Boolean(rejectedRecipient);
|
||||||
|
|
||||||
|
// Get the rejection reason from the rejected recipient
|
||||||
|
const rejectionReason = rejectedRecipient?.rejectionReason ?? '';
|
||||||
|
|
||||||
|
const fields = await prisma.field.findMany({
|
||||||
|
where: {
|
||||||
|
envelopeId: envelope.id,
|
||||||
|
},
|
||||||
|
include: {
|
||||||
|
signature: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Skip the field check if the document is rejected
|
||||||
|
if (!isRejected && fieldsContainUnsignedRequiredField(fields)) {
|
||||||
|
throw new Error(`Document ${envelope.id} has unsigned required fields`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isResealing) {
|
||||||
|
// If we're resealing we want to use the initial data for the document
|
||||||
|
// so we aren't placing fields on top of eachother.
|
||||||
|
envelopeItems = envelopeItems.map((envelopeItem) => ({
|
||||||
|
...envelopeItem,
|
||||||
|
documentData: {
|
||||||
|
...envelopeItem.documentData,
|
||||||
|
data: envelopeItem.documentData.initialData,
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!envelope.qrToken) {
|
||||||
|
await prisma.envelope.update({
|
||||||
|
where: {
|
||||||
|
id: envelope.id,
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
qrToken: prefixedId('qr'),
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const newDocumentData: Array<{ oldDocumentDataId: string; newDocumentDataId: string }> = [];
|
const legacyDocumentId = mapSecondaryIdToDocumentId(envelope.secondaryId);
|
||||||
|
|
||||||
for (const envelopeItem of envelopeItems) {
|
const { certificateData, auditLogData } = await getCertificateAndAuditLogData({
|
||||||
|
legacyDocumentId,
|
||||||
|
documentMeta: envelope.documentMeta,
|
||||||
|
settings,
|
||||||
|
});
|
||||||
|
|
||||||
|
// !: The commented out code is our desired implementation but we're seemingly
|
||||||
|
// !: running into issues with inngest parallelism in production.
|
||||||
|
// !: Until this is resolved we will do this sequentially which is slower but
|
||||||
|
// !: will actually work.
|
||||||
|
// const decoratePromises: Array<Promise<{ oldDocumentDataId: string; newDocumentDataId: string }>> =
|
||||||
|
// [];
|
||||||
|
|
||||||
|
// for (const envelopeItem of envelopeItems) {
|
||||||
|
// const task = io.runTask(`decorate-${envelopeItem.id}`, async () => {
|
||||||
|
// const envelopeItemFields = envelope.envelopeItems.find(
|
||||||
|
// (item) => item.id === envelopeItem.id,
|
||||||
|
// )?.field;
|
||||||
|
|
||||||
|
// if (!envelopeItemFields) {
|
||||||
|
// throw new Error(`Envelope item fields not found for envelope item ${envelopeItem.id}`);
|
||||||
|
// }
|
||||||
|
|
||||||
|
// return decorateAndSignPdf({
|
||||||
|
// envelope,
|
||||||
|
// envelopeItem,
|
||||||
|
// envelopeItemFields,
|
||||||
|
// isRejected,
|
||||||
|
// rejectionReason,
|
||||||
|
// certificateData,
|
||||||
|
// auditLogData,
|
||||||
|
// });
|
||||||
|
// });
|
||||||
|
|
||||||
|
// decoratePromises.push(task);
|
||||||
|
// }
|
||||||
|
|
||||||
|
// const newDocumentData = await Promise.all(decoratePromises);
|
||||||
|
|
||||||
|
// TODO: Remove once parallelization is working
|
||||||
|
const newDocumentData: Array<{ oldDocumentDataId: string; newDocumentDataId: string }> = [];
|
||||||
|
|
||||||
|
for (const envelopeItem of envelopeItems) {
|
||||||
|
const result = await io.runTask(`decorate-${envelopeItem.id}`, async () => {
|
||||||
const envelopeItemFields = envelope.envelopeItems.find(
|
const envelopeItemFields = envelope.envelopeItems.find(
|
||||||
(item) => item.id === envelopeItem.id,
|
(item) => item.id === envelopeItem.id,
|
||||||
)?.field;
|
)?.field;
|
||||||
@ -183,7 +235,7 @@ export const run = async ({
|
|||||||
throw new Error(`Envelope item fields not found for envelope item ${envelopeItem.id}`);
|
throw new Error(`Envelope item fields not found for envelope item ${envelopeItem.id}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = await decorateAndSignPdf({
|
return decorateAndSignPdf({
|
||||||
envelope,
|
envelope,
|
||||||
envelopeItem,
|
envelopeItem,
|
||||||
envelopeItemFields,
|
envelopeItemFields,
|
||||||
@ -192,10 +244,25 @@ export const run = async ({
|
|||||||
certificateData,
|
certificateData,
|
||||||
auditLogData,
|
auditLogData,
|
||||||
});
|
});
|
||||||
|
});
|
||||||
|
|
||||||
newDocumentData.push(result);
|
newDocumentData.push(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const postHog = PostHogServerClient();
|
||||||
|
|
||||||
|
if (postHog) {
|
||||||
|
postHog.capture({
|
||||||
|
distinctId: nanoid(),
|
||||||
|
event: 'App: Document Sealed',
|
||||||
|
properties: {
|
||||||
|
documentId: envelope.id,
|
||||||
|
isRejected,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
await io.runTask('update-document', async () => {
|
||||||
await prisma.$transaction(async (tx) => {
|
await prisma.$transaction(async (tx) => {
|
||||||
for (const { oldDocumentDataId, newDocumentDataId } of newDocumentData) {
|
for (const { oldDocumentDataId, newDocumentDataId } of newDocumentData) {
|
||||||
const newData = await tx.documentData.findFirstOrThrow({
|
const newData = await tx.documentData.findFirstOrThrow({
|
||||||
@ -237,24 +304,18 @@ export const run = async ({
|
|||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
|
||||||
envelopeId: envelope.id,
|
|
||||||
envelopeStatus: envelope.status,
|
|
||||||
isRejected,
|
|
||||||
};
|
|
||||||
});
|
});
|
||||||
|
|
||||||
await io.runTask('send-completed-email', async () => {
|
await io.runTask('send-completed-email', async () => {
|
||||||
let shouldSendCompletedEmail = sendEmail && !isResealing && !isRejected;
|
let shouldSendCompletedEmail = sendEmail && !isResealing && !isRejected;
|
||||||
|
|
||||||
if (isResealing && !isDocumentCompleted(envelopeStatus)) {
|
if (isResealing && !isDocumentCompleted(envelope.status)) {
|
||||||
shouldSendCompletedEmail = sendEmail;
|
shouldSendCompletedEmail = sendEmail;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (shouldSendCompletedEmail) {
|
if (shouldSendCompletedEmail) {
|
||||||
await sendCompletedEmail({
|
await sendCompletedEmail({
|
||||||
id: { type: 'envelopeId', id: envelopeId },
|
id: { type: 'envelopeId', id: envelope.id },
|
||||||
requestMetadata,
|
requestMetadata,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -262,7 +323,7 @@ export const run = async ({
|
|||||||
|
|
||||||
const updatedEnvelope = await prisma.envelope.findFirstOrThrow({
|
const updatedEnvelope = await prisma.envelope.findFirstOrThrow({
|
||||||
where: {
|
where: {
|
||||||
id: envelopeId,
|
id: envelope.id,
|
||||||
},
|
},
|
||||||
include: {
|
include: {
|
||||||
documentMeta: true,
|
documentMeta: true,
|
||||||
|
|||||||
@ -116,28 +116,28 @@ async function getTeamInsights(
|
|||||||
): Promise<OrganisationDetailedInsights> {
|
): Promise<OrganisationDetailedInsights> {
|
||||||
const teamsQuery = kyselyPrisma.$kysely
|
const teamsQuery = kyselyPrisma.$kysely
|
||||||
.selectFrom('Team as t')
|
.selectFrom('Team as t')
|
||||||
.leftJoin('Envelope as e', (join) =>
|
|
||||||
join
|
|
||||||
.onRef('t.id', '=', 'e.teamId')
|
|
||||||
.on('e.deletedAt', 'is', null)
|
|
||||||
.on('e.type', '=', sql.lit(EnvelopeType.DOCUMENT)),
|
|
||||||
)
|
|
||||||
.leftJoin('TeamGroup as tg', 'tg.teamId', 't.id')
|
|
||||||
.leftJoin('OrganisationGroup as og', 'og.id', 'tg.organisationGroupId')
|
|
||||||
.leftJoin('OrganisationGroupMember as ogm', 'ogm.groupId', 'og.id')
|
|
||||||
.leftJoin('OrganisationMember as om', 'om.id', 'ogm.organisationMemberId')
|
|
||||||
.where('t.organisationId', '=', organisationId)
|
.where('t.organisationId', '=', organisationId)
|
||||||
.select([
|
.select((eb) => [
|
||||||
't.id as id',
|
't.id',
|
||||||
't.name as name',
|
't.name',
|
||||||
't.createdAt as createdAt',
|
't.createdAt',
|
||||||
sql<number>`COUNT(DISTINCT om."userId")`.as('memberCount'),
|
eb
|
||||||
(createdAtFrom
|
.selectFrom('TeamGroup as tg')
|
||||||
? sql<number>`COUNT(DISTINCT CASE WHEN e.id IS NOT NULL AND e."createdAt" >= ${createdAtFrom} THEN e.id END)`
|
.innerJoin('OrganisationGroup as og', 'og.id', 'tg.organisationGroupId')
|
||||||
: sql<number>`COUNT(DISTINCT e.id)`
|
.innerJoin('OrganisationGroupMember as ogm', 'ogm.groupId', 'og.id')
|
||||||
).as('documentCount'),
|
.innerJoin('OrganisationMember as om', 'om.id', 'ogm.organisationMemberId')
|
||||||
|
.whereRef('tg.teamId', '=', 't.id')
|
||||||
|
.select(sql<number>`count(distinct om."userId")`.as('count'))
|
||||||
|
.as('memberCount'),
|
||||||
|
eb
|
||||||
|
.selectFrom('Envelope as e')
|
||||||
|
.whereRef('e.teamId', '=', 't.id')
|
||||||
|
.where('e.deletedAt', 'is', null)
|
||||||
|
.where('e.type', '=', sql.lit(EnvelopeType.DOCUMENT))
|
||||||
|
.$if(!!createdAtFrom, (qb) => qb.where('e.createdAt', '>=', createdAtFrom!))
|
||||||
|
.select(sql<number>`count(e.id)`.as('count'))
|
||||||
|
.as('documentCount'),
|
||||||
])
|
])
|
||||||
.groupBy(['t.id', 't.name', 't.createdAt'])
|
|
||||||
.orderBy('documentCount', 'desc')
|
.orderBy('documentCount', 'desc')
|
||||||
.limit(perPage)
|
.limit(perPage)
|
||||||
.offset(offset);
|
.offset(offset);
|
||||||
@ -164,48 +164,38 @@ async function getUserInsights(
|
|||||||
perPage: number,
|
perPage: number,
|
||||||
createdAtFrom: Date | null,
|
createdAtFrom: Date | null,
|
||||||
): Promise<OrganisationDetailedInsights> {
|
): Promise<OrganisationDetailedInsights> {
|
||||||
const usersBase = kyselyPrisma.$kysely
|
const usersQuery = kyselyPrisma.$kysely
|
||||||
.selectFrom('OrganisationMember as om')
|
.selectFrom('OrganisationMember as om')
|
||||||
.innerJoin('User as u', 'u.id', 'om.userId')
|
.innerJoin('User as u', 'u.id', 'om.userId')
|
||||||
.where('om.organisationId', '=', organisationId)
|
.where('om.organisationId', '=', organisationId)
|
||||||
.leftJoin('Envelope as e', (join) =>
|
.select((eb) => [
|
||||||
join
|
'u.id',
|
||||||
.onRef('e.userId', '=', 'u.id')
|
'u.name',
|
||||||
.on('e.deletedAt', 'is', null)
|
'u.email',
|
||||||
.on('e.type', '=', sql.lit(EnvelopeType.DOCUMENT)),
|
'u.createdAt',
|
||||||
)
|
eb
|
||||||
.leftJoin('Team as td', (join) =>
|
.selectFrom('Envelope as e')
|
||||||
join.onRef('td.id', '=', 'e.teamId').on('td.organisationId', '=', organisationId),
|
.innerJoin('Team as t', 't.id', 'e.teamId')
|
||||||
)
|
.whereRef('e.userId', '=', 'u.id')
|
||||||
.leftJoin('Recipient as r', (join) =>
|
.where('t.organisationId', '=', organisationId)
|
||||||
join.onRef('r.email', '=', 'u.email').on('r.signedAt', 'is not', null),
|
.where('e.deletedAt', 'is', null)
|
||||||
)
|
.where('e.type', '=', sql.lit(EnvelopeType.DOCUMENT))
|
||||||
.leftJoin('Envelope as se', (join) =>
|
.$if(!!createdAtFrom, (qb) => qb.where('e.createdAt', '>=', createdAtFrom!))
|
||||||
join
|
.select(sql<number>`count(e.id)`.as('count'))
|
||||||
.onRef('se.id', '=', 'r.envelopeId')
|
.as('documentCount'),
|
||||||
.on('se.deletedAt', 'is', null)
|
eb
|
||||||
.on('se.type', '=', sql.lit(EnvelopeType.DOCUMENT)),
|
.selectFrom('Recipient as r')
|
||||||
)
|
.innerJoin('Envelope as e', 'e.id', 'r.envelopeId')
|
||||||
.leftJoin('Team as ts', (join) =>
|
.innerJoin('Team as t', 't.id', 'e.teamId')
|
||||||
join.onRef('ts.id', '=', 'se.teamId').on('ts.organisationId', '=', organisationId),
|
.whereRef('r.email', '=', 'u.email')
|
||||||
);
|
.where('r.signedAt', 'is not', null)
|
||||||
|
.where('t.organisationId', '=', organisationId)
|
||||||
const usersQuery = usersBase
|
.where('e.deletedAt', 'is', null)
|
||||||
.select([
|
.where('e.type', '=', sql.lit(EnvelopeType.DOCUMENT))
|
||||||
'u.id as id',
|
.$if(!!createdAtFrom, (qb) => qb.where('e.createdAt', '>=', createdAtFrom!))
|
||||||
'u.name as name',
|
.select(sql<number>`count(e.id)`.as('count'))
|
||||||
'u.email as email',
|
.as('signedDocumentCount'),
|
||||||
'u.createdAt as createdAt',
|
|
||||||
(createdAtFrom
|
|
||||||
? sql<number>`COUNT(DISTINCT CASE WHEN e.id IS NOT NULL AND td.id IS NOT NULL AND e."createdAt" >= ${createdAtFrom} THEN e.id END)`
|
|
||||||
: sql<number>`COUNT(DISTINCT CASE WHEN td.id IS NOT NULL THEN e.id END)`
|
|
||||||
).as('documentCount'),
|
|
||||||
(createdAtFrom
|
|
||||||
? sql<number>`COUNT(DISTINCT CASE WHEN e.id IS NOT NULL AND td.id IS NOT NULL AND e.status = 'COMPLETED' AND e."createdAt" >= ${createdAtFrom} THEN e.id END)`
|
|
||||||
: sql<number>`COUNT(DISTINCT CASE WHEN e.id IS NOT NULL AND td.id IS NOT NULL AND e.status = 'COMPLETED' THEN e.id END)`
|
|
||||||
).as('signedDocumentCount'),
|
|
||||||
])
|
])
|
||||||
.groupBy(['u.id', 'u.name', 'u.email', 'u.createdAt'])
|
|
||||||
.orderBy('u.createdAt', 'desc')
|
.orderBy('u.createdAt', 'desc')
|
||||||
.limit(perPage)
|
.limit(perPage)
|
||||||
.offset(offset);
|
.offset(offset);
|
||||||
@ -292,72 +282,51 @@ async function getOrganisationSummary(
|
|||||||
organisationId: string,
|
organisationId: string,
|
||||||
createdAtFrom: Date | null,
|
createdAtFrom: Date | null,
|
||||||
): Promise<OrganisationSummary> {
|
): Promise<OrganisationSummary> {
|
||||||
const summaryQuery = kyselyPrisma.$kysely
|
const teamCountQuery = kyselyPrisma.$kysely
|
||||||
.selectFrom('Organisation as o')
|
.selectFrom('Team')
|
||||||
.where('o.id', '=', organisationId)
|
.where('organisationId', '=', organisationId)
|
||||||
.select([
|
.select(sql<number>`count(id)`.as('count'))
|
||||||
sql<number>`(SELECT COUNT(DISTINCT t2.id) FROM "Team" AS t2 WHERE t2."organisationId" = o.id)`.as(
|
.executeTakeFirst();
|
||||||
'totalTeams',
|
|
||||||
),
|
|
||||||
sql<number>`(SELECT COUNT(DISTINCT om2."userId") FROM "OrganisationMember" AS om2 WHERE om2."organisationId" = o.id)`.as(
|
|
||||||
'totalMembers',
|
|
||||||
),
|
|
||||||
sql<number>`(
|
|
||||||
SELECT COUNT(DISTINCT e2.id)
|
|
||||||
FROM "Envelope" AS e2
|
|
||||||
INNER JOIN "Team" AS t2 ON t2.id = e2."teamId"
|
|
||||||
WHERE t2."organisationId" = o.id AND e2."deletedAt" IS NULL AND e2.type = 'DOCUMENT'
|
|
||||||
)`.as('totalDocuments'),
|
|
||||||
sql<number>`(
|
|
||||||
SELECT COUNT(DISTINCT e2.id)
|
|
||||||
FROM "Envelope" AS e2
|
|
||||||
INNER JOIN "Team" AS t2 ON t2.id = e2."teamId"
|
|
||||||
WHERE t2."organisationId" = o.id AND e2."deletedAt" IS NULL AND e2.type = 'DOCUMENT' AND e2.status IN ('DRAFT', 'PENDING')
|
|
||||||
)`.as('activeDocuments'),
|
|
||||||
sql<number>`(
|
|
||||||
SELECT COUNT(DISTINCT e2.id)
|
|
||||||
FROM "Envelope" AS e2
|
|
||||||
INNER JOIN "Team" AS t2 ON t2.id = e2."teamId"
|
|
||||||
WHERE t2."organisationId" = o.id AND e2."deletedAt" IS NULL AND e2.type = 'DOCUMENT' AND e2.status = 'COMPLETED'
|
|
||||||
)`.as('completedDocuments'),
|
|
||||||
(createdAtFrom
|
|
||||||
? sql<number>`(
|
|
||||||
SELECT COUNT(DISTINCT e2.id)
|
|
||||||
FROM "Envelope" AS e2
|
|
||||||
INNER JOIN "Team" AS t2 ON t2.id = e2."teamId"
|
|
||||||
WHERE t2."organisationId" = o.id
|
|
||||||
AND e2."deletedAt" IS NULL
|
|
||||||
AND e2.type = 'DOCUMENT'
|
|
||||||
AND e2.status = 'COMPLETED'
|
|
||||||
AND e2."createdAt" >= ${createdAtFrom}
|
|
||||||
)`
|
|
||||||
: sql<number>`(
|
|
||||||
SELECT COUNT(DISTINCT e2.id)
|
|
||||||
FROM "Envelope" AS e2
|
|
||||||
INNER JOIN "Team" AS t2 ON t2.id = e2."teamId"
|
|
||||||
WHERE t2."organisationId" = o.id
|
|
||||||
AND e2."deletedAt" IS NULL
|
|
||||||
AND e2.type = 'DOCUMENT'
|
|
||||||
AND e2.status = 'COMPLETED'
|
|
||||||
)`
|
|
||||||
).as('volumeThisPeriod'),
|
|
||||||
sql<number>`(
|
|
||||||
SELECT COUNT(DISTINCT e2.id)
|
|
||||||
FROM "Envelope" AS e2
|
|
||||||
INNER JOIN "Team" AS t2 ON t2.id = e2."teamId"
|
|
||||||
WHERE t2."organisationId" = o.id AND e2."deletedAt" IS NULL AND e2.type = 'DOCUMENT' AND e2.status = 'COMPLETED'
|
|
||||||
)`.as('volumeAllTime'),
|
|
||||||
]);
|
|
||||||
|
|
||||||
const result = await summaryQuery.executeTakeFirst();
|
const memberCountQuery = kyselyPrisma.$kysely
|
||||||
|
.selectFrom('OrganisationMember')
|
||||||
|
.where('organisationId', '=', organisationId)
|
||||||
|
.select(sql<number>`count(id)`.as('count'))
|
||||||
|
.executeTakeFirst();
|
||||||
|
|
||||||
|
const envelopeStatsQuery = kyselyPrisma.$kysely
|
||||||
|
.selectFrom('Envelope as e')
|
||||||
|
.innerJoin('Team as t', 't.id', 'e.teamId')
|
||||||
|
.where('t.organisationId', '=', organisationId)
|
||||||
|
.where('e.deletedAt', 'is', null)
|
||||||
|
.where('e.type', '=', sql.lit(EnvelopeType.DOCUMENT))
|
||||||
|
.select([
|
||||||
|
sql<number>`count(e.id)`.as('totalDocuments'),
|
||||||
|
sql<number>`count(case when e.status in ('DRAFT', 'PENDING') then 1 end)`.as(
|
||||||
|
'activeDocuments',
|
||||||
|
),
|
||||||
|
sql<number>`count(case when e.status = 'COMPLETED' then 1 end)`.as('completedDocuments'),
|
||||||
|
sql<number>`count(case when e.status = 'COMPLETED' then 1 end)`.as('volumeAllTime'),
|
||||||
|
(createdAtFrom
|
||||||
|
? sql<number>`count(case when e.status = 'COMPLETED' and e."createdAt" >= ${createdAtFrom} then 1 end)`
|
||||||
|
: sql<number>`count(case when e.status = 'COMPLETED' then 1 end)`
|
||||||
|
).as('volumeThisPeriod'),
|
||||||
|
])
|
||||||
|
.executeTakeFirst();
|
||||||
|
|
||||||
|
const [teamCount, memberCount, envelopeStats] = await Promise.all([
|
||||||
|
teamCountQuery,
|
||||||
|
memberCountQuery,
|
||||||
|
envelopeStatsQuery,
|
||||||
|
]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
totalTeams: Number(result?.totalTeams || 0),
|
totalTeams: Number(teamCount?.count || 0),
|
||||||
totalMembers: Number(result?.totalMembers || 0),
|
totalMembers: Number(memberCount?.count || 0),
|
||||||
totalDocuments: Number(result?.totalDocuments || 0),
|
totalDocuments: Number(envelopeStats?.totalDocuments || 0),
|
||||||
activeDocuments: Number(result?.activeDocuments || 0),
|
activeDocuments: Number(envelopeStats?.activeDocuments || 0),
|
||||||
completedDocuments: Number(result?.completedDocuments || 0),
|
completedDocuments: Number(envelopeStats?.completedDocuments || 0),
|
||||||
volumeThisPeriod: Number(result?.volumeThisPeriod || 0),
|
volumeThisPeriod: Number(envelopeStats?.volumeThisPeriod || 0),
|
||||||
volumeAllTime: Number(result?.volumeAllTime || 0),
|
volumeAllTime: Number(envelopeStats?.volumeAllTime || 0),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@ -33,25 +33,32 @@ export async function getSigningVolume({
|
|||||||
|
|
||||||
let findQuery = kyselyPrisma.$kysely
|
let findQuery = kyselyPrisma.$kysely
|
||||||
.selectFrom('Organisation as o')
|
.selectFrom('Organisation as o')
|
||||||
.leftJoin('Team as t', 'o.id', 't.organisationId')
|
|
||||||
.leftJoin('Envelope as e', (join) =>
|
|
||||||
join
|
|
||||||
.onRef('t.id', '=', 'e.teamId')
|
|
||||||
.on('e.status', '=', sql.lit(DocumentStatus.COMPLETED))
|
|
||||||
.on('e.deletedAt', 'is', null)
|
|
||||||
.on('e.type', '=', sql.lit(EnvelopeType.DOCUMENT)),
|
|
||||||
)
|
|
||||||
.where((eb) =>
|
.where((eb) =>
|
||||||
eb.or([eb('o.name', 'ilike', `%${search}%`), eb('t.name', 'ilike', `%${search}%`)]),
|
eb.or([
|
||||||
|
eb('o.name', 'ilike', `%${search}%`),
|
||||||
|
eb.exists(
|
||||||
|
eb
|
||||||
|
.selectFrom('Team as t')
|
||||||
|
.whereRef('t.organisationId', '=', 'o.id')
|
||||||
|
.where('t.name', 'ilike', `%${search}%`),
|
||||||
|
),
|
||||||
|
]),
|
||||||
)
|
)
|
||||||
.select([
|
.select((eb) => [
|
||||||
'o.id as id',
|
'o.id as id',
|
||||||
'o.createdAt as createdAt',
|
'o.createdAt as createdAt',
|
||||||
'o.customerId as customerId',
|
'o.customerId as customerId',
|
||||||
sql<string>`COALESCE(o.name, 'Unknown')`.as('name'),
|
sql<string>`COALESCE(o.name, 'Unknown')`.as('name'),
|
||||||
sql<number>`COUNT(DISTINCT e.id)`.as('signingVolume'),
|
eb
|
||||||
])
|
.selectFrom('Envelope as e')
|
||||||
.groupBy(['o.id', 'o.name', 'o.customerId']);
|
.innerJoin('Team as t', 't.id', 'e.teamId')
|
||||||
|
.whereRef('t.organisationId', '=', 'o.id')
|
||||||
|
.where('e.status', '=', sql.lit(DocumentStatus.COMPLETED))
|
||||||
|
.where('e.deletedAt', 'is', null)
|
||||||
|
.where('e.type', '=', sql.lit(EnvelopeType.DOCUMENT))
|
||||||
|
.select(sql<number>`count(e.id)`.as('count'))
|
||||||
|
.as('signingVolume'),
|
||||||
|
]);
|
||||||
|
|
||||||
switch (sortBy) {
|
switch (sortBy) {
|
||||||
case 'name':
|
case 'name':
|
||||||
@ -71,11 +78,18 @@ export async function getSigningVolume({
|
|||||||
|
|
||||||
const countQuery = kyselyPrisma.$kysely
|
const countQuery = kyselyPrisma.$kysely
|
||||||
.selectFrom('Organisation as o')
|
.selectFrom('Organisation as o')
|
||||||
.leftJoin('Team as t', 'o.id', 't.organisationId')
|
|
||||||
.where((eb) =>
|
.where((eb) =>
|
||||||
eb.or([eb('o.name', 'ilike', `%${search}%`), eb('t.name', 'ilike', `%${search}%`)]),
|
eb.or([
|
||||||
|
eb('o.name', 'ilike', `%${search}%`),
|
||||||
|
eb.exists(
|
||||||
|
eb
|
||||||
|
.selectFrom('Team as t')
|
||||||
|
.whereRef('t.organisationId', '=', 'o.id')
|
||||||
|
.where('t.name', 'ilike', `%${search}%`),
|
||||||
|
),
|
||||||
|
]),
|
||||||
)
|
)
|
||||||
.select(() => [sql<number>`COUNT(DISTINCT o.id)`.as('count')]);
|
.select(({ fn }) => [fn.countAll().as('count')]);
|
||||||
|
|
||||||
const [results, [{ count }]] = await Promise.all([findQuery.execute(), countQuery.execute()]);
|
const [results, [{ count }]] = await Promise.all([findQuery.execute(), countQuery.execute()]);
|
||||||
|
|
||||||
@ -104,64 +118,77 @@ export async function getOrganisationInsights({
|
|||||||
const offset = Math.max(page - 1, 0) * perPage;
|
const offset = Math.max(page - 1, 0) * perPage;
|
||||||
|
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
let dateCondition = sql`1=1`;
|
let dateCondition = sql<boolean>`1=1`;
|
||||||
|
|
||||||
if (startDate && endDate) {
|
if (startDate && endDate) {
|
||||||
dateCondition = sql`e."createdAt" >= ${startDate} AND e."createdAt" <= ${endDate}`;
|
dateCondition = sql<boolean>`e."createdAt" >= ${startDate} AND e."createdAt" <= ${endDate}`;
|
||||||
} else {
|
} else {
|
||||||
switch (dateRange) {
|
switch (dateRange) {
|
||||||
case 'last30days': {
|
case 'last30days': {
|
||||||
const thirtyDaysAgo = new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000);
|
const thirtyDaysAgo = new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000);
|
||||||
dateCondition = sql`e."createdAt" >= ${thirtyDaysAgo}`;
|
dateCondition = sql<boolean>`e."createdAt" >= ${thirtyDaysAgo}`;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 'last90days': {
|
case 'last90days': {
|
||||||
const ninetyDaysAgo = new Date(now.getTime() - 90 * 24 * 60 * 60 * 1000);
|
const ninetyDaysAgo = new Date(now.getTime() - 90 * 24 * 60 * 60 * 1000);
|
||||||
dateCondition = sql`e."createdAt" >= ${ninetyDaysAgo}`;
|
dateCondition = sql<boolean>`e."createdAt" >= ${ninetyDaysAgo}`;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 'lastYear': {
|
case 'lastYear': {
|
||||||
const oneYearAgo = new Date(now.getFullYear() - 1, now.getMonth(), now.getDate());
|
const oneYearAgo = new Date(now.getFullYear() - 1, now.getMonth(), now.getDate());
|
||||||
dateCondition = sql`e."createdAt" >= ${oneYearAgo}`;
|
dateCondition = sql<boolean>`e."createdAt" >= ${oneYearAgo}`;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 'allTime':
|
case 'allTime':
|
||||||
default:
|
default:
|
||||||
dateCondition = sql`1=1`;
|
dateCondition = sql<boolean>`1=1`;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let findQuery = kyselyPrisma.$kysely
|
let findQuery = kyselyPrisma.$kysely
|
||||||
.selectFrom('Organisation as o')
|
.selectFrom('Organisation as o')
|
||||||
.leftJoin('Team as t', 'o.id', 't.organisationId')
|
|
||||||
.leftJoin('Envelope as e', (join) =>
|
|
||||||
join
|
|
||||||
.onRef('t.id', '=', 'e.teamId')
|
|
||||||
.on('e.status', '=', sql.lit(DocumentStatus.COMPLETED))
|
|
||||||
.on('e.deletedAt', 'is', null)
|
|
||||||
.on('e.type', '=', sql.lit(EnvelopeType.DOCUMENT)),
|
|
||||||
)
|
|
||||||
.leftJoin('OrganisationMember as om', 'o.id', 'om.organisationId')
|
|
||||||
.leftJoin('Subscription as s', 'o.id', 's.organisationId')
|
.leftJoin('Subscription as s', 'o.id', 's.organisationId')
|
||||||
.where((eb) =>
|
.where((eb) =>
|
||||||
eb.or([eb('o.name', 'ilike', `%${search}%`), eb('t.name', 'ilike', `%${search}%`)]),
|
eb.or([
|
||||||
|
eb('o.name', 'ilike', `%${search}%`),
|
||||||
|
eb.exists(
|
||||||
|
eb
|
||||||
|
.selectFrom('Team as t')
|
||||||
|
.whereRef('t.organisationId', '=', 'o.id')
|
||||||
|
.where('t.name', 'ilike', `%${search}%`),
|
||||||
|
),
|
||||||
|
]),
|
||||||
)
|
)
|
||||||
.select([
|
.select((eb) => [
|
||||||
'o.id as id',
|
'o.id as id',
|
||||||
'o.createdAt as createdAt',
|
'o.createdAt as createdAt',
|
||||||
'o.customerId as customerId',
|
'o.customerId as customerId',
|
||||||
sql<string>`COALESCE(o.name, 'Unknown')`.as('name'),
|
sql<string>`COALESCE(o.name, 'Unknown')`.as('name'),
|
||||||
sql<number>`COUNT(DISTINCT CASE WHEN e.id IS NOT NULL AND ${dateCondition} THEN e.id END)`.as(
|
|
||||||
'signingVolume',
|
|
||||||
),
|
|
||||||
sql<number>`GREATEST(COUNT(DISTINCT t.id), 1)`.as('teamCount'),
|
|
||||||
sql<number>`COUNT(DISTINCT om."userId")`.as('memberCount'),
|
|
||||||
sql<string>`CASE WHEN s.status IS NOT NULL THEN s.status ELSE NULL END`.as(
|
sql<string>`CASE WHEN s.status IS NOT NULL THEN s.status ELSE NULL END`.as(
|
||||||
'subscriptionStatus',
|
'subscriptionStatus',
|
||||||
),
|
),
|
||||||
])
|
eb
|
||||||
.groupBy(['o.id', 'o.name', 'o.customerId', 's.status']);
|
.selectFrom('Team as t')
|
||||||
|
.whereRef('t.organisationId', '=', 'o.id')
|
||||||
|
.select(sql<number>`count(t.id)`.as('count'))
|
||||||
|
.as('teamCount'),
|
||||||
|
eb
|
||||||
|
.selectFrom('OrganisationMember as om')
|
||||||
|
.whereRef('om.organisationId', '=', 'o.id')
|
||||||
|
.select(sql<number>`count(om.id)`.as('count'))
|
||||||
|
.as('memberCount'),
|
||||||
|
eb
|
||||||
|
.selectFrom('Envelope as e')
|
||||||
|
.innerJoin('Team as t', 't.id', 'e.teamId')
|
||||||
|
.whereRef('t.organisationId', '=', 'o.id')
|
||||||
|
.where('e.status', '=', sql.lit(DocumentStatus.COMPLETED))
|
||||||
|
.where('e.deletedAt', 'is', null)
|
||||||
|
.where('e.type', '=', sql.lit(EnvelopeType.DOCUMENT))
|
||||||
|
.where(dateCondition)
|
||||||
|
.select(sql<number>`count(e.id)`.as('count'))
|
||||||
|
.as('signingVolume'),
|
||||||
|
]);
|
||||||
|
|
||||||
switch (sortBy) {
|
switch (sortBy) {
|
||||||
case 'name':
|
case 'name':
|
||||||
@ -181,11 +208,18 @@ export async function getOrganisationInsights({
|
|||||||
|
|
||||||
const countQuery = kyselyPrisma.$kysely
|
const countQuery = kyselyPrisma.$kysely
|
||||||
.selectFrom('Organisation as o')
|
.selectFrom('Organisation as o')
|
||||||
.leftJoin('Team as t', 'o.id', 't.organisationId')
|
|
||||||
.where((eb) =>
|
.where((eb) =>
|
||||||
eb.or([eb('o.name', 'ilike', `%${search}%`), eb('t.name', 'ilike', `%${search}%`)]),
|
eb.or([
|
||||||
|
eb('o.name', 'ilike', `%${search}%`),
|
||||||
|
eb.exists(
|
||||||
|
eb
|
||||||
|
.selectFrom('Team as t')
|
||||||
|
.whereRef('t.organisationId', '=', 'o.id')
|
||||||
|
.where('t.name', 'ilike', `%${search}%`),
|
||||||
|
),
|
||||||
|
]),
|
||||||
)
|
)
|
||||||
.select(() => [sql<number>`COUNT(DISTINCT o.id)`.as('count')]);
|
.select(({ fn }) => [fn.countAll().as('count')]);
|
||||||
|
|
||||||
const [results, [{ count }]] = await Promise.all([findQuery.execute(), countQuery.execute()]);
|
const [results, [{ count }]] = await Promise.all([findQuery.execute(), countQuery.execute()]);
|
||||||
|
|
||||||
|
|||||||
@ -103,7 +103,6 @@ export const getDocumentAndSenderByToken = async ({
|
|||||||
select: {
|
select: {
|
||||||
name: true,
|
name: true,
|
||||||
teamEmail: true,
|
teamEmail: true,
|
||||||
url: true,
|
|
||||||
teamGlobalSettings: {
|
teamGlobalSettings: {
|
||||||
select: {
|
select: {
|
||||||
brandingEnabled: true,
|
brandingEnabled: true,
|
||||||
|
|||||||
@ -8,7 +8,7 @@ msgstr ""
|
|||||||
"Language: pl\n"
|
"Language: pl\n"
|
||||||
"Project-Id-Version: documenso-app\n"
|
"Project-Id-Version: documenso-app\n"
|
||||||
"Report-Msgid-Bugs-To: \n"
|
"Report-Msgid-Bugs-To: \n"
|
||||||
"PO-Revision-Date: 2025-11-20 02:32\n"
|
"PO-Revision-Date: 2025-11-17 02:33\n"
|
||||||
"Last-Translator: \n"
|
"Last-Translator: \n"
|
||||||
"Language-Team: Polish\n"
|
"Language-Team: Polish\n"
|
||||||
"Plural-Forms: nplurals=4; plural=(n==1 ? 0 : (n%10>=2 && n%10<=4) && (n%100<12 || n%100>14) ? 1 : n!=1 && (n%10>=0 && n%10<=1) || (n%10>=5 && n%10<=9) || (n%100>=12 && n%100<=14) ? 2 : 3);\n"
|
"Plural-Forms: nplurals=4; plural=(n==1 ? 0 : (n%10>=2 && n%10<=4) && (n%100<12 || n%100>14) ? 1 : n!=1 && (n%10>=0 && n%10<=1) || (n%10>=5 && n%10<=9) || (n%100>=12 && n%100<=14) ? 2 : 3);\n"
|
||||||
@ -179,7 +179,7 @@ msgstr "Sprawdź i {recipientActionVerb} dokument utworzony przez zespół {0}"
|
|||||||
#: apps/remix/app/components/general/envelope/envelope-drop-zone-wrapper.tsx
|
#: apps/remix/app/components/general/envelope/envelope-drop-zone-wrapper.tsx
|
||||||
#: apps/remix/app/components/general/document/document-upload-button-legacy.tsx
|
#: apps/remix/app/components/general/document/document-upload-button-legacy.tsx
|
||||||
msgid "{0} of {1} documents remaining this month."
|
msgid "{0} of {1} documents remaining this month."
|
||||||
msgstr "Pozostało {0} z {1} dokumentów w tym miesiącu."
|
msgstr "{0} z {1} dokumentów pozostałych w tym miesiącu."
|
||||||
|
|
||||||
#. placeholder {0}: table.getFilteredSelectedRowModel().rows.length
|
#. placeholder {0}: table.getFilteredSelectedRowModel().rows.length
|
||||||
#. placeholder {1}: table.getFilteredRowModel().rows.length
|
#. placeholder {1}: table.getFilteredRowModel().rows.length
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@ -8,17 +8,15 @@ export type EnvelopeItemPdfUrlOptions =
|
|||||||
envelopeItem: Pick<EnvelopeItem, 'id' | 'envelopeId'>;
|
envelopeItem: Pick<EnvelopeItem, 'id' | 'envelopeId'>;
|
||||||
token: string | undefined;
|
token: string | undefined;
|
||||||
version: 'original' | 'signed';
|
version: 'original' | 'signed';
|
||||||
presignToken?: undefined;
|
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
type: 'view';
|
type: 'view';
|
||||||
envelopeItem: Pick<EnvelopeItem, 'id' | 'envelopeId'>;
|
envelopeItem: Pick<EnvelopeItem, 'id' | 'envelopeId'>;
|
||||||
token: string | undefined;
|
token: string | undefined;
|
||||||
presignToken?: string | undefined;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getEnvelopeItemPdfUrl = (options: EnvelopeItemPdfUrlOptions) => {
|
export const getEnvelopeItemPdfUrl = (options: EnvelopeItemPdfUrlOptions) => {
|
||||||
const { envelopeItem, token, type, presignToken } = options;
|
const { envelopeItem, token, type } = options;
|
||||||
|
|
||||||
const { id, envelopeId } = envelopeItem;
|
const { id, envelopeId } = envelopeItem;
|
||||||
|
|
||||||
@ -26,11 +24,11 @@ export const getEnvelopeItemPdfUrl = (options: EnvelopeItemPdfUrlOptions) => {
|
|||||||
const version = options.version;
|
const version = options.version;
|
||||||
|
|
||||||
return token
|
return token
|
||||||
? `${NEXT_PUBLIC_WEBAPP_URL()}/api/files/token/${token}/envelopeItem/${id}/download/${version}${presignToken ? `?presignToken=${presignToken}` : ''}`
|
? `${NEXT_PUBLIC_WEBAPP_URL()}/api/files/token/${token}/envelopeItem/${id}/download/${version}`
|
||||||
: `${NEXT_PUBLIC_WEBAPP_URL()}/api/files/envelope/${envelopeId}/envelopeItem/${id}/download/${version}`;
|
: `${NEXT_PUBLIC_WEBAPP_URL()}/api/files/envelope/${envelopeId}/envelopeItem/${id}/download/${version}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
return token
|
return token
|
||||||
? `${NEXT_PUBLIC_WEBAPP_URL()}/api/files/token/${token}/envelopeItem/${id}${presignToken ? `?presignToken=${presignToken}` : ''}`
|
? `${NEXT_PUBLIC_WEBAPP_URL()}/api/files/token/${token}/envelopeItem/${id}`
|
||||||
: `${NEXT_PUBLIC_WEBAPP_URL()}/api/files/envelope/${envelopeId}/envelopeItem/${id}`;
|
: `${NEXT_PUBLIC_WEBAPP_URL()}/api/files/envelope/${envelopeId}/envelopeItem/${id}`;
|
||||||
};
|
};
|
||||||
|
|||||||
@ -0,0 +1,29 @@
|
|||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "Envelope_teamId_deletedAt_type_status_idx" ON "Envelope"("teamId", "deletedAt", "type", "status");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "Envelope_teamId_deletedAt_type_createdAt_idx" ON "Envelope"("teamId", "deletedAt", "type", "createdAt");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "Envelope_userId_deletedAt_type_idx" ON "Envelope"("userId", "deletedAt", "type");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "Envelope_status_deletedAt_type_idx" ON "Envelope"("status", "deletedAt", "type");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "Organisation_name_idx" ON "Organisation"("name");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "OrganisationMember_organisationId_idx" ON "OrganisationMember"("organisationId");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "Recipient_email_idx" ON "Recipient"("email");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "Recipient_signedAt_idx" ON "Recipient"("signedAt");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "Recipient_envelopeId_signedAt_idx" ON "Recipient"("envelopeId", "signedAt");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "Team_organisationId_name_idx" ON "Team"("organisationId", "name");
|
||||||
@ -433,6 +433,10 @@ model Envelope {
|
|||||||
@@index([folderId])
|
@@index([folderId])
|
||||||
@@index([teamId])
|
@@index([teamId])
|
||||||
@@index([userId])
|
@@index([userId])
|
||||||
|
@@index([teamId, deletedAt, type, status])
|
||||||
|
@@index([teamId, deletedAt, type, createdAt])
|
||||||
|
@@index([userId, deletedAt, type])
|
||||||
|
@@index([status, deletedAt, type])
|
||||||
}
|
}
|
||||||
|
|
||||||
model EnvelopeItem {
|
model EnvelopeItem {
|
||||||
@ -585,6 +589,9 @@ model Recipient {
|
|||||||
|
|
||||||
@@index([envelopeId])
|
@@index([envelopeId])
|
||||||
@@index([token])
|
@@index([token])
|
||||||
|
@@index([email])
|
||||||
|
@@index([signedAt])
|
||||||
|
@@index([envelopeId, signedAt])
|
||||||
}
|
}
|
||||||
|
|
||||||
enum FieldType {
|
enum FieldType {
|
||||||
@ -694,6 +701,8 @@ model Organisation {
|
|||||||
|
|
||||||
organisationAuthenticationPortalId String @unique
|
organisationAuthenticationPortalId String @unique
|
||||||
organisationAuthenticationPortal OrganisationAuthenticationPortal @relation(fields: [organisationAuthenticationPortalId], references: [id])
|
organisationAuthenticationPortal OrganisationAuthenticationPortal @relation(fields: [organisationAuthenticationPortalId], references: [id])
|
||||||
|
|
||||||
|
@@index([name])
|
||||||
}
|
}
|
||||||
|
|
||||||
model OrganisationMember {
|
model OrganisationMember {
|
||||||
@ -710,6 +719,7 @@ model OrganisationMember {
|
|||||||
organisationGroupMembers OrganisationGroupMember[]
|
organisationGroupMembers OrganisationGroupMember[]
|
||||||
|
|
||||||
@@unique([userId, organisationId])
|
@@unique([userId, organisationId])
|
||||||
|
@@index([organisationId])
|
||||||
}
|
}
|
||||||
|
|
||||||
model OrganisationMemberInvite {
|
model OrganisationMemberInvite {
|
||||||
@ -884,6 +894,7 @@ model Team {
|
|||||||
teamGlobalSettings TeamGlobalSettings @relation(fields: [teamGlobalSettingsId], references: [id], onDelete: Cascade)
|
teamGlobalSettings TeamGlobalSettings @relation(fields: [teamGlobalSettingsId], references: [id], onDelete: Cascade)
|
||||||
|
|
||||||
@@index([organisationId])
|
@@index([organisationId])
|
||||||
|
@@index([organisationId, name])
|
||||||
}
|
}
|
||||||
|
|
||||||
model TeamEmail {
|
model TeamEmail {
|
||||||
|
|||||||
@ -25,7 +25,6 @@ import { redistributeEnvelopeRoute } from './redistribute-envelope';
|
|||||||
import { setEnvelopeFieldsRoute } from './set-envelope-fields';
|
import { setEnvelopeFieldsRoute } from './set-envelope-fields';
|
||||||
import { setEnvelopeRecipientsRoute } from './set-envelope-recipients';
|
import { setEnvelopeRecipientsRoute } from './set-envelope-recipients';
|
||||||
import { signEnvelopeFieldRoute } from './sign-envelope-field';
|
import { signEnvelopeFieldRoute } from './sign-envelope-field';
|
||||||
import { signingStatusEnvelopeRoute } from './signing-status-envelope';
|
|
||||||
import { updateEnvelopeRoute } from './update-envelope';
|
import { updateEnvelopeRoute } from './update-envelope';
|
||||||
import { updateEnvelopeItemsRoute } from './update-envelope-items';
|
import { updateEnvelopeItemsRoute } from './update-envelope-items';
|
||||||
import { useEnvelopeRoute } from './use-envelope';
|
import { useEnvelopeRoute } from './use-envelope';
|
||||||
@ -73,5 +72,4 @@ export const envelopeRouter = router({
|
|||||||
duplicate: duplicateEnvelopeRoute,
|
duplicate: duplicateEnvelopeRoute,
|
||||||
distribute: distributeEnvelopeRoute,
|
distribute: distributeEnvelopeRoute,
|
||||||
redistribute: redistributeEnvelopeRoute,
|
redistribute: redistributeEnvelopeRoute,
|
||||||
signingStatus: signingStatusEnvelopeRoute,
|
|
||||||
});
|
});
|
||||||
|
|||||||
@ -1,82 +0,0 @@
|
|||||||
import { DocumentStatus, EnvelopeType, RecipientRole, SigningStatus } from '@prisma/client';
|
|
||||||
|
|
||||||
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
|
||||||
import { prisma } from '@documenso/prisma';
|
|
||||||
|
|
||||||
import { maybeAuthenticatedProcedure } from '../trpc';
|
|
||||||
import {
|
|
||||||
ZSigningStatusEnvelopeRequestSchema,
|
|
||||||
ZSigningStatusEnvelopeResponseSchema,
|
|
||||||
} from './signing-status-envelope.types';
|
|
||||||
|
|
||||||
// Internal route - not intended for public API usage
|
|
||||||
export const signingStatusEnvelopeRoute = maybeAuthenticatedProcedure
|
|
||||||
.input(ZSigningStatusEnvelopeRequestSchema)
|
|
||||||
.output(ZSigningStatusEnvelopeResponseSchema)
|
|
||||||
.query(async ({ input, ctx }) => {
|
|
||||||
const { token } = input;
|
|
||||||
|
|
||||||
ctx.logger.info({
|
|
||||||
input: {
|
|
||||||
token,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const envelope = await prisma.envelope.findFirst({
|
|
||||||
where: {
|
|
||||||
type: EnvelopeType.DOCUMENT,
|
|
||||||
recipients: {
|
|
||||||
some: {
|
|
||||||
token,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
include: {
|
|
||||||
recipients: {
|
|
||||||
select: {
|
|
||||||
id: true,
|
|
||||||
name: true,
|
|
||||||
email: true,
|
|
||||||
signingStatus: true,
|
|
||||||
role: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!envelope) {
|
|
||||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
|
||||||
message: 'Envelope not found',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if envelope is rejected
|
|
||||||
if (envelope.status === DocumentStatus.REJECTED) {
|
|
||||||
return {
|
|
||||||
status: 'REJECTED',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (envelope.status === DocumentStatus.COMPLETED) {
|
|
||||||
return {
|
|
||||||
status: 'COMPLETED',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const isComplete =
|
|
||||||
envelope.recipients.some((recipient) => recipient.signingStatus === SigningStatus.REJECTED) ||
|
|
||||||
envelope.recipients.every(
|
|
||||||
(recipient) =>
|
|
||||||
recipient.role === RecipientRole.CC || recipient.signingStatus === SigningStatus.SIGNED,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (isComplete) {
|
|
||||||
return {
|
|
||||||
status: 'PROCESSING',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
status: 'PENDING',
|
|
||||||
};
|
|
||||||
});
|
|
||||||
@ -1,14 +0,0 @@
|
|||||||
import { z } from 'zod';
|
|
||||||
|
|
||||||
export const EnvelopeSigningStatus = z.enum(['PENDING', 'PROCESSING', 'COMPLETED', 'REJECTED']);
|
|
||||||
|
|
||||||
export const ZSigningStatusEnvelopeRequestSchema = z.object({
|
|
||||||
token: z.string().describe('The recipient token to check the signing status for'),
|
|
||||||
});
|
|
||||||
|
|
||||||
export const ZSigningStatusEnvelopeResponseSchema = z.object({
|
|
||||||
status: EnvelopeSigningStatus.describe('The current signing status of the envelope'),
|
|
||||||
});
|
|
||||||
|
|
||||||
export type TSigningStatusEnvelopeRequest = z.infer<typeof ZSigningStatusEnvelopeRequestSchema>;
|
|
||||||
export type TSigningStatusEnvelopeResponse = z.infer<typeof ZSigningStatusEnvelopeResponseSchema>;
|
|
||||||
@ -127,11 +127,11 @@ export const DocumentShareButton = ({
|
|||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
disabled={!token || !documentId}
|
disabled={!token || !documentId}
|
||||||
className={cn('h-11 w-full max-w-lg flex-1', className)}
|
className={cn('flex-1 text-[11px]', className)}
|
||||||
loading={isLoading}
|
loading={isLoading}
|
||||||
>
|
>
|
||||||
{!isLoading && <Sparkles className="mr-2 h-5 w-5" />}
|
{!isLoading && <Sparkles className="mr-2 h-5 w-5" />}
|
||||||
<Trans>Share</Trans>
|
<Trans>Share Signature Card</Trans>
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
|
|||||||
@ -56,7 +56,6 @@ export type PDFViewerProps = {
|
|||||||
className?: string;
|
className?: string;
|
||||||
envelopeItem: Pick<EnvelopeItem, 'id' | 'envelopeId'>;
|
envelopeItem: Pick<EnvelopeItem, 'id' | 'envelopeId'>;
|
||||||
token: string | undefined;
|
token: string | undefined;
|
||||||
presignToken?: string | undefined;
|
|
||||||
version: 'original' | 'signed';
|
version: 'original' | 'signed';
|
||||||
onDocumentLoad?: (_doc: LoadedPDFDocument) => void;
|
onDocumentLoad?: (_doc: LoadedPDFDocument) => void;
|
||||||
onPageClick?: OnPDFViewerPageClick;
|
onPageClick?: OnPDFViewerPageClick;
|
||||||
@ -68,7 +67,6 @@ export const PDFViewer = ({
|
|||||||
className,
|
className,
|
||||||
envelopeItem,
|
envelopeItem,
|
||||||
token,
|
token,
|
||||||
presignToken,
|
|
||||||
version,
|
version,
|
||||||
onDocumentLoad,
|
onDocumentLoad,
|
||||||
onPageClick,
|
onPageClick,
|
||||||
@ -168,7 +166,6 @@ export const PDFViewer = ({
|
|||||||
type: 'view',
|
type: 'view',
|
||||||
envelopeItem: envelopeItem,
|
envelopeItem: envelopeItem,
|
||||||
token,
|
token,
|
||||||
presignToken,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const bytes = await fetch(documentUrl).then(async (res) => await res.arrayBuffer());
|
const bytes = await fetch(documentUrl).then(async (res) => await res.arrayBuffer());
|
||||||
|
|||||||
@ -119,7 +119,6 @@
|
|||||||
"GOOGLE_APPLICATION_CREDENTIALS",
|
"GOOGLE_APPLICATION_CREDENTIALS",
|
||||||
"E2E_TEST_AUTHENTICATE_USERNAME",
|
"E2E_TEST_AUTHENTICATE_USERNAME",
|
||||||
"E2E_TEST_AUTHENTICATE_USER_EMAIL",
|
"E2E_TEST_AUTHENTICATE_USER_EMAIL",
|
||||||
"E2E_TEST_AUTHENTICATE_USER_PASSWORD",
|
"E2E_TEST_AUTHENTICATE_USER_PASSWORD"
|
||||||
"NEXT_PRIVATE_OIDC_PROMPT"
|
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user