* add Travel Schedule modal * UI to list schedules * set shouldDirty * add backend to add new travelSchedule * implement deleting a schedule * check if schedule is overlapping * WIP * fix finding overlapping travel schedule * only use travelSchedule when default availability is used * adjust date overrides to timezone schedule * first version of changeTimeZone cron job * fix tests by adding travelSchedules * fixes for cron job api call * improve unit tests * add migration * fix type error * fix collective-scheduling test * clean up cron job * code clean up * code clean up * show timezone from travel schedule for date override * add date override tests * show tz on date override only in default schedule * fix deleting old schedules * minor fixes in cron api handler * code clean up * code clean up from feedback * fix asia/kalkota comment * fix dark mode * fix start and end date conversion to utc * add first unit test for travel schedules * Fix modal render issue * show timezone city wtihout _ * fix dark more for datepicker * reset values after closing dialog * remove session from middleware * exit loop early once schedule is found * fix type error * add getTravelSchedules handler * clean up DatePicker * fix type error * code clean up * code clean up * add indexes * use deleteMany * fix icons --------- Co-authored-by: CarinaWolli <wollencarina@gmail.com> Co-authored-by: Alex van Andel <me@alexvanandel.com> Co-authored-by: Joe Au-Yeung <65426560+joeauyeung@users.noreply.github.com>
161 lines
4.9 KiB
TypeScript
161 lines
4.9 KiB
TypeScript
import type { FormValues } from "@pages/settings/my-account/general";
|
|
import { useState } from "react";
|
|
import type { UseFormSetValue } from "react-hook-form";
|
|
|
|
import dayjs from "@calcom/dayjs";
|
|
import { useTimePreferences } from "@calcom/features/bookings/lib/timePreferences";
|
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
|
import {
|
|
Dialog,
|
|
DialogContent,
|
|
DialogFooter,
|
|
DialogClose,
|
|
Button,
|
|
Label,
|
|
DateRangePicker,
|
|
TimezoneSelect,
|
|
SettingsToggle,
|
|
DatePicker,
|
|
} from "@calcom/ui";
|
|
|
|
interface TravelScheduleModalProps {
|
|
open: boolean;
|
|
onOpenChange: () => void;
|
|
setValue: UseFormSetValue<FormValues>;
|
|
existingSchedules: FormValues["travelSchedules"];
|
|
}
|
|
|
|
const TravelScheduleModal = ({
|
|
open,
|
|
onOpenChange,
|
|
setValue,
|
|
existingSchedules,
|
|
}: TravelScheduleModalProps) => {
|
|
const { t } = useLocale();
|
|
const { timezone: preferredTimezone } = useTimePreferences();
|
|
|
|
const [startDate, setStartDate] = useState<Date>(new Date());
|
|
const [endDate, setEndDate] = useState<Date | undefined>(new Date());
|
|
|
|
const [selectedTimeZone, setSelectedTimeZone] = useState(preferredTimezone);
|
|
const [isNoEndDate, setIsNoEndDate] = useState(false);
|
|
const [errorMessage, setErrorMessage] = useState("");
|
|
|
|
const isOverlapping = (newSchedule: { startDate: Date; endDate?: Date }) => {
|
|
const newStart = dayjs(newSchedule.startDate);
|
|
const newEnd = newSchedule.endDate ? dayjs(newSchedule.endDate) : null;
|
|
|
|
for (const schedule of existingSchedules) {
|
|
const start = dayjs(schedule.startDate);
|
|
const end = schedule.endDate ? dayjs(schedule.endDate) : null;
|
|
|
|
if (!newEnd) {
|
|
// if the start date is after or on the existing schedule's start date and before the existing schedule's end date (if it has one)
|
|
if (newStart.isSame(start) || newStart.isAfter(start)) {
|
|
if (!end || newStart.isSame(end) || newStart.isBefore(end)) return true;
|
|
}
|
|
} else {
|
|
// For schedules with an end date, check for any overlap
|
|
if (newStart.isSame(end) || newStart.isBefore(end) || end === null) {
|
|
if (newEnd.isSame(start) || newEnd.isAfter(start)) {
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
const resetValues = () => {
|
|
setStartDate(new Date());
|
|
setEndDate(new Date());
|
|
setSelectedTimeZone(preferredTimezone);
|
|
setIsNoEndDate(false);
|
|
};
|
|
|
|
const createNewSchedule = () => {
|
|
const newSchedule = {
|
|
startDate,
|
|
endDate,
|
|
timeZone: selectedTimeZone,
|
|
};
|
|
|
|
if (!isOverlapping(newSchedule)) {
|
|
setValue("travelSchedules", existingSchedules.concat(newSchedule), { shouldDirty: true });
|
|
onOpenChange();
|
|
resetValues();
|
|
} else {
|
|
setErrorMessage(t("overlaps_with_existing_schedule"));
|
|
}
|
|
};
|
|
|
|
return (
|
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
|
<DialogContent
|
|
title={t("travel_schedule")}
|
|
description={t("travel_schedule_description")}
|
|
type="creation">
|
|
<div>
|
|
{!isNoEndDate ? (
|
|
<>
|
|
<Label className="mt-2">{t("time_range")}</Label>
|
|
<DateRangePicker
|
|
startDate={startDate}
|
|
endDate={endDate ?? startDate}
|
|
onDatesChange={({ startDate: newStartDate, endDate: newEndDate }) => {
|
|
setStartDate(newStartDate);
|
|
setEndDate(newEndDate);
|
|
setErrorMessage("");
|
|
}}
|
|
/>
|
|
</>
|
|
) : (
|
|
<>
|
|
<Label className="mt-2">{t("date")}</Label>
|
|
<DatePicker
|
|
minDate={new Date()}
|
|
date={startDate}
|
|
className="w-56"
|
|
onDatesChange={(newDate) => {
|
|
setStartDate(newDate);
|
|
setErrorMessage("");
|
|
}}
|
|
/>
|
|
</>
|
|
)}
|
|
<div className="text-error mt-1 text-sm">{errorMessage}</div>
|
|
<div className="mt-3">
|
|
<SettingsToggle
|
|
labelClassName="mt-1 font-normal"
|
|
title={t("schedule_tz_without_end_date")}
|
|
checked={isNoEndDate}
|
|
onCheckedChange={(e) => {
|
|
setEndDate(!e ? startDate : undefined);
|
|
setIsNoEndDate(e);
|
|
setErrorMessage("");
|
|
}}
|
|
/>
|
|
</div>
|
|
<Label className="mt-6">{t("timezone")}</Label>
|
|
<TimezoneSelect
|
|
id="timeZone"
|
|
value={selectedTimeZone}
|
|
onChange={({ value }) => setSelectedTimeZone(value)}
|
|
className="mb-11 mt-2 w-full rounded-md text-sm"
|
|
/>
|
|
</div>
|
|
<DialogFooter showDivider className="relative">
|
|
<DialogClose />
|
|
<Button
|
|
onClick={() => {
|
|
createNewSchedule();
|
|
}}>
|
|
{t("add")}
|
|
</Button>
|
|
</DialogFooter>
|
|
</DialogContent>
|
|
</Dialog>
|
|
);
|
|
};
|
|
|
|
export default TravelScheduleModal;
|