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) => {
|
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
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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",
|
||||||
|
|
Loading…
Reference in a new issue