availability: end time should not be lower than start time (#1673)
* added start-end time check * fixed init value for selected * added zod validation * cleanup Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
This commit is contained in:
		
							parent
							
								
									c709f9ed1b
								
							
						
					
					
						commit
						675340cb73
					
				
					 3 changed files with 94 additions and 19 deletions
				
			
		|  | @ -48,34 +48,52 @@ type TimeRangeFieldProps = { | |||
| const TimeRangeField = ({ name }: TimeRangeFieldProps) => { | ||||
|   // Lazy-loaded options, otherwise adding a field has a noticable redraw delay.
 | ||||
|   const [options, setOptions] = useState<Option[]>([]); | ||||
|   const [selected, setSelected] = useState<number | undefined>(); | ||||
|   // const { i18n } = useLocale();
 | ||||
| 
 | ||||
|   const handleSelected = (value: number | undefined) => { | ||||
|     setSelected(value); | ||||
|   }; | ||||
| 
 | ||||
|   const getOption = (time: ConfigType) => ({ | ||||
|     value: dayjs(time).toDate().valueOf(), | ||||
|     label: dayjs(time).utc().format("HH:mm"), | ||||
|     // .toLocaleTimeString(i18n.language, { minute: "numeric", hour: "numeric" }),
 | ||||
|   }); | ||||
| 
 | ||||
|   const timeOptions = useCallback((offsetOrLimit: { offset?: number; limit?: number } = {}) => { | ||||
|     const { limit, offset } = offsetOrLimit; | ||||
|     return TIMES.filter((time) => (!limit || time.isBefore(limit)) && (!offset || time.isAfter(offset))).map( | ||||
|       (t) => getOption(t) | ||||
|     ); | ||||
|   }, []); | ||||
|   const timeOptions = useCallback( | ||||
|     (offsetOrLimitorSelected: { offset?: number; limit?: number; selected?: number } = {}) => { | ||||
|       const { limit, offset, selected } = offsetOrLimitorSelected; | ||||
|       return TIMES.filter( | ||||
|         (time) => | ||||
|           (!limit || time.isBefore(limit)) && | ||||
|           (!offset || time.isAfter(offset)) && | ||||
|           (!selected || time.isAfter(selected)) | ||||
|       ).map((t) => getOption(t)); | ||||
|     }, | ||||
|     [] | ||||
|   ); | ||||
| 
 | ||||
|   return ( | ||||
|     <> | ||||
|       <Controller | ||||
|         name={`${name}.start`} | ||||
|         render={({ field: { onChange, value } }) => ( | ||||
|           <Select | ||||
|             className="w-[6rem]" | ||||
|             options={options} | ||||
|             onFocus={() => setOptions(timeOptions())} | ||||
|             onBlur={() => setOptions([])} | ||||
|             defaultValue={getOption(value)} | ||||
|             onChange={(option) => onChange(new Date(option?.value as number))} | ||||
|           /> | ||||
|         )} | ||||
|         render={({ field: { onChange, value } }) => { | ||||
|           handleSelected(value); | ||||
|           return ( | ||||
|             <Select | ||||
|               className="w-[6rem]" | ||||
|               options={options} | ||||
|               onFocus={() => setOptions(timeOptions())} | ||||
|               onBlur={() => setOptions([])} | ||||
|               defaultValue={getOption(value)} | ||||
|               onChange={(option) => { | ||||
|                 onChange(new Date(option?.value as number)); | ||||
|                 handleSelected(option?.value); | ||||
|               }} | ||||
|             /> | ||||
|           ); | ||||
|         }} | ||||
|       /> | ||||
|       <span>-</span> | ||||
|       <Controller | ||||
|  | @ -84,7 +102,7 @@ const TimeRangeField = ({ name }: TimeRangeFieldProps) => { | |||
|           <Select | ||||
|             className="w-[6rem]" | ||||
|             options={options} | ||||
|             onFocus={() => setOptions(timeOptions())} | ||||
|             onFocus={() => setOptions(timeOptions({ selected }))} | ||||
|             onBlur={() => setOptions([])} | ||||
|             defaultValue={getOption(value)} | ||||
|             onChange={(option) => onChange(new Date(option?.value as number))} | ||||
|  | @ -123,7 +141,7 @@ const ScheduleBlock = ({ name, day, weekday }: ScheduleBlockProps) => { | |||
|   return ( | ||||
|     <fieldset className="flex flex-col space-y-2 sm:space-y-0 sm:flex-row justify-between py-5 min-h-[86px]"> | ||||
|       <div className="w-1/3"> | ||||
|         <label className="flex items-center rtl:space-x-reverse space-x-2"> | ||||
|         <label className="flex items-center space-x-2 rtl:space-x-reverse"> | ||||
|           <input | ||||
|             type="checkbox" | ||||
|             checked={fields.length > 0} | ||||
|  | @ -136,7 +154,7 @@ const ScheduleBlock = ({ name, day, weekday }: ScheduleBlockProps) => { | |||
|       <div className="flex-grow"> | ||||
|         {fields.map((field, index) => ( | ||||
|           <div key={field.id} className="flex justify-between mb-1"> | ||||
|             <div className="flex items-center rtl:space-x-reverse space-x-2"> | ||||
|             <div className="flex items-center space-x-2 rtl:space-x-reverse"> | ||||
|               <TimeRangeField name={`${name}.${day}.${index}`} /> | ||||
|             </div> | ||||
|             <Button | ||||
|  |  | |||
|  | @ -1,4 +1,9 @@ | |||
| import { zodResolver } from "@hookform/resolvers/zod"; | ||||
| import dayjs from "dayjs"; | ||||
| import timezone from "dayjs/plugin/timezone"; | ||||
| import utc from "dayjs/plugin/utc"; | ||||
| import { useForm } from "react-hook-form"; | ||||
| import { z } from "zod"; | ||||
| 
 | ||||
| import { QueryCell } from "@lib/QueryCell"; | ||||
| import { DEFAULT_SCHEDULE } from "@lib/availability"; | ||||
|  | @ -9,9 +14,13 @@ import { Schedule as ScheduleType } from "@lib/types/schedule"; | |||
| 
 | ||||
| import Shell from "@components/Shell"; | ||||
| import { Form } from "@components/form/fields"; | ||||
| import { Alert } from "@components/ui/Alert"; | ||||
| import Button from "@components/ui/Button"; | ||||
| import Schedule from "@components/ui/form/Schedule"; | ||||
| 
 | ||||
| dayjs.extend(utc); | ||||
| dayjs.extend(timezone); | ||||
| 
 | ||||
| type FormValues = { | ||||
|   schedule: ScheduleType; | ||||
| }; | ||||
|  | @ -36,10 +45,41 @@ export function AvailabilityForm(props: inferQueryOutput<"viewer.availability">) | |||
|     return responseData.data; | ||||
|   }; | ||||
| 
 | ||||
|   const schema = z.object({ | ||||
|     schedule: z | ||||
|       .object({ | ||||
|         start: z.date(), | ||||
|         end: z.date(), | ||||
|       }) | ||||
|       .superRefine((val, ctx) => { | ||||
|         if (dayjs(val.end).isBefore(dayjs(val.start))) { | ||||
|           ctx.addIssue({ | ||||
|             code: z.ZodIssueCode.custom, | ||||
|             message: "Invalid entry: End time can not be before start time", | ||||
|             path: ["end"], | ||||
|           }); | ||||
|         } | ||||
|       }) | ||||
|       .optional() | ||||
|       .array() | ||||
|       .array(), | ||||
|   }); | ||||
| 
 | ||||
|   const days = [ | ||||
|     t("sunday_time_error"), | ||||
|     t("monday_time_error"), | ||||
|     t("tuesday_time_error"), | ||||
|     t("wednesday_time_error"), | ||||
|     t("thursday_time_error"), | ||||
|     t("friday_time_error"), | ||||
|     t("saturday_time_error"), | ||||
|   ]; | ||||
| 
 | ||||
|   const form = useForm({ | ||||
|     defaultValues: { | ||||
|       schedule: props.schedule || DEFAULT_SCHEDULE, | ||||
|     }, | ||||
|     resolver: zodResolver(schema), | ||||
|   }); | ||||
| 
 | ||||
|   return ( | ||||
|  | @ -54,6 +94,15 @@ export function AvailabilityForm(props: inferQueryOutput<"viewer.availability">) | |||
|           <h3 className="mb-5 text-base font-medium leading-6 text-gray-900">{t("change_start_end")}</h3> | ||||
|           <Schedule name="schedule" /> | ||||
|         </div> | ||||
|         {form.formState.errors.schedule && ( | ||||
|           <Alert | ||||
|             className="mt-1" | ||||
|             severity="error" | ||||
|             message={ | ||||
|               days[form.formState.errors.schedule.length - 1] + " : " + t("error_end_time_before_start_time") | ||||
|             } | ||||
|           /> | ||||
|         )} | ||||
|         <div className="text-right"> | ||||
|           <Button>{t("save")}</Button> | ||||
|         </div> | ||||
|  |  | |||
|  | @ -231,6 +231,14 @@ | |||
|   "failed": "Failed", | ||||
|   "password_has_been_reset_login": "Your password has been reset. You can now login with your newly created password.", | ||||
|   "unexpected_error_try_again": "An unexpected error occurred. Try again.", | ||||
|   "sunday_time_error":"Invalid time on Sunday", | ||||
|   "monday_time_error":"Invalid time on Monday", | ||||
|   "tuesday_time_error":"Invalid time on Tuesday", | ||||
|   "wednesday_time_error":"Invalid time on Wednesday", | ||||
|   "thursday_time_error":"Invalid time on Thursday", | ||||
|   "friday_time_error":"Invalid time on Friday", | ||||
|   "saturday_time_error":"Invalid time on Saturday", | ||||
|   "error_end_time_before_start_time": "End time cannot be before start time", | ||||
|   "back_to_bookings": "Back to bookings", | ||||
|   "free_to_pick_another_event_type": "Feel free to pick another event anytime.", | ||||
|   "cancelled": "Cancelled", | ||||
|  |  | |||
		Loading…
	
		Reference in a new issue
	
	 Syed Ali Shahbaz
						Syed Ali Shahbaz