Files
calendar/apps/web/components/settings/TravelScheduleModal.tsx
T
Benny JooGitHubDevin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
15268bcfb4 refactor: move timezone-select, settings, and insights components from packages/features to apps/web/modules (#27227)
* refactor: move timezone-select, settings, and insights components from packages/features to apps/web/modules

Co-Authored-By: benny@cal.com <sldisek783@gmail.com>

* fix

* fix

* fix

* wip

* wip

* wip

* wip

* wip

* wip

* wip

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-01-26 12:05:18 +00:00

192 lines
6.5 KiB
TypeScript

import dayjs from "@calcom/dayjs";
import { useTimePreferences } from "@calcom/features/bookings/lib/timePreferences";
import { TimezoneSelect } from "@calcom/web/modules/timezone/components/TimezoneSelect";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { DatePicker, SettingsToggle } from "@calcom/ui/components/form";
import { DatePickerWithRange as DateRangePicker } from "@calcom/ui/components/form/date-range-picker/DateRangePicker";
import { Button } from "@coss/ui/components/button";
import {
Dialog,
DialogClose,
DialogDescription,
DialogFooter,
DialogHeader,
DialogPanel,
DialogPopup,
DialogTitle,
} from "@coss/ui/components/dialog";
import { Label } from "@coss/ui/components/label";
import { useIsMobile } from "@coss/ui/hooks/use-mobile";
import { useState } from "react";
import type { UseFormSetValue } from "react-hook-form";
import type { FormValues } from "~/settings/my-account/general-view";
interface TravelScheduleModalProps {
open: boolean;
onOpenChange: (open: boolean) => void;
setValue: UseFormSetValue<FormValues>;
existingSchedules: FormValues["travelSchedules"];
}
const TravelScheduleModal = ({
open,
onOpenChange,
setValue,
existingSchedules,
}: TravelScheduleModalProps) => {
const { t } = useLocale();
const { timezone: preferredTimezone } = useTimePreferences();
const isMobile = useIsMobile();
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 [isDateRangeOpen, setIsDateRangeOpen] = 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);
setIsDateRangeOpen(false);
};
const createNewSchedule = () => {
const newSchedule = {
startDate,
endDate,
timeZone: selectedTimeZone,
};
if (!isOverlapping(newSchedule)) {
setValue("travelSchedules", existingSchedules.concat(newSchedule), { shouldDirty: true });
onOpenChange(false);
resetValues();
} else {
setErrorMessage(t("overlaps_with_existing_schedule"));
}
};
return (
<Dialog
open={open}
disablePointerDismissal={isDateRangeOpen}
onOpenChange={(nextOpen) => {
onOpenChange(nextOpen);
if (!nextOpen) {
setIsDateRangeOpen(false);
}
}}>
<DialogPopup>
<DialogHeader>
<DialogTitle>{t("travel_schedule")}</DialogTitle>
<DialogDescription>{t("travel_schedule_description")}</DialogDescription>
</DialogHeader>
<DialogPanel>
<div>
{!isNoEndDate ? (
<>
<Label className="mt-2">{t("time_range")}</Label>
<DateRangePicker
dates={{
startDate,
endDate,
}}
popoverModal={isMobile}
onPopoverOpenChange={setIsDateRangeOpen}
onDatesChange={({ startDate: newStartDate, endDate: newEndDate }) => {
// If newStartDate does become undefined - we resort back to to-todays date
setStartDate(newStartDate ?? new Date());
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={(checked) => {
setEndDate(!checked ? startDate : undefined);
setIsNoEndDate(checked);
if (checked) {
setIsDateRangeOpen(false);
}
setErrorMessage("");
}}
/>
</div>
<Label className="mt-6">{t("timezone")}</Label>
<TimezoneSelect
id="timeZone"
value={selectedTimeZone}
onChange={({ value }) => setSelectedTimeZone(value)}
menuPortalTarget={typeof document === "undefined" ? undefined : document.body}
menuPlacement={isMobile ? "top" : "auto"}
styles={{ menuPortal: (base) => Object.assign({}, base, { zIndex: 9999 }) }}
className="mb-11 mt-2 w-full rounded-md text-sm"
/>
</div>
</DialogPanel>
<DialogFooter>
<DialogClose render={<Button variant="ghost" />}>{t("cancel")}</DialogClose>
<Button
disabled={isNoEndDate ? !startDate : !startDate || !endDate}
onClick={() => {
createNewSchedule();
}}>
{t("add")}
</Button>
</DialogFooter>
</DialogPopup>
</Dialog>
);
};
export default TravelScheduleModal;