From 6768ee3b06d738fc8be269143ea02e25cfb08d11 Mon Sep 17 00:00:00 2001 From: Reese Herber Date: Tue, 28 Jul 2026 09:23:19 -0700 Subject: [PATCH] feat: allow sending a pending document back to a signer for correction Recipients and document owners can now route a pending envelope back to an earlier recipient (or the sender) to fix a mistake, instead of rejecting and restarting the whole document. The target recipient's signing status and fields are reset so they can redo their part; every other recipient's data is left untouched. Affected parties are notified by email and the action is recorded in the audit log. Closes #143 --- .../document-signing-page-view-v2.tsx | 36 ++- .../document-signing-send-back-dialog.tsx | 218 ++++++++++++++ .../document-page-view-recipients.tsx | 36 +++ ...cument-send-back-for-correction-dialog.tsx | 165 ++++++++++ ...late-document-sent-back-for-correction.tsx | 58 ++++ .../document-sent-back-for-correction.tsx | 75 +++++ packages/lib/jobs/client.ts | 2 + ...sent-back-for-correction-emails.handler.ts | 162 ++++++++++ .../send-sent-back-for-correction-emails.ts | 35 +++ .../send-document-back-for-correction.ts | 283 ++++++++++++++++++ packages/lib/types/document-audit-logs.ts | 18 ++ packages/lib/utils/document-audit-logs.ts | 20 ++ .../trpc/server/envelope-router/router.ts | 2 + .../send-back-for-correction.ts | 42 +++ .../send-back-for-correction.types.ts | 31 ++ .../trpc/server/recipient-router/router.ts | 28 ++ .../trpc/server/recipient-router/schema.ts | 14 + 17 files changed, 1224 insertions(+), 1 deletion(-) create mode 100644 apps/remix/app/components/general/document-signing/document-signing-send-back-dialog.tsx create mode 100644 apps/remix/app/components/general/document/document-send-back-for-correction-dialog.tsx create mode 100644 packages/email/template-components/template-document-sent-back-for-correction.tsx create mode 100644 packages/email/templates/document-sent-back-for-correction.tsx create mode 100644 packages/lib/jobs/definitions/emails/send-sent-back-for-correction-emails.handler.ts create mode 100644 packages/lib/jobs/definitions/emails/send-sent-back-for-correction-emails.ts create mode 100644 packages/lib/server-only/document/send-document-back-for-correction.ts create mode 100644 packages/trpc/server/envelope-router/send-back-for-correction.ts create mode 100644 packages/trpc/server/envelope-router/send-back-for-correction.types.ts diff --git a/apps/remix/app/components/general/document-signing/document-signing-page-view-v2.tsx b/apps/remix/app/components/general/document-signing/document-signing-page-view-v2.tsx index 370770dc8a..825fd2e1a5 100644 --- a/apps/remix/app/components/general/document-signing/document-signing-page-view-v2.tsx +++ b/apps/remix/app/components/general/document-signing/document-signing-page-view-v2.tsx @@ -1,7 +1,7 @@ import { useMemo, useRef, useState } from 'react'; import { Plural, Trans, useLingui } from '@lingui/react/macro'; -import { EnvelopeType, RecipientRole } from '@prisma/client'; +import { EnvelopeType, RecipientRole, SigningStatus } from '@prisma/client'; import { motion } from 'framer-motion'; import { ArrowLeftIcon, @@ -10,6 +10,7 @@ import { PanelLeftCloseIcon, PanelLeftOpenIcon, PaperclipIcon, + RotateCcwIcon, } from 'lucide-react'; import { Link } from 'react-router'; import { match } from 'ts-pattern'; @@ -41,6 +42,7 @@ import EnvelopeSignerForm from '../envelope-signing/envelope-signer-form'; import { EnvelopeSignerHeader } from '../envelope-signing/envelope-signer-header'; import { DocumentSigningMobileWidget } from './document-signing-mobile-widget'; import { DocumentSigningRejectDialog } from './document-signing-reject-dialog'; +import { DocumentSigningSendBackDialog } from './document-signing-send-back-dialog'; import { useRequiredEnvelopeSigningContext } from './envelope-signing-provider'; export const DocumentSigningPageViewV2 = () => { @@ -50,6 +52,7 @@ export const DocumentSigningPageViewV2 = () => { const { isDirectTemplate, + envelopeData, envelope, recipient, recipientFields, @@ -81,6 +84,22 @@ export const DocumentSigningPageViewV2 = () => { return recipientFields.filter((field) => !field.inserted); }, [recipientFieldsRemaining, selectedAssistantRecipientFields, currentEnvelopeItem]); + /** + * Recipients who have already completed their part, and can therefore be sent the + * document back to for correction. + */ + const earlierRecipients = useMemo( + () => + envelope.recipients.filter( + (candidate) => + candidate.id !== recipient.id && + candidate.signingStatus === SigningStatus.SIGNED && + candidate.role !== RecipientRole.CC && + candidate.role !== RecipientRole.VIEWER, + ), + [envelope.recipients, recipient.id], + ); + return (
@@ -227,6 +246,21 @@ export const DocumentSigningPageViewV2 = () => { } /> )} + + {envelope.type === EnvelopeType.DOCUMENT && allowDocumentRejection && ( + + + Send Back for Correction + + } + /> + )}
)} diff --git a/apps/remix/app/components/general/document-signing/document-signing-send-back-dialog.tsx b/apps/remix/app/components/general/document-signing/document-signing-send-back-dialog.tsx new file mode 100644 index 0000000000..8ee11e703c --- /dev/null +++ b/apps/remix/app/components/general/document-signing/document-signing-send-back-dialog.tsx @@ -0,0 +1,218 @@ +import { useEffect, useState } from 'react'; + +import { zodResolver } from '@hookform/resolvers/zod'; +import { msg } from '@lingui/core/macro'; +import { Trans, useLingui } from '@lingui/react/macro'; +import { useForm } from 'react-hook-form'; +import { z } from 'zod'; + +import { trpc } from '@documenso/trpc/react'; +import { Button } from '@documenso/ui/primitives/button'; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, + DialogTrigger, +} from '@documenso/ui/primitives/dialog'; +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, +} from '@documenso/ui/primitives/form/form'; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from '@documenso/ui/primitives/select'; +import { Textarea } from '@documenso/ui/primitives/textarea'; +import { useToast } from '@documenso/ui/primitives/use-toast'; + +const SEND_BACK_TO_SENDER_VALUE = 'sender'; + +const ZSendBackFormSchema = z.object({ + target: z.string().min(1, msg`Please select who to send the document back to`), + reason: z + .string() + .min(1, msg`Please provide a reason`) + .max(500, msg`Reason must be less than 500 characters`), +}); + +type TSendBackFormSchema = z.infer; + +export interface DocumentSigningSendBackDialogProps { + documentId: number; + token: string; + senderName: string; + earlierRecipients: { id: number; name: string; email: string }[]; + onSentBack?: () => void | Promise; + trigger?: React.ReactNode; +} + +export function DocumentSigningSendBackDialog({ + documentId, + token, + senderName, + earlierRecipients, + onSentBack, + trigger, +}: DocumentSigningSendBackDialogProps) { + const { t } = useLingui(); + const { toast } = useToast(); + + const [isOpen, setIsOpen] = useState(false); + + const { mutateAsync: sendDocumentBackForCorrectionWithToken } = + trpc.recipient.sendDocumentBackForCorrectionWithToken.useMutation(); + + const form = useForm({ + resolver: zodResolver(ZSendBackFormSchema), + defaultValues: { + target: '', + reason: '', + }, + }); + + const onSendBack = async ({ target, reason }: TSendBackFormSchema) => { + try { + await sendDocumentBackForCorrectionWithToken({ + documentId, + token, + targetRecipientId: target === SEND_BACK_TO_SENDER_VALUE ? null : Number(target), + reason, + }); + + toast({ + title: t`Document sent back`, + description: t`The document has been sent back for correction.`, + duration: 5000, + }); + + setIsOpen(false); + + await onSentBack?.(); + } catch (err) { + toast({ + title: t`Error`, + description: t`An error occurred while sending the document back. Please try again.`, + variant: 'destructive', + duration: 5000, + }); + } + }; + + useEffect(() => { + if (!isOpen) { + form.reset(); + } + }, [isOpen]); + + return ( + + + {trigger ?? ( + + )} + + + + + + Send Back for Correction + + + + + Send this document back to an earlier recipient (or the sender) to fix a mistake. + Their previously collected fields will be cleared so they can redo them; everyone + else's information is left as-is. + + + + +
+ + ( + + + Send back to + + + + + + + )} + /> + + ( + + + Reason + + +