feat: expiry endpoint

This commit is contained in:
Ephraim Atta-Duncan
2024-11-17 11:02:52 +00:00
parent ca2b6bea95
commit e31a10a943
8 changed files with 237 additions and 47 deletions

View File

@ -419,6 +419,8 @@ export const EditDocumentForm = ({
isDocumentEnterprise={isDocumentEnterprise} isDocumentEnterprise={isDocumentEnterprise}
onSubmit={onAddSignersFormSubmit} onSubmit={onAddSignersFormSubmit}
isDocumentPdfLoaded={isDocumentPdfLoaded} isDocumentPdfLoaded={isDocumentPdfLoaded}
documentId={document.id}
// teamId={team?.id}
/> />
<AddFieldsFormPartial <AddFieldsFormPartial

View File

@ -0,0 +1,107 @@
import { prisma } from '@documenso/prisma';
import type { Team } from '@documenso/prisma/client';
import type { RequestMetadata } from '../../universal/extract-request-metadata';
export type SetRecipientExpiryOptions = {
documentId: number;
recipientId: number;
expiry: Date;
userId: number;
teamId?: number;
requestMetadata?: RequestMetadata;
};
export const setRecipientExpiry = async ({
documentId,
recipientId,
expiry,
userId,
teamId,
requestMetadata,
}: SetRecipientExpiryOptions) => {
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;
};

View File

@ -2,6 +2,7 @@ import { TRPCError } from '@trpc/server';
import { completeDocumentWithToken } from '@documenso/lib/server-only/document/complete-document-with-token'; 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 { 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 { setRecipientsForDocument } from '@documenso/lib/server-only/recipient/set-recipients-for-document';
import { setRecipientsForTemplate } from '@documenso/lib/server-only/recipient/set-recipients-for-template'; import { setRecipientsForTemplate } from '@documenso/lib/server-only/recipient/set-recipients-for-template';
import { extractNextApiRequestMetadata } from '@documenso/lib/universal/extract-request-metadata'; import { extractNextApiRequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
@ -12,6 +13,7 @@ import {
ZAddTemplateSignersMutationSchema, ZAddTemplateSignersMutationSchema,
ZCompleteDocumentWithTokenMutationSchema, ZCompleteDocumentWithTokenMutationSchema,
ZRejectDocumentWithTokenMutationSchema, ZRejectDocumentWithTokenMutationSchema,
ZSetSignerExpirySchema,
} from './schema'; } from './schema';
export const recipientRouter = router({ 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 addTemplateSigners: authenticatedProcedure
.input(ZAddTemplateSignersMutationSchema) .input(ZAddTemplateSignersMutationSchema)
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {

View File

@ -80,3 +80,12 @@ export const ZRejectDocumentWithTokenMutationSchema = z.object({
export type TRejectDocumentWithTokenMutationSchema = z.infer< export type TRejectDocumentWithTokenMutationSchema = z.infer<
typeof ZRejectDocumentWithTokenMutationSchema 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<typeof ZSetSignerExpirySchema>;

View File

@ -52,6 +52,7 @@ export type AddSignersFormProps = {
isDocumentEnterprise: boolean; isDocumentEnterprise: boolean;
onSubmit: (_data: TAddSignersFormSchema) => void; onSubmit: (_data: TAddSignersFormSchema) => void;
isDocumentPdfLoaded: boolean; isDocumentPdfLoaded: boolean;
documentId: number;
}; };
export const AddSignersFormPartial = ({ export const AddSignersFormPartial = ({
@ -62,6 +63,7 @@ export const AddSignersFormPartial = ({
isDocumentEnterprise, isDocumentEnterprise,
onSubmit, onSubmit,
isDocumentPdfLoaded, isDocumentPdfLoaded,
documentId,
}: AddSignersFormProps) => { }: AddSignersFormProps) => {
const { _ } = useLingui(); const { _ } = useLingui();
const { toast } = useToast(); const { toast } = useToast();
@ -634,6 +636,8 @@ export const AddSignersFormPartial = ({
'mb-6': form.formState.errors.signers?.[index], 'mb-6': form.formState.errors.signers?.[index],
})} })}
onDelete={() => onRemoveSigner(index)} onDelete={() => onRemoveSigner(index)}
signer={signer}
documentId={documentId}
deleteDisabled={ deleteDisabled={
snapshot.isDragging || snapshot.isDragging ||
isSubmitting || isSubmitting ||

View File

@ -6,24 +6,22 @@ import { ZRecipientActionAuthTypesSchema } from '@documenso/lib/types/document-a
import { ZMapNegativeOneToUndefinedSchema } from './add-settings.types'; import { ZMapNegativeOneToUndefinedSchema } from './add-settings.types';
import { DocumentSigningOrder, RecipientRole } from '.prisma/client'; 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 export const ZAddSignersFormSchema = z
.object({ .object({
signers: z.array( signers: z.array(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(),
),
}),
),
signingOrder: z.nativeEnum(DocumentSigningOrder), signingOrder: z.nativeEnum(DocumentSigningOrder),
}) })
.refine( .refine(
@ -37,3 +35,4 @@ export const ZAddSignersFormSchema = z
); );
export type TAddSignersFormSchema = z.infer<typeof ZAddSignersFormSchema>; export type TAddSignersFormSchema = z.infer<typeof ZAddSignersFormSchema>;
export type TAddSignerSchema = z.infer<typeof ZAddSignerSchema>;

View File

@ -1,12 +1,16 @@
'use client'; 'use client';
import { useRouter } from 'next/navigation';
import { zodResolver } from '@hookform/resolvers/zod'; 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 { format } from 'date-fns';
import { Calendar as CalendarIcon } from 'lucide-react'; import { Calendar as CalendarIcon } from 'lucide-react';
import { useForm } from 'react-hook-form'; import { useForm } from 'react-hook-form';
import * as z from 'zod'; import * as z from 'zod';
import { trpc } from '@documenso/trpc/react';
import { Button } from '@documenso/ui/primitives/button'; import { Button } from '@documenso/ui/primitives/button';
import { Calendar } from '@documenso/ui/primitives/calendar'; import { Calendar } from '@documenso/ui/primitives/calendar';
import { import {
@ -30,9 +34,11 @@ import {
import { Popover, PopoverContent, PopoverTrigger } from '@documenso/ui/primitives/popover'; import { Popover, PopoverContent, PopoverTrigger } from '@documenso/ui/primitives/popover';
import { cn } from '../../lib/utils'; import { cn } from '../../lib/utils';
import { useToast } from '../use-toast';
import type { TAddSignerSchema as Signer } from './add-signers.types';
const formSchema = z.object({ const formSchema = z.object({
expiryDate: z.date({ expiry: z.date({
required_error: 'Please select an expiry date.', required_error: 'Please select an expiry date.',
}), }),
}); });
@ -40,37 +46,61 @@ const formSchema = z.object({
type DocumentExpiryDialogProps = { type DocumentExpiryDialogProps = {
open: boolean; open: boolean;
onOpenChange: (_open: boolean) => void; 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<z.infer<typeof formSchema>>({ const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema), resolver: zodResolver(formSchema),
}); });
// const { mutateAsync: moveDocument, isLoading } = trpc.document.moveDocumentToTeam.useMutation({ const { mutateAsync: setSignerExpiry, isLoading } = trpc.recipient.setSignerExpiry.useMutation({
// onSuccess: () => { onSuccess: () => {
// router.refresh(); router.refresh();
// toast({ toast({
// title: _(msg`Document moved`), title: _(msg`Signer Expiry Set`),
// description: _(msg`The document has been successfully moved to the selected team.`), description: _(msg`The expiry date for the signer has been set.`),
// duration: 5000, duration: 5000,
// }); });
// onOpenChange(false); onOpenChange(false);
// }, },
// onError: (error) => { onError: (error) => {
// toast({ toast({
// title: _(msg`Error`), title: _(msg`Error`),
// description: error.message || _(msg`An error occurred while moving the document.`), description: error.message || _(msg`An error occurred while setting the expiry date.`),
// variant: 'destructive', variant: 'destructive',
// duration: 7500, duration: 7500,
// }); });
// }, },
// }); });
function onSubmit(values: z.infer<typeof formSchema>) { const onSetExpiry = async (values: z.infer<typeof formSchema>) => {
console.log(values); if (!signer.nativeId) {
onOpenChange(false); 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 ( return (
<Dialog open={open} onOpenChange={onOpenChange}> <Dialog open={open} onOpenChange={onOpenChange}>
@ -83,10 +113,10 @@ export default function DocumentExpiryDialog({ open, onOpenChange }: DocumentExp
</DialogDescription> </DialogDescription>
</DialogHeader> </DialogHeader>
<Form {...form}> <Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-8"> <form onSubmit={form.handleSubmit(onSetExpiry)} className="space-y-8">
<FormField <FormField
control={form.control} control={form.control}
name="expiryDate" name="expiry"
render={({ field }) => ( render={({ field }) => (
<FormItem className="flex flex-col"> <FormItem className="flex flex-col">
<FormLabel>Expiry Date</FormLabel> <FormLabel>Expiry Date</FormLabel>
@ -128,7 +158,7 @@ export default function DocumentExpiryDialog({ open, onOpenChange }: DocumentExp
<Trans>Cancel</Trans> <Trans>Cancel</Trans>
</Button> </Button>
</DialogClose> </DialogClose>
<Button type="submit"> <Button type="submit" loading={isLoading}>
<Trans>Save Changes</Trans> <Trans>Save Changes</Trans>
</Button> </Button>
</DialogFooter> </DialogFooter>

View File

@ -13,15 +13,23 @@ import {
} from '@documenso/ui/primitives/dropdown-menu'; } from '@documenso/ui/primitives/dropdown-menu';
import { cn } from '../../lib/utils'; import { cn } from '../../lib/utils';
import type { TAddSignerSchema as Signer } from './add-signers.types';
import DocumentExpiryDialog from './document-expiry-dialog'; import DocumentExpiryDialog from './document-expiry-dialog';
type SignerActionDropdownProps = { type SignerActionDropdownProps = {
onDelete: () => void; onDelete: () => void;
deleteDisabled?: boolean; deleteDisabled?: boolean;
className?: string; 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); const [isExpiryDialogOpen, setExpiryDialogOpen] = useState(false);
return ( return (
@ -45,7 +53,12 @@ export function SignerActionDropdown({ deleteDisabled, className }: SignerAction
</DropdownMenuContent> </DropdownMenuContent>
</DropdownMenu> </DropdownMenu>
</div> </div>
<DocumentExpiryDialog open={isExpiryDialogOpen} onOpenChange={setExpiryDialogOpen} /> <DocumentExpiryDialog
open={isExpiryDialogOpen}
onOpenChange={setExpiryDialogOpen}
signer={signer}
documentId={documentId}
/>
</> </>
); );
} }