mirror of
https://github.com/documenso/documenso.git
synced 2025-11-19 03:01:59 +10:00
feat: period select for expiry
This commit is contained in:
16
packages/ui/lib/calculate-period.ts
Normal file
16
packages/ui/lib/calculate-period.ts
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import { differenceInDays, differenceInMonths, differenceInWeeks } from 'date-fns';
|
||||||
|
|
||||||
|
export const calculatePeriod = (expiryDate: Date) => {
|
||||||
|
const now = new Date();
|
||||||
|
const daysDiff = differenceInDays(expiryDate, now);
|
||||||
|
const weeksDiff = differenceInWeeks(expiryDate, now);
|
||||||
|
const monthsDiff = differenceInMonths(expiryDate, now);
|
||||||
|
|
||||||
|
if (monthsDiff > 0) {
|
||||||
|
return { amount: monthsDiff, unit: 'months' as const };
|
||||||
|
} else if (weeksDiff > 0) {
|
||||||
|
return { amount: weeksDiff, unit: 'weeks' as const };
|
||||||
|
} else {
|
||||||
|
return { amount: daysDiff, unit: 'days' as const };
|
||||||
|
}
|
||||||
|
};
|
||||||
@ -1,11 +1,13 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
|
import { useState } from 'react';
|
||||||
|
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
|
|
||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
import { Trans, msg } from '@lingui/macro';
|
import { Trans, msg } from '@lingui/macro';
|
||||||
import { useLingui } from '@lingui/react';
|
import { useLingui } from '@lingui/react';
|
||||||
import { format } from 'date-fns';
|
import { addDays, addMonths, addWeeks, 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';
|
||||||
@ -32,17 +34,31 @@ import {
|
|||||||
FormMessage,
|
FormMessage,
|
||||||
} from '@documenso/ui/primitives/form/form';
|
} from '@documenso/ui/primitives/form/form';
|
||||||
import { Popover, PopoverContent, PopoverTrigger } from '@documenso/ui/primitives/popover';
|
import { Popover, PopoverContent, PopoverTrigger } from '@documenso/ui/primitives/popover';
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from '@documenso/ui/primitives/select';
|
||||||
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@documenso/ui/primitives/tabs';
|
||||||
|
|
||||||
|
import { calculatePeriod } from '../../lib/calculate-period';
|
||||||
import { cn } from '../../lib/utils';
|
import { cn } from '../../lib/utils';
|
||||||
import { useToast } from '../use-toast';
|
import { useToast } from '../use-toast';
|
||||||
import type { TAddSignerSchema as Signer } from './add-signers.types';
|
import type { TAddSignerSchema as Signer } from './add-signers.types';
|
||||||
|
|
||||||
const formSchema = z.object({
|
const dateFormSchema = z.object({
|
||||||
expiry: z.date({
|
expiry: z.date({
|
||||||
required_error: 'Please select an expiry date.',
|
required_error: 'Please select an expiry date.',
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const periodFormSchema = z.object({
|
||||||
|
amount: z.number().min(1, 'Please enter a number greater than 0.'),
|
||||||
|
unit: z.enum(['days', 'weeks', 'months']),
|
||||||
|
});
|
||||||
|
|
||||||
type DocumentExpiryDialogProps = {
|
type DocumentExpiryDialogProps = {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
onOpenChange: (_open: boolean) => void;
|
onOpenChange: (_open: boolean) => void;
|
||||||
@ -56,21 +72,53 @@ export default function DocumentExpiryDialog({
|
|||||||
signer,
|
signer,
|
||||||
documentId,
|
documentId,
|
||||||
}: DocumentExpiryDialogProps) {
|
}: DocumentExpiryDialogProps) {
|
||||||
const { toast } = useToast();
|
|
||||||
const router = useRouter();
|
|
||||||
|
|
||||||
const { _ } = useLingui();
|
const { _ } = useLingui();
|
||||||
|
const router = useRouter();
|
||||||
|
const { toast } = useToast();
|
||||||
|
const [activeTab, setActiveTab] = useState<'date' | 'period'>('date');
|
||||||
|
|
||||||
const form = useForm<z.infer<typeof formSchema>>({
|
const dateForm = useForm<z.infer<typeof dateFormSchema>>({
|
||||||
resolver: zodResolver(formSchema),
|
resolver: zodResolver(dateFormSchema),
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
expiry: signer.expiry,
|
expiry: signer.expiry,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const periodForm = useForm<z.infer<typeof periodFormSchema>>({
|
||||||
|
resolver: zodResolver(periodFormSchema),
|
||||||
|
defaultValues: signer.expiry
|
||||||
|
? calculatePeriod(signer.expiry)
|
||||||
|
: {
|
||||||
|
amount: undefined,
|
||||||
|
unit: undefined,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const watchAmount = periodForm.watch('amount');
|
||||||
|
const watchUnit = periodForm.watch('unit');
|
||||||
|
|
||||||
const { mutateAsync: setSignerExpiry, isLoading } = trpc.recipient.setSignerExpiry.useMutation({
|
const { mutateAsync: setSignerExpiry, isLoading } = trpc.recipient.setSignerExpiry.useMutation({
|
||||||
onSuccess: () => {
|
onSuccess: ({ expired }) => {
|
||||||
router.refresh();
|
router.refresh();
|
||||||
|
|
||||||
|
periodForm.reset(
|
||||||
|
expired
|
||||||
|
? calculatePeriod(expired)
|
||||||
|
: {
|
||||||
|
amount: undefined,
|
||||||
|
unit: undefined,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
dateForm.reset(
|
||||||
|
{
|
||||||
|
expiry: expired ?? undefined,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
keepValues: false,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
toast({
|
toast({
|
||||||
title: _(msg`Signer Expiry Set`),
|
title: _(msg`Signer Expiry Set`),
|
||||||
description: _(msg`The expiry date for the signer has been set.`),
|
description: _(msg`The expiry date for the signer has been set.`),
|
||||||
@ -88,7 +136,9 @@ export default function DocumentExpiryDialog({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const onSetExpiry = async (values: z.infer<typeof formSchema>) => {
|
const onSetExpiry = async (
|
||||||
|
values: z.infer<typeof dateFormSchema> | z.infer<typeof periodFormSchema>,
|
||||||
|
) => {
|
||||||
if (!signer.nativeId) {
|
if (!signer.nativeId) {
|
||||||
return toast({
|
return toast({
|
||||||
title: _(msg`Error`),
|
title: _(msg`Error`),
|
||||||
@ -98,11 +148,38 @@ export default function DocumentExpiryDialog({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let expiryDate: Date;
|
||||||
|
|
||||||
|
if ('expiry' in values) {
|
||||||
|
expiryDate = values.expiry;
|
||||||
|
} else {
|
||||||
|
const now = new Date();
|
||||||
|
switch (values.unit) {
|
||||||
|
case 'days':
|
||||||
|
expiryDate = addDays(now, values.amount);
|
||||||
|
break;
|
||||||
|
case 'weeks':
|
||||||
|
expiryDate = addWeeks(now, values.amount);
|
||||||
|
break;
|
||||||
|
case 'months':
|
||||||
|
expiryDate = addMonths(now, values.amount);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('finalll expiry date', expiryDate);
|
||||||
|
|
||||||
await setSignerExpiry({
|
await setSignerExpiry({
|
||||||
documentId,
|
documentId,
|
||||||
signerId: signer.nativeId,
|
signerId: signer.nativeId,
|
||||||
expiry: new Date(values.expiry),
|
expiry: expiryDate,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// TODO: Implement logic to update expiry when resending document
|
||||||
|
// This should be handled on the server-side when a document is resent
|
||||||
|
|
||||||
|
// TODO: Implement logic to mark recipients as expired
|
||||||
|
// This should be a scheduled task or part of the completion process on the server
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -115,58 +192,143 @@ export default function DocumentExpiryDialog({
|
|||||||
to sign the document after this date.
|
to sign the document after this date.
|
||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
<Form {...form}>
|
<Tabs
|
||||||
<form onSubmit={form.handleSubmit(onSetExpiry)} className="space-y-8">
|
value={activeTab}
|
||||||
<FormField
|
onValueChange={(value) => {
|
||||||
control={form.control}
|
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
||||||
name="expiry"
|
return setActiveTab(value as 'date' | 'period');
|
||||||
render={({ field }) => (
|
}}
|
||||||
<FormItem className="flex flex-col">
|
>
|
||||||
<FormLabel>Expiry Date</FormLabel>
|
<TabsList className="grid w-full grid-cols-2">
|
||||||
<Popover>
|
<TabsTrigger value="date">Specific Date</TabsTrigger>
|
||||||
<PopoverTrigger asChild>
|
<TabsTrigger value="period">Time Period</TabsTrigger>
|
||||||
<FormControl>
|
</TabsList>
|
||||||
<Button
|
<TabsContent value="date">
|
||||||
variant={'outline'}
|
<Form {...dateForm}>
|
||||||
className={cn(
|
<form onSubmit={dateForm.handleSubmit(onSetExpiry)} className="space-y-8">
|
||||||
'w-[240px] pl-3 text-left font-normal',
|
<FormField
|
||||||
!field.value && 'text-muted-foreground',
|
control={dateForm.control}
|
||||||
)}
|
name="expiry"
|
||||||
>
|
render={({ field }) => (
|
||||||
{field.value ? format(field.value, 'PPP') : <span>Pick a date</span>}
|
<FormItem className="flex flex-col">
|
||||||
<CalendarIcon className="ml-auto h-4 w-4 opacity-50" />
|
<FormLabel>Expiry Date</FormLabel>
|
||||||
</Button>
|
<Popover>
|
||||||
</FormControl>
|
<PopoverTrigger asChild>
|
||||||
</PopoverTrigger>
|
<FormControl>
|
||||||
<PopoverContent className="z-[1100] w-auto p-0 " align="start">
|
<Button
|
||||||
<Calendar
|
variant={'outline'}
|
||||||
mode="single"
|
className={cn(
|
||||||
selected={field.value}
|
'w-[240px] pl-3 text-left font-normal',
|
||||||
onSelect={field.onChange}
|
!field.value && 'text-muted-foreground',
|
||||||
disabled={(date) => date < new Date() || date < new Date('1900-01-01')}
|
)}
|
||||||
initialFocus
|
>
|
||||||
/>
|
{field.value ? format(field.value, 'PPP') : <span>Pick a date</span>}
|
||||||
</PopoverContent>
|
<CalendarIcon className="ml-auto h-4 w-4 opacity-50" />
|
||||||
</Popover>
|
</Button>
|
||||||
<FormDescription>
|
</FormControl>
|
||||||
The document will expire at 11:59 PM on the selected date.
|
</PopoverTrigger>
|
||||||
</FormDescription>
|
<PopoverContent className="z-[1100] w-auto p-0" align="start">
|
||||||
<FormMessage />
|
<Calendar
|
||||||
</FormItem>
|
mode="single"
|
||||||
)}
|
selected={field.value}
|
||||||
/>
|
onSelect={field.onChange}
|
||||||
<DialogFooter>
|
disabled={(date) => date < new Date() || date < new Date('1900-01-01')}
|
||||||
<DialogClose asChild>
|
initialFocus
|
||||||
<Button type="button" variant="secondary">
|
/>
|
||||||
<Trans>Cancel</Trans>
|
</PopoverContent>
|
||||||
</Button>
|
</Popover>
|
||||||
</DialogClose>
|
<FormDescription>
|
||||||
<Button type="submit" loading={isLoading}>
|
The document will expire at 11:59 PM on the selected date.
|
||||||
<Trans>Save Changes</Trans>
|
</FormDescription>
|
||||||
</Button>
|
<FormMessage />
|
||||||
</DialogFooter>
|
</FormItem>
|
||||||
</form>
|
)}
|
||||||
</Form>
|
/>
|
||||||
|
<DialogFooter>
|
||||||
|
<DialogClose asChild>
|
||||||
|
<Button type="button" variant="secondary">
|
||||||
|
<Trans>Cancel</Trans>
|
||||||
|
</Button>
|
||||||
|
</DialogClose>
|
||||||
|
<Button type="submit" loading={isLoading}>
|
||||||
|
<Trans>Save Changes</Trans>
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</form>
|
||||||
|
</Form>
|
||||||
|
</TabsContent>
|
||||||
|
<TabsContent value="period">
|
||||||
|
<Form {...periodForm}>
|
||||||
|
<form onSubmit={periodForm.handleSubmit(onSetExpiry)} className="space-y-8">
|
||||||
|
<div className="flex space-x-4">
|
||||||
|
<FormField
|
||||||
|
control={periodForm.control}
|
||||||
|
name="amount"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem className="flex-1">
|
||||||
|
<FormLabel>Amount</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Select
|
||||||
|
onValueChange={(value) => field.onChange(parseInt(value, 10))}
|
||||||
|
value={watchAmount?.toString()}
|
||||||
|
>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue placeholder="Select amount" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{[1, 2, 3, 4, 5, 6, 7, 8, 9, 10].map((num) => (
|
||||||
|
<SelectItem key={num} value={num.toString()}>
|
||||||
|
{num}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<FormField
|
||||||
|
control={periodForm.control}
|
||||||
|
name="unit"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem className="flex-1">
|
||||||
|
<FormLabel>Unit</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Select onValueChange={field.onChange} value={watchUnit}>
|
||||||
|
{' '}
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue placeholder="Select unit" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="days">Days</SelectItem>
|
||||||
|
<SelectItem value="weeks">Weeks</SelectItem>
|
||||||
|
<SelectItem value="months">Months</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<FormDescription>
|
||||||
|
The document will expire after the selected time period from now.
|
||||||
|
</FormDescription>
|
||||||
|
<DialogFooter>
|
||||||
|
<DialogClose asChild>
|
||||||
|
<Button type="button" variant="secondary">
|
||||||
|
<Trans>Cancel</Trans>
|
||||||
|
</Button>
|
||||||
|
</DialogClose>
|
||||||
|
<Button type="submit" loading={isLoading}>
|
||||||
|
<Trans>Save Changes</Trans>
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</form>
|
||||||
|
</Form>
|
||||||
|
</TabsContent>
|
||||||
|
</Tabs>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user