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
parent
c709f9ed1b
commit
675340cb73
|
@ -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 New Issue