diff --git a/apps/web/src/app/(dashboard)/documents/[id]/edit-document.tsx b/apps/web/src/app/(dashboard)/documents/[id]/edit-document.tsx index 3030794ba..b2e3d7fea 100644 --- a/apps/web/src/app/(dashboard)/documents/[id]/edit-document.tsx +++ b/apps/web/src/app/(dashboard)/documents/[id]/edit-document.tsx @@ -419,6 +419,8 @@ export const EditDocumentForm = ({ isDocumentEnterprise={isDocumentEnterprise} onSubmit={onAddSignersFormSubmit} isDocumentPdfLoaded={isDocumentPdfLoaded} + documentId={document.id} + // teamId={team?.id} /> { + const recipient = await prisma.recipient.findFirst({ + where: { + id: recipientId, + Document: { + id: documentId, + ...(teamId + ? { + team: { + id: teamId, + members: { + some: { + userId, + }, + }, + }, + } + : { + userId, + teamId: null, + }), + }, + }, + }); + + if (!recipient) { + throw new Error('Recipient not found'); + } + + const user = await prisma.user.findFirstOrThrow({ + where: { + id: userId, + }, + }); + + let team: Team | null = null; + + if (teamId) { + team = await prisma.team.findFirst({ + where: { + id: teamId, + members: { + some: { + userId, + }, + }, + }, + }); + } + + const updatedRecipient = await prisma.$transaction(async (tx) => { + const updated = await tx.recipient.update({ + where: { + id: recipient.id, + }, + data: { + expired: new Date(expiry), + }, + }); + + // TODO: fix the audit logs + // await tx.documentAuditLog.create({ + // data: createDocumentAuditLogData({ + // type: 'RECIPIENT_EXPIRY_UPDATED', + // documentId, + // user: { + // id: team?.id ?? user.id, + // email: team?.name ?? user.email, + // name: team ? '' : user.name, + // }, + // data: { + // recipientEmail: recipient.email, + // recipientName: recipient.name, + // recipientId: recipient.id, + // recipientRole: recipient.role, + // expiry, + // }, + // requestMetadata, + // }), + // }); + + return updated; + }); + + return updatedRecipient; +}; diff --git a/packages/trpc/server/recipient-router/router.ts b/packages/trpc/server/recipient-router/router.ts index f106ff553..330d2fe56 100644 --- a/packages/trpc/server/recipient-router/router.ts +++ b/packages/trpc/server/recipient-router/router.ts @@ -2,6 +2,7 @@ import { TRPCError } from '@trpc/server'; import { completeDocumentWithToken } from '@documenso/lib/server-only/document/complete-document-with-token'; import { rejectDocumentWithToken } from '@documenso/lib/server-only/document/reject-document-with-token'; +import { setRecipientExpiry } from '@documenso/lib/server-only/recipient/set-recipient-expiry'; import { setRecipientsForDocument } from '@documenso/lib/server-only/recipient/set-recipients-for-document'; import { setRecipientsForTemplate } from '@documenso/lib/server-only/recipient/set-recipients-for-template'; import { extractNextApiRequestMetadata } from '@documenso/lib/universal/extract-request-metadata'; @@ -12,6 +13,7 @@ import { ZAddTemplateSignersMutationSchema, ZCompleteDocumentWithTokenMutationSchema, ZRejectDocumentWithTokenMutationSchema, + ZSetSignerExpirySchema, } from './schema'; export const recipientRouter = router({ @@ -45,6 +47,30 @@ export const recipientRouter = router({ } }), + setSignerExpiry: authenticatedProcedure + .input(ZSetSignerExpirySchema) + .mutation(async ({ input, ctx }) => { + try { + const { documentId, signerId, expiry, teamId } = input; + + return await setRecipientExpiry({ + documentId, + recipientId: signerId, + expiry, + teamId, + userId: ctx.user.id, + requestMetadata: extractNextApiRequestMetadata(ctx.req), + }); + } catch (error) { + console.log(error); + + throw new TRPCError({ + code: 'BAD_REQUEST', + message: "We're unable to set the expiry for this signer. Please try again later.", + }); + } + }), + addTemplateSigners: authenticatedProcedure .input(ZAddTemplateSignersMutationSchema) .mutation(async ({ input, ctx }) => { diff --git a/packages/trpc/server/recipient-router/schema.ts b/packages/trpc/server/recipient-router/schema.ts index 60e47a439..f0f16be38 100644 --- a/packages/trpc/server/recipient-router/schema.ts +++ b/packages/trpc/server/recipient-router/schema.ts @@ -80,3 +80,12 @@ export const ZRejectDocumentWithTokenMutationSchema = z.object({ export type TRejectDocumentWithTokenMutationSchema = z.infer< typeof ZRejectDocumentWithTokenMutationSchema >; + +export const ZSetSignerExpirySchema = z.object({ + documentId: z.number(), + signerId: z.number(), + expiry: z.date(), + teamId: z.number().optional(), +}); + +export type TSetSignerExpirySchema = z.infer; diff --git a/packages/ui/primitives/document-flow/add-signers.tsx b/packages/ui/primitives/document-flow/add-signers.tsx index 7b30d91a2..c28b03e76 100644 --- a/packages/ui/primitives/document-flow/add-signers.tsx +++ b/packages/ui/primitives/document-flow/add-signers.tsx @@ -52,6 +52,7 @@ export type AddSignersFormProps = { isDocumentEnterprise: boolean; onSubmit: (_data: TAddSignersFormSchema) => void; isDocumentPdfLoaded: boolean; + documentId: number; }; export const AddSignersFormPartial = ({ @@ -62,6 +63,7 @@ export const AddSignersFormPartial = ({ isDocumentEnterprise, onSubmit, isDocumentPdfLoaded, + documentId, }: AddSignersFormProps) => { const { _ } = useLingui(); const { toast } = useToast(); @@ -634,6 +636,8 @@ export const AddSignersFormPartial = ({ 'mb-6': form.formState.errors.signers?.[index], })} onDelete={() => onRemoveSigner(index)} + signer={signer} + documentId={documentId} deleteDisabled={ snapshot.isDragging || isSubmitting || diff --git a/packages/ui/primitives/document-flow/add-signers.types.ts b/packages/ui/primitives/document-flow/add-signers.types.ts index 543af0f36..d78773433 100644 --- a/packages/ui/primitives/document-flow/add-signers.types.ts +++ b/packages/ui/primitives/document-flow/add-signers.types.ts @@ -6,24 +6,22 @@ import { ZRecipientActionAuthTypesSchema } from '@documenso/lib/types/document-a import { ZMapNegativeOneToUndefinedSchema } from './add-settings.types'; import { DocumentSigningOrder, RecipientRole } from '.prisma/client'; +export const ZAddSignerSchema = z.object({ + formId: z.string().min(1), + nativeId: z.number().optional(), + email: z + .string() + .email({ message: msg`Invalid email`.id }) + .min(1), + name: z.string(), + role: z.nativeEnum(RecipientRole), + signingOrder: z.number().optional(), + actionAuth: ZMapNegativeOneToUndefinedSchema.pipe(ZRecipientActionAuthTypesSchema.optional()), +}); + export const ZAddSignersFormSchema = z .object({ - signers: z.array( - z.object({ - formId: z.string().min(1), - nativeId: z.number().optional(), - email: z - .string() - .email({ message: msg`Invalid email`.id }) - .min(1), - name: z.string(), - role: z.nativeEnum(RecipientRole), - signingOrder: z.number().optional(), - actionAuth: ZMapNegativeOneToUndefinedSchema.pipe( - ZRecipientActionAuthTypesSchema.optional(), - ), - }), - ), + signers: z.array(ZAddSignerSchema), signingOrder: z.nativeEnum(DocumentSigningOrder), }) .refine( @@ -37,3 +35,4 @@ export const ZAddSignersFormSchema = z ); export type TAddSignersFormSchema = z.infer; +export type TAddSignerSchema = z.infer; diff --git a/packages/ui/primitives/document-flow/document-expiry-dialog.tsx b/packages/ui/primitives/document-flow/document-expiry-dialog.tsx index 73d1e7402..67256c469 100644 --- a/packages/ui/primitives/document-flow/document-expiry-dialog.tsx +++ b/packages/ui/primitives/document-flow/document-expiry-dialog.tsx @@ -1,12 +1,16 @@ 'use client'; +import { useRouter } from 'next/navigation'; + import { zodResolver } from '@hookform/resolvers/zod'; -import { Trans } from '@lingui/macro'; +import { Trans, msg } from '@lingui/macro'; +import { useLingui } from '@lingui/react'; import { format } from 'date-fns'; import { Calendar as CalendarIcon } from 'lucide-react'; import { useForm } from 'react-hook-form'; import * as z from 'zod'; +import { trpc } from '@documenso/trpc/react'; import { Button } from '@documenso/ui/primitives/button'; import { Calendar } from '@documenso/ui/primitives/calendar'; import { @@ -30,9 +34,11 @@ import { import { Popover, PopoverContent, PopoverTrigger } from '@documenso/ui/primitives/popover'; import { cn } from '../../lib/utils'; +import { useToast } from '../use-toast'; +import type { TAddSignerSchema as Signer } from './add-signers.types'; const formSchema = z.object({ - expiryDate: z.date({ + expiry: z.date({ required_error: 'Please select an expiry date.', }), }); @@ -40,37 +46,61 @@ const formSchema = z.object({ type DocumentExpiryDialogProps = { open: boolean; onOpenChange: (_open: boolean) => void; + signer: Signer; + documentId: number; }; -export default function DocumentExpiryDialog({ open, onOpenChange }: DocumentExpiryDialogProps) { +export default function DocumentExpiryDialog({ + open, + onOpenChange, + signer, + documentId, +}: DocumentExpiryDialogProps) { + const { toast } = useToast(); + const router = useRouter(); + + const { _ } = useLingui(); + const form = useForm>({ resolver: zodResolver(formSchema), }); - // const { mutateAsync: moveDocument, isLoading } = trpc.document.moveDocumentToTeam.useMutation({ - // onSuccess: () => { - // router.refresh(); - // toast({ - // title: _(msg`Document moved`), - // description: _(msg`The document has been successfully moved to the selected team.`), - // duration: 5000, - // }); - // onOpenChange(false); - // }, - // onError: (error) => { - // toast({ - // title: _(msg`Error`), - // description: error.message || _(msg`An error occurred while moving the document.`), - // variant: 'destructive', - // duration: 7500, - // }); - // }, - // }); + const { mutateAsync: setSignerExpiry, isLoading } = trpc.recipient.setSignerExpiry.useMutation({ + onSuccess: () => { + router.refresh(); + toast({ + title: _(msg`Signer Expiry Set`), + description: _(msg`The expiry date for the signer has been set.`), + duration: 5000, + }); + onOpenChange(false); + }, + onError: (error) => { + toast({ + title: _(msg`Error`), + description: error.message || _(msg`An error occurred while setting the expiry date.`), + variant: 'destructive', + duration: 7500, + }); + }, + }); - function onSubmit(values: z.infer) { - console.log(values); - onOpenChange(false); - } + const onSetExpiry = async (values: z.infer) => { + if (!signer.nativeId) { + return toast({ + title: _(msg`Error`), + description: _(msg`An error occurred while setting the expiry date.`), + variant: 'destructive', + duration: 7500, + }); + } + + await setSignerExpiry({ + documentId, + signerId: signer.nativeId, + expiry: new Date(values.expiry), + }); + }; return ( @@ -83,10 +113,10 @@ export default function DocumentExpiryDialog({ open, onOpenChange }: DocumentExp
- + ( Expiry Date @@ -128,7 +158,7 @@ export default function DocumentExpiryDialog({ open, onOpenChange }: DocumentExp Cancel - diff --git a/packages/ui/primitives/document-flow/signer-action-dropdown.tsx b/packages/ui/primitives/document-flow/signer-action-dropdown.tsx index 73ecb262b..a8a3efd90 100644 --- a/packages/ui/primitives/document-flow/signer-action-dropdown.tsx +++ b/packages/ui/primitives/document-flow/signer-action-dropdown.tsx @@ -13,15 +13,23 @@ import { } from '@documenso/ui/primitives/dropdown-menu'; import { cn } from '../../lib/utils'; +import type { TAddSignerSchema as Signer } from './add-signers.types'; import DocumentExpiryDialog from './document-expiry-dialog'; type SignerActionDropdownProps = { onDelete: () => void; deleteDisabled?: boolean; className?: string; + signer: Signer; + documentId: number; }; -export function SignerActionDropdown({ deleteDisabled, className }: SignerActionDropdownProps) { +export function SignerActionDropdown({ + deleteDisabled, + className, + signer, + documentId, +}: SignerActionDropdownProps) { const [isExpiryDialogOpen, setExpiryDialogOpen] = useState(false); return ( @@ -45,7 +53,12 @@ export function SignerActionDropdown({ deleteDisabled, className }: SignerAction - + ); }