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>
pull/1668/head
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) => {
// 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

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 { 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>

View File

@ -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",