fix(blocker): disallow blocker end times before start time
This commit is contained in:
parent
6ae22a23c7
commit
c014b77f9b
2 changed files with 169 additions and 110 deletions
|
@ -25,14 +25,6 @@ export const BlockedSlotsSchema = zod
|
||||||
created_at: zod.date(),
|
created_at: zod.date(),
|
||||||
updated_at: zod.date(),
|
updated_at: zod.date(),
|
||||||
})
|
})
|
||||||
.refine(
|
|
||||||
(data) => {
|
|
||||||
return new Date(data.start_time) < new Date(data.end_time);
|
|
||||||
},
|
|
||||||
{
|
|
||||||
message: 'Start time must be before end time',
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.openapi('BlockedSlotsSchema', {
|
.openapi('BlockedSlotsSchema', {
|
||||||
description: 'Blocked time slot in the user calendar',
|
description: 'Blocked time slot in the user calendar',
|
||||||
});
|
});
|
||||||
|
@ -47,11 +39,37 @@ export const BlockedSlotResponseSchema = zod.object({
|
||||||
blocked_slot: BlockedSlotsSchema,
|
blocked_slot: BlockedSlotsSchema,
|
||||||
});
|
});
|
||||||
|
|
||||||
export const createBlockedSlotSchema = BlockedSlotsSchema.omit({
|
export const createBlockedSlotSchema = zod
|
||||||
id: true,
|
.object({
|
||||||
created_at: true,
|
start_time: eventStartTimeSchema,
|
||||||
updated_at: true,
|
end_time: eventEndTimeSchema,
|
||||||
});
|
reason: zod.string().nullish(),
|
||||||
|
})
|
||||||
|
.refine(
|
||||||
|
(data) => {
|
||||||
|
return new Date(data.start_time) < new Date(data.end_time);
|
||||||
|
},
|
||||||
|
{
|
||||||
|
message: 'Start time must be before end time',
|
||||||
|
path: ['end_time'],
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
export const createBlockedSlotClientSchema = zod
|
||||||
|
.object({
|
||||||
|
start_time: zod.iso.datetime({ local: true }),
|
||||||
|
end_time: zod.iso.datetime({ local: true }),
|
||||||
|
reason: zod.string().nullish(),
|
||||||
|
})
|
||||||
|
.refine(
|
||||||
|
(data) => {
|
||||||
|
return new Date(data.start_time) < new Date(data.end_time);
|
||||||
|
},
|
||||||
|
{
|
||||||
|
message: 'Start time must be before end time',
|
||||||
|
path: ['end_time'],
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
export const updateBlockedSlotSchema = zod.object({
|
export const updateBlockedSlotSchema = zod.object({
|
||||||
start_time: eventStartTimeSchema.optional(),
|
start_time: eventStartTimeSchema.optional(),
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
import useZodForm from '@/lib/hooks/useZodForm';
|
import useZodForm from '@/lib/hooks/useZodForm';
|
||||||
import {
|
import {
|
||||||
updateBlockedSlotSchema,
|
updateBlockedSlotSchema,
|
||||||
createBlockedSlotSchema,
|
createBlockedSlotClientSchema,
|
||||||
} from '@/app/api/blocked_slots/validation';
|
} from '@/app/api/blocked_slots/validation';
|
||||||
import {
|
import {
|
||||||
useGetApiBlockedSlotsSlotID,
|
useGetApiBlockedSlotsSlotID,
|
||||||
|
@ -40,12 +40,7 @@ export default function BlockedSlotForm({
|
||||||
handleSubmit: handleCreateSubmit,
|
handleSubmit: handleCreateSubmit,
|
||||||
formState: formStateCreate,
|
formState: formStateCreate,
|
||||||
reset: resetCreate,
|
reset: resetCreate,
|
||||||
} = useZodForm(
|
} = useZodForm(createBlockedSlotClientSchema);
|
||||||
createBlockedSlotSchema.extend({
|
|
||||||
start_time: eventStartTimeSchema.or(zod.iso.datetime({ local: true })),
|
|
||||||
end_time: eventStartTimeSchema.or(zod.iso.datetime({ local: true })),
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
register: registerUpdate,
|
register: registerUpdate,
|
||||||
|
@ -145,6 +140,7 @@ export default function BlockedSlotForm({
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (existingBlockedSlotId)
|
||||||
return (
|
return (
|
||||||
<div className='flex items-center justify-center h-full'>
|
<div className='flex items-center justify-center h-full'>
|
||||||
<Card className='w-[max(80%, 500px)] max-w-screen p-0 gap-0 max-xl:w-[95%] mx-auto'>
|
<Card className='w-[max(80%, 500px)] max-w-screen p-0 gap-0 max-xl:w-[95%] mx-auto'>
|
||||||
|
@ -154,55 +150,36 @@ export default function BlockedSlotForm({
|
||||||
<Logo colorType='monochrome' logoType='submark' width={50} />
|
<Logo colorType='monochrome' logoType='submark' width={50} />
|
||||||
</div>
|
</div>
|
||||||
<div className='items-center ml-auto mr-auto max-sm:mb-6 max-sm:w-full max-sm:flex max-sm:justify-center'>
|
<div className='items-center ml-auto mr-auto max-sm:mb-6 max-sm:w-full max-sm:flex max-sm:justify-center'>
|
||||||
<h1 className='text-center'>
|
<h1 className='text-center'>{'Update Blocker'}</h1>
|
||||||
{existingBlockedSlotId ? 'Update Blocker' : 'Create Blocker'}
|
|
||||||
</h1>
|
|
||||||
</div>
|
</div>
|
||||||
<div className='w-0 sm:w-[100px]'></div>
|
<div className='w-0 sm:w-[100px]'></div>
|
||||||
</div>
|
</div>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<form
|
<form onSubmit={onUpdateSubmit}>
|
||||||
onSubmit={existingBlockedSlotId ? onUpdateSubmit : onCreateSubmit}
|
|
||||||
>
|
|
||||||
<div className='grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3'>
|
<div className='grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3'>
|
||||||
<LabeledInput
|
<LabeledInput
|
||||||
label='Start Time'
|
label='Start Time'
|
||||||
type='datetime-local'
|
type='datetime-local'
|
||||||
id='start_time'
|
id='start_time'
|
||||||
{...(existingBlockedSlotId
|
{...registerUpdate('start_time')}
|
||||||
? registerUpdate('start_time')
|
error={formStateUpdate.errors.start_time?.message}
|
||||||
: registerCreate('start_time'))}
|
|
||||||
error={
|
|
||||||
formStateCreate.errors.start_time?.message ||
|
|
||||||
formStateUpdate.errors.start_time?.message
|
|
||||||
}
|
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
<LabeledInput
|
<LabeledInput
|
||||||
label='End Time'
|
label='End Time'
|
||||||
type='datetime-local'
|
type='datetime-local'
|
||||||
id='end_time'
|
id='end_time'
|
||||||
{...(existingBlockedSlotId
|
{...registerUpdate('end_time')}
|
||||||
? registerUpdate('end_time')
|
error={formStateUpdate.errors.end_time?.message}
|
||||||
: registerCreate('end_time'))}
|
|
||||||
error={
|
|
||||||
formStateCreate.errors.end_time?.message ||
|
|
||||||
formStateUpdate.errors.end_time?.message
|
|
||||||
}
|
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
<LabeledInput
|
<LabeledInput
|
||||||
label='Reason'
|
label='Reason'
|
||||||
type='text'
|
type='text'
|
||||||
id='reason'
|
id='reason'
|
||||||
{...(existingBlockedSlotId
|
{...registerUpdate('reason')}
|
||||||
? registerUpdate('reason')
|
error={formStateUpdate.errors.reason?.message}
|
||||||
: registerCreate('reason'))}
|
|
||||||
error={
|
|
||||||
formStateCreate.errors.reason?.message ||
|
|
||||||
formStateUpdate.errors.reason?.message
|
|
||||||
}
|
|
||||||
placeholder='Optional reason for blocking this slot'
|
placeholder='Optional reason for blocking this slot'
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
@ -210,11 +187,80 @@ export default function BlockedSlotForm({
|
||||||
<Button
|
<Button
|
||||||
type='submit'
|
type='submit'
|
||||||
variant='primary'
|
variant='primary'
|
||||||
disabled={
|
disabled={formStateUpdate.isSubmitting}
|
||||||
formStateCreate.isSubmitting || formStateUpdate.isSubmitting
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
{existingBlockedSlotId ? 'Update Blocker' : 'Create Blocker'}
|
{'Update Blocker'}
|
||||||
|
</Button>
|
||||||
|
{existingBlockedSlotId && (
|
||||||
|
<Button
|
||||||
|
type='button'
|
||||||
|
variant='destructive'
|
||||||
|
onClick={onDeleteSubmit}
|
||||||
|
>
|
||||||
|
Delete Blocker
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{formStateUpdate.errors.root && (
|
||||||
|
<p className='text-red-500 text-sm mt-1'>
|
||||||
|
{formStateUpdate.errors.root.message}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</form>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
else
|
||||||
|
return (
|
||||||
|
<div className='flex items-center justify-center h-full'>
|
||||||
|
<Card className='w-[max(80%, 500px)] max-w-screen p-0 gap-0 max-xl:w-[95%] mx-auto'>
|
||||||
|
<CardHeader className='p-0 m-0 gap-0 px-6'>
|
||||||
|
<div className='h-full mt-0 ml-2 mb-16 flex items-center justify-between max-sm:grid max-sm:grid-row-start:auto max-sm:mb-6 max-sm:mt-10 max-sm:ml-0'>
|
||||||
|
<div className='w-[100px] max-sm:w-full max-sm:flex max-sm:justify-center'>
|
||||||
|
<Logo colorType='monochrome' logoType='submark' width={50} />
|
||||||
|
</div>
|
||||||
|
<div className='items-center ml-auto mr-auto max-sm:mb-6 max-sm:w-full max-sm:flex max-sm:justify-center'>
|
||||||
|
<h1 className='text-center'>{'Create Blocker'}</h1>
|
||||||
|
</div>
|
||||||
|
<div className='w-0 sm:w-[100px]'></div>
|
||||||
|
</div>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<form onSubmit={onCreateSubmit}>
|
||||||
|
<div className='grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3'>
|
||||||
|
<LabeledInput
|
||||||
|
label='Start Time'
|
||||||
|
type='datetime-local'
|
||||||
|
id='start_time'
|
||||||
|
{...registerCreate('start_time')}
|
||||||
|
error={formStateCreate.errors.start_time?.message}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<LabeledInput
|
||||||
|
label='End Time'
|
||||||
|
type='datetime-local'
|
||||||
|
id='end_time'
|
||||||
|
{...registerCreate('end_time')}
|
||||||
|
error={formStateCreate.errors.end_time?.message}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<LabeledInput
|
||||||
|
label='Reason'
|
||||||
|
type='text'
|
||||||
|
id='reason'
|
||||||
|
{...registerCreate('reason')}
|
||||||
|
error={formStateCreate.errors.reason?.message}
|
||||||
|
placeholder='Optional reason for blocking this slot'
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className='flex justify-end gap-2 p-4'>
|
||||||
|
<Button
|
||||||
|
type='submit'
|
||||||
|
variant='primary'
|
||||||
|
disabled={formStateCreate.isSubmitting}
|
||||||
|
>
|
||||||
|
{'Create Blocker'}
|
||||||
</Button>
|
</Button>
|
||||||
{existingBlockedSlotId && (
|
{existingBlockedSlotId && (
|
||||||
<Button
|
<Button
|
||||||
|
@ -231,11 +277,6 @@ export default function BlockedSlotForm({
|
||||||
{formStateCreate.errors.root.message}
|
{formStateCreate.errors.root.message}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
{formStateUpdate.errors.root && (
|
|
||||||
<p className='text-red-500 text-sm mt-1'>
|
|
||||||
{formStateUpdate.errors.root.message}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</form>
|
</form>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue