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:
Syed Ali Shahbaz 2022-02-03 18:53:29 +05:30 committed by GitHub
parent c709f9ed1b
commit 675340cb73
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 94 additions and 19 deletions

View file

@ -48,34 +48,52 @@ type TimeRangeFieldProps = {
const TimeRangeField = ({ name }: TimeRangeFieldProps) => { const TimeRangeField = ({ name }: TimeRangeFieldProps) => {
// Lazy-loaded options, otherwise adding a field has a noticable redraw delay. // Lazy-loaded options, otherwise adding a field has a noticable redraw delay.
const [options, setOptions] = useState<Option[]>([]); const [options, setOptions] = useState<Option[]>([]);
const [selected, setSelected] = useState<number | undefined>();
// const { i18n } = useLocale(); // const { i18n } = useLocale();
const handleSelected = (value: number | undefined) => {
setSelected(value);
};
const getOption = (time: ConfigType) => ({ const getOption = (time: ConfigType) => ({
value: dayjs(time).toDate().valueOf(), value: dayjs(time).toDate().valueOf(),
label: dayjs(time).utc().format("HH:mm"), label: dayjs(time).utc().format("HH:mm"),
// .toLocaleTimeString(i18n.language, { minute: "numeric", hour: "numeric" }), // .toLocaleTimeString(i18n.language, { minute: "numeric", hour: "numeric" }),
}); });
const timeOptions = useCallback((offsetOrLimit: { offset?: number; limit?: number } = {}) => { const timeOptions = useCallback(
const { limit, offset } = offsetOrLimit; (offsetOrLimitorSelected: { offset?: number; limit?: number; selected?: number } = {}) => {
return TIMES.filter((time) => (!limit || time.isBefore(limit)) && (!offset || time.isAfter(offset))).map( const { limit, offset, selected } = offsetOrLimitorSelected;
(t) => getOption(t) return TIMES.filter(
(time) =>
(!limit || time.isBefore(limit)) &&
(!offset || time.isAfter(offset)) &&
(!selected || time.isAfter(selected))
).map((t) => getOption(t));
},
[]
); );
}, []);
return ( return (
<> <>
<Controller <Controller
name={`${name}.start`} name={`${name}.start`}
render={({ field: { onChange, value } }) => ( render={({ field: { onChange, value } }) => {
handleSelected(value);
return (
<Select <Select
className="w-[6rem]" className="w-[6rem]"
options={options} options={options}
onFocus={() => setOptions(timeOptions())} onFocus={() => setOptions(timeOptions())}
onBlur={() => setOptions([])} onBlur={() => setOptions([])}
defaultValue={getOption(value)} defaultValue={getOption(value)}
onChange={(option) => onChange(new Date(option?.value as number))} onChange={(option) => {
onChange(new Date(option?.value as number));
handleSelected(option?.value);
}}
/> />
)} );
}}
/> />
<span>-</span> <span>-</span>
<Controller <Controller
@ -84,7 +102,7 @@ const TimeRangeField = ({ name }: TimeRangeFieldProps) => {
<Select <Select
className="w-[6rem]" className="w-[6rem]"
options={options} options={options}
onFocus={() => setOptions(timeOptions())} onFocus={() => setOptions(timeOptions({ selected }))}
onBlur={() => setOptions([])} onBlur={() => setOptions([])}
defaultValue={getOption(value)} defaultValue={getOption(value)}
onChange={(option) => onChange(new Date(option?.value as number))} onChange={(option) => onChange(new Date(option?.value as number))}
@ -123,7 +141,7 @@ const ScheduleBlock = ({ name, day, weekday }: ScheduleBlockProps) => {
return ( return (
<fieldset className="flex flex-col space-y-2 sm:space-y-0 sm:flex-row justify-between py-5 min-h-[86px]"> <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"> <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 <input
type="checkbox" type="checkbox"
checked={fields.length > 0} checked={fields.length > 0}
@ -136,7 +154,7 @@ const ScheduleBlock = ({ name, day, weekday }: ScheduleBlockProps) => {
<div className="flex-grow"> <div className="flex-grow">
{fields.map((field, index) => ( {fields.map((field, index) => (
<div key={field.id} className="flex justify-between mb-1"> <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}`} /> <TimeRangeField name={`${name}.${day}.${index}`} />
</div> </div>
<Button <Button

View file

@ -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 { useForm } from "react-hook-form";
import { z } from "zod";
import { QueryCell } from "@lib/QueryCell"; import { QueryCell } from "@lib/QueryCell";
import { DEFAULT_SCHEDULE } from "@lib/availability"; import { DEFAULT_SCHEDULE } from "@lib/availability";
@ -9,9 +14,13 @@ import { Schedule as ScheduleType } from "@lib/types/schedule";
import Shell from "@components/Shell"; import Shell from "@components/Shell";
import { Form } from "@components/form/fields"; import { Form } from "@components/form/fields";
import { Alert } from "@components/ui/Alert";
import Button from "@components/ui/Button"; import Button from "@components/ui/Button";
import Schedule from "@components/ui/form/Schedule"; import Schedule from "@components/ui/form/Schedule";
dayjs.extend(utc);
dayjs.extend(timezone);
type FormValues = { type FormValues = {
schedule: ScheduleType; schedule: ScheduleType;
}; };
@ -36,10 +45,41 @@ export function AvailabilityForm(props: inferQueryOutput<"viewer.availability">)
return responseData.data; 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({ const form = useForm({
defaultValues: { defaultValues: {
schedule: props.schedule || DEFAULT_SCHEDULE, schedule: props.schedule || DEFAULT_SCHEDULE,
}, },
resolver: zodResolver(schema),
}); });
return ( 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> <h3 className="mb-5 text-base font-medium leading-6 text-gray-900">{t("change_start_end")}</h3>
<Schedule name="schedule" /> <Schedule name="schedule" />
</div> </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"> <div className="text-right">
<Button>{t("save")}</Button> <Button>{t("save")}</Button>
</div> </div>

View file

@ -231,6 +231,14 @@
"failed": "Failed", "failed": "Failed",
"password_has_been_reset_login": "Your password has been reset. You can now login with your newly created password.", "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.", "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", "back_to_bookings": "Back to bookings",
"free_to_pick_another_event_type": "Feel free to pick another event anytime.", "free_to_pick_another_event_type": "Feel free to pick another event anytime.",
"cancelled": "Cancelled", "cancelled": "Cancelled",