## What does this PR do? Fixes all date shifts that are due browser time, by ensuring everything is UTC so it's not transformed to different times. The times weren't actually shifting, dates were, but after casting these to UTC properly the times were also shifting; so that was a bug after a bugfix, time wise it was only a problem to determine all day events (which didn't completely work) ### Yes, but how do I test this? Before: Create a all day date override in +05:30 Shift your local system time to -04:00 Note that the day is different After - do the same: The day is the same! Fixes #8365 Fixes #6978 Fixes #6482
205 lines
6.5 KiB
TypeScript
205 lines
6.5 KiB
TypeScript
import { useState, useMemo } from "react";
|
|
import { useForm } from "react-hook-form";
|
|
|
|
import type { Dayjs } from "@calcom/dayjs";
|
|
import dayjs from "@calcom/dayjs";
|
|
import { classNames } from "@calcom/lib";
|
|
import { daysInMonth, yyyymmdd } from "@calcom/lib/date-fns";
|
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
|
import useMediaQuery from "@calcom/lib/hooks/useMediaQuery";
|
|
import type { WorkingHours } from "@calcom/types/schedule";
|
|
import {
|
|
Dialog,
|
|
DialogContent,
|
|
DialogTrigger,
|
|
DialogHeader,
|
|
DialogClose,
|
|
Switch,
|
|
Form,
|
|
Button,
|
|
} from "@calcom/ui";
|
|
|
|
import DatePicker from "../../calendars/DatePicker";
|
|
import type { TimeRange } from "./Schedule";
|
|
import { DayRanges } from "./Schedule";
|
|
|
|
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
|
const noop = () => {};
|
|
|
|
const DateOverrideForm = ({
|
|
value,
|
|
workingHours,
|
|
excludedDates,
|
|
onChange,
|
|
onClose = noop,
|
|
}: {
|
|
workingHours?: WorkingHours[];
|
|
onChange: (newValue: TimeRange[]) => void;
|
|
excludedDates: string[];
|
|
value?: TimeRange[];
|
|
onClose?: () => void;
|
|
}) => {
|
|
const [browsingDate, setBrowsingDate] = useState<Dayjs>();
|
|
const { t, i18n, isLocaleReady } = useLocale();
|
|
const [datesUnavailable, setDatesUnavailable] = useState(
|
|
value &&
|
|
value[0].start.getUTCHours() === 0 &&
|
|
value[0].start.getUTCMinutes() === 0 &&
|
|
value[0].end.getUTCHours() === 0 &&
|
|
value[0].end.getUTCMinutes() === 0
|
|
);
|
|
|
|
const [date, setDate] = useState<Dayjs | null>(value ? dayjs.utc(value[0].start) : null);
|
|
const includedDates = useMemo(
|
|
() =>
|
|
workingHours
|
|
? workingHours.reduce((dates, workingHour) => {
|
|
for (let dNum = 1; dNum <= daysInMonth(browsingDate || dayjs()); dNum++) {
|
|
const d = browsingDate ? browsingDate.date(dNum) : dayjs.utc().date(dNum);
|
|
if (workingHour.days.includes(d.day())) {
|
|
dates.push(yyyymmdd(d));
|
|
}
|
|
}
|
|
return dates;
|
|
}, [] as string[])
|
|
: [],
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
[browsingDate]
|
|
);
|
|
|
|
const form = useForm({
|
|
values: {
|
|
range: value
|
|
? value.map((range) => ({
|
|
start: new Date(
|
|
dayjs
|
|
.utc()
|
|
.hour(range.start.getUTCHours())
|
|
.minute(range.start.getUTCMinutes())
|
|
.second(0)
|
|
.format()
|
|
),
|
|
end: new Date(
|
|
dayjs.utc().hour(range.end.getUTCHours()).minute(range.end.getUTCMinutes()).second(0).format()
|
|
),
|
|
}))
|
|
: (workingHours || []).reduce((dayRanges, workingHour) => {
|
|
if (date && workingHour.days.includes(date.day())) {
|
|
dayRanges.push({
|
|
start: dayjs.utc().startOf("day").add(workingHour.startTime, "minute").toDate(),
|
|
end: dayjs.utc().startOf("day").add(workingHour.endTime, "minute").toDate(),
|
|
});
|
|
}
|
|
return dayRanges;
|
|
}, [] as TimeRange[]),
|
|
},
|
|
});
|
|
|
|
return (
|
|
<Form
|
|
form={form}
|
|
handleSubmit={(values) => {
|
|
if (!date) return;
|
|
onChange(
|
|
(datesUnavailable
|
|
? [
|
|
{
|
|
start: date.utc(true).startOf("day").toDate(),
|
|
end: date.utc(true).startOf("day").add(1, "day").toDate(),
|
|
},
|
|
]
|
|
: values.range
|
|
).map((item) => ({
|
|
start: date.hour(item.start.getUTCHours()).minute(item.start.getUTCMinutes()).utc(true).toDate(),
|
|
end: date.hour(item.end.getUTCHours()).minute(item.end.getUTCMinutes()).utc(true).toDate(),
|
|
}))
|
|
);
|
|
onClose();
|
|
}}
|
|
className="p-6 sm:flex sm:p-0">
|
|
<div className={classNames(date && "sm:border-subtle w-full sm:border-r sm:pr-6", "sm:p-4 md:p-8")}>
|
|
<DialogHeader title={t("date_overrides_dialog_title")} />
|
|
<DatePicker
|
|
includedDates={includedDates}
|
|
excludedDates={excludedDates}
|
|
weekStart={0}
|
|
selected={date}
|
|
onChange={(day) => setDate(day)}
|
|
onMonthChange={(newMonth) => {
|
|
setBrowsingDate(newMonth);
|
|
}}
|
|
browsingDate={browsingDate}
|
|
locale={isLocaleReady ? i18n.language : "en"}
|
|
/>
|
|
</div>
|
|
{date && (
|
|
<div className="relative mt-8 flex w-full flex-col sm:mt-0 sm:p-4 md:p-8">
|
|
<div className="mb-4 flex-grow space-y-4">
|
|
<p className="text-medium text-emphasis text-sm">{t("date_overrides_dialog_which_hours")}</p>
|
|
<div>
|
|
{datesUnavailable ? (
|
|
<p className="text-subtle rounded border p-2 text-sm">{t("date_overrides_unavailable")}</p>
|
|
) : (
|
|
<DayRanges name="range" />
|
|
)}
|
|
</div>
|
|
<Switch
|
|
label={t("date_overrides_mark_all_day_unavailable_one")}
|
|
checked={datesUnavailable}
|
|
onCheckedChange={setDatesUnavailable}
|
|
data-testid="date-override-mark-unavailable"
|
|
/>
|
|
</div>
|
|
<div className="mt-4 flex flex-row-reverse sm:mt-0">
|
|
<Button
|
|
className="ml-2"
|
|
color="primary"
|
|
type="submit"
|
|
disabled={!date}
|
|
data-testid="add-override-submit-btn">
|
|
{value ? t("date_overrides_update_btn") : t("date_overrides_add_btn")}
|
|
</Button>
|
|
<DialogClose onClick={onClose} />
|
|
</div>
|
|
</div>
|
|
)}
|
|
</Form>
|
|
);
|
|
};
|
|
|
|
const DateOverrideInputDialog = ({
|
|
Trigger,
|
|
excludedDates = [],
|
|
...passThroughProps
|
|
}: {
|
|
workingHours: WorkingHours[];
|
|
excludedDates?: string[];
|
|
Trigger: React.ReactNode;
|
|
onChange: (newValue: TimeRange[]) => void;
|
|
value?: TimeRange[];
|
|
}) => {
|
|
const isMobile = useMediaQuery("(max-width: 768px)");
|
|
const [open, setOpen] = useState(false);
|
|
{
|
|
/* enableOverflow is used to allow overflow when there are too many overrides to show on mobile.
|
|
ref:- https://github.com/calcom/cal.com/pull/6215
|
|
*/
|
|
}
|
|
const enableOverflow = isMobile;
|
|
return (
|
|
<Dialog open={open} onOpenChange={setOpen}>
|
|
<DialogTrigger asChild>{Trigger}</DialogTrigger>
|
|
|
|
<DialogContent enableOverflow={enableOverflow} size="md" className="p-0">
|
|
<DateOverrideForm
|
|
excludedDates={excludedDates}
|
|
{...passThroughProps}
|
|
onClose={() => setOpen(false)}
|
|
/>
|
|
</DialogContent>
|
|
</Dialog>
|
|
);
|
|
};
|
|
|
|
export default DateOverrideInputDialog;
|