From c3094be15d9443d05e317fb327f27496f00e96f3 Mon Sep 17 00:00:00 2001 From: Rares Mardare Date: Mon, 17 Jun 2024 16:30:08 +0300 Subject: [PATCH] Make rotation start and rotation end take timezone into consideration (#4481) # What this PR does - Fixes Rotation Start and Rotation End to take selected offset into consideration - Fixed issue where week/month period was being restored when the offset was being changed (now it defaults to start of week, midnight, in selected timezone offset) Related to https://github.com/grafana/oncall/issues/4428 --- src/containers/Rotation/Rotation.tsx | 4 +- .../RotationForm/RotationForm.helpers.test.ts | 21 +--- .../RotationForm/RotationForm.helpers.ts | 34 +----- src/containers/RotationForm/RotationForm.tsx | 103 ++++++++++++------ .../RotationForm/ScheduleOverrideForm.tsx | 2 +- src/containers/RotationForm/ShiftSwapForm.tsx | 2 +- .../RotationForm/parts/DateTimePicker.tsx | 55 +++++----- src/containers/Rotations/Rotations.helpers.ts | 32 ++++++ src/containers/Rotations/Rotations.tsx | 33 ++++-- src/containers/Rotations/ScheduleFinal.tsx | 4 +- src/containers/Rotations/SchedulePersonal.tsx | 4 +- src/models/timezone/timezone.ts | 17 ++- src/pages/schedule/Schedule.helpers.ts | 35 +++++- src/pages/schedule/Schedule.tsx | 26 +++-- 14 files changed, 221 insertions(+), 151 deletions(-) diff --git a/src/containers/Rotation/Rotation.tsx b/src/containers/Rotation/Rotation.tsx index 6f56d6a02c..4210eaf16a 100644 --- a/src/containers/Rotation/Rotation.tsx +++ b/src/containers/Rotation/Rotation.tsx @@ -40,7 +40,7 @@ interface RotationProps { export const Rotation: FC = observer((props) => { const { - timezoneStore: { calendarStartDate, getDateInSelectedTimezone }, + timezoneStore: { calendarStartDate, getDateInSelectedTimezone, selectedTimezoneOffset }, scheduleStore: { scheduleView: storeScheduleView }, } = useStore(); const { @@ -144,7 +144,7 @@ export const Rotation: FC = observer((props) => { const base = 60 * 60 * 24 * days; return firstShiftOffset / base; - }, [events, startDate]); + }, [events, startDate, selectedTimezoneOffset]); return (
diff --git a/src/containers/RotationForm/RotationForm.helpers.test.ts b/src/containers/RotationForm/RotationForm.helpers.test.ts index 6814710f10..d36e72c2b1 100644 --- a/src/containers/RotationForm/RotationForm.helpers.test.ts +++ b/src/containers/RotationForm/RotationForm.helpers.test.ts @@ -2,31 +2,12 @@ import dayjs, { Dayjs } from 'dayjs'; import timezone from 'dayjs/plugin/timezone'; import utc from 'dayjs/plugin/utc'; -import { dayJSAddWithDSTFixed, getDateForDatePicker } from './RotationForm.helpers'; +import { dayJSAddWithDSTFixed } from './RotationForm.helpers'; dayjs.extend(timezone); dayjs.extend(utc); describe('RotationForm helpers', () => { - describe('getDateForDatePicker()', () => { - it(`should return the same regular JS Date as input dayJsDate - even if selected day of month doesn't exist in current month - (in this case there is no 30th Feb and it should still work ok)`, () => { - jest.useFakeTimers().setSystemTime(new Date('2024-02-01')); - - const inputDate = dayjs() - .utcOffset(360) - .set('year', 2024) - .set('month', 3) // 0-indexed so April - .set('date', 30) - .set('hour', 12) - .set('minute', 20); - const result = getDateForDatePicker(inputDate); - - expect(result.toString()).toContain('Tue Apr 30 2024'); - }); - }); - describe('dayJSAddWithDSTFixed() @london-tz', () => { it(`corrects resulting hour to be the same as in input if start date is before London DST (GMT + 0) and resulting date is within London DST (GMT + 1)`, () => { diff --git a/src/containers/RotationForm/RotationForm.helpers.ts b/src/containers/RotationForm/RotationForm.helpers.ts index b0651d1608..da86c3b551 100644 --- a/src/containers/RotationForm/RotationForm.helpers.ts +++ b/src/containers/RotationForm/RotationForm.helpers.ts @@ -1,6 +1,4 @@ -import dayjs, { Dayjs, ManipulateType } from 'dayjs'; - -import { Timezone } from 'models/timezone/timezone.types'; +import { Dayjs, ManipulateType } from 'dayjs'; import { RepeatEveryPeriod } from './RotationForm.types'; @@ -11,19 +9,6 @@ export const getRepeatShiftsEveryOptions = (repeatEveryPeriod: number) => { .map((i) => ({ label: String(i), value: i })); }; -export const toDate = (moment: dayjs.Dayjs, timezone: Timezone) => { - const localMoment = moment.tz(timezone); - - return new Date( - localMoment.get('year'), - localMoment.get('month'), - localMoment.get('date'), - localMoment.get('hour'), - localMoment.get('minute'), - localMoment.get('second') - ); -}; - export interface TimeUnit { unit: RepeatEveryPeriod; value: number; @@ -171,23 +156,6 @@ export const repeatEveryInSeconds = (repeatEveryPeriod: RepeatEveryPeriod, repea return repeatEveryPeriodMultiplier[repeatEveryPeriod] * repeatEveryValue; }; -export const getDateForDatePicker = (dayJsDate: Dayjs) => { - const date = new Date(); - // Day of the month needs to be set to 1st day at first to prevent incorrect month increment - // when selected day of month doesn't exist in current month - // E.g. selected date is 30th March and current month is Feb, so in this case date.setMonth(2) results in April - - date.setDate(1); // temporary selection to prevent incorrect month increment - - date.setFullYear(dayJsDate.year()); - date.setMonth(dayJsDate.month()); - date.setDate(dayJsDate.date()); - date.setHours(dayJsDate.hour()); - date.setMinutes(dayJsDate.minute()); - date.setSeconds(dayJsDate.second()); - return date; -}; - export const dayJSAddWithDSTFixed = ({ baseDate, addParams, diff --git a/src/containers/RotationForm/RotationForm.tsx b/src/containers/RotationForm/RotationForm.tsx index c9c6376add..b068f68e94 100644 --- a/src/containers/RotationForm/RotationForm.tsx +++ b/src/containers/RotationForm/RotationForm.tsx @@ -1,6 +1,7 @@ import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { + Alert, Button, Field, HorizontalGroup, @@ -52,11 +53,11 @@ import { ApiSchemas } from 'network/oncall-api/api.types'; import { getDateTime, getSelectedDays, - getStartOfWeekBasedOnCurrentDate, getUTCByDay, getUTCString, getUTCWeekStart, getWeekStartString, + toDateWithTimezoneOffset, } from 'pages/schedule/Schedule.helpers'; import { isTopNavbar } from 'plugin/GrafanaPluginRootPage.helpers'; import { useStore } from 'state/useStore'; @@ -82,8 +83,25 @@ interface RotationFormProps { onShowRotationForm: (shiftId: Shift['id']) => void; } +const getStartShift = (start: dayjs.Dayjs, timezoneOffset: number, isNewRotation = false) => { + if (isNewRotation) { + // all new rotations default to midnight in selected timezone offset + return toDateWithTimezoneOffset(start, timezoneOffset) + .set('date', 1) + .set('year', start.year()) + .set('month', start.month()) + .set('date', start.date()) + .set('hour', 0) + .set('minute', 0) + .set('second', 0); + } + + return toDateWithTimezoneOffset(start, timezoneOffset); +}; + export const RotationForm = observer((props: RotationFormProps) => { const store = useStore(); + const { onHide, onCreate, @@ -92,7 +110,7 @@ export const RotationForm = observer((props: RotationFormProps) => { onDelete, layerPriority, shiftId, - shiftStart: propsShiftStart = getStartOfWeekBasedOnCurrentDate(store.timezoneStore.currentDateInSelectedTimezone), + shiftStart: propsShiftStart = store.timezoneStore.calendarStartDate, shiftEnd: propsShiftEnd, shiftColor = '#3D71D9', onShowRotationForm, @@ -101,7 +119,6 @@ export const RotationForm = observer((props: RotationFormProps) => { const shift = store.scheduleStore.shifts[shiftId]; const [errors, setErrors] = useState<{ [key: string]: string[] }>({}); - const [bounds, setDraggableBounds] = useState<{ left: number; right: number; top: number; bottom: number }>( undefined ); @@ -111,13 +128,19 @@ export const RotationForm = observer((props: RotationFormProps) => { const [offsetTop, setOffsetTop] = useState(GRAFANA_HEADER_HEIGHT + 10); const [draggablePosition, setDraggablePosition] = useState<{ x: number; y: number }>(undefined); - const [shiftStart, setShiftStart] = useState(propsShiftStart); - const [shiftEnd, setShiftEnd] = useState(propsShiftEnd || shiftStart.add(1, 'day')); + const [shiftStart, setShiftStart] = useState( + getStartShift(propsShiftStart, store.timezoneStore.selectedTimezoneOffset, shiftId === 'new') + ); + + const [shiftEnd, setShiftEnd] = useState( + propsShiftEnd?.utcOffset(store.timezoneStore.selectedTimezoneOffset) || shiftStart.add(1, 'day') + ); + const [activePeriod, setActivePeriod] = useState(undefined); const [shiftPeriodDefaultValue, setShiftPeriodDefaultValue] = useState(undefined); const [rotationStart, setRotationStart] = useState(shiftStart); - const [endLess, setEndless] = useState(true); + const [endLess, setEndless] = useState(shift?.until === undefined ? true : !Boolean(shift.until)); const [rotationEnd, setRotationEnd] = useState(shiftStart.add(1, 'month')); const [repeatEveryValue, setRepeatEveryValue] = useState(1); @@ -248,10 +271,11 @@ export const RotationForm = observer((props: RotationFormProps) => { shift, endLess, rotationName, + store.timezoneStore.selectedTimezoneOffset, ] ); - useEffect(handleChange, [params, store.timezoneStore.calendarStartDate]); + useEffect(handleChange, [params, store.timezoneStore.calendarStartDate, store.timezoneStore.selectedTimezoneOffset]); const create = useCallback(async () => { try { @@ -330,28 +354,22 @@ export const RotationForm = observer((props: RotationFormProps) => { } }; - const handleRotationStartChange = useCallback( - (value) => { - setRotationStart(value); - setShiftStart(value); - if (showActiveOnSelectedPartOfDay) { - setShiftEnd( - dayJSAddWithDSTFixed({ + const handleRotationStartChange = (value: dayjs.Dayjs) => { + setRotationStart(value); + setShiftStart(value); + + setShiftEnd( + showActiveOnSelectedPartOfDay + ? dayJSAddWithDSTFixed({ baseDate: value, addParams: [activePeriod, 'seconds'], }) - ); - } else { - setShiftEnd( - dayJSAddWithDSTFixed({ + : dayJSAddWithDSTFixed({ baseDate: value, addParams: [repeatEveryValue, repeatEveryPeriodToUnitName[repeatEveryPeriod]], }) - ); - } - }, - [showActiveOnSelectedPartOfDay, activePeriod, repeatEveryPeriod, repeatEveryValue] - ); + ); + }; const handleActivePeriodChange = useCallback( (value) => { @@ -435,15 +453,23 @@ export const RotationForm = observer((props: RotationFormProps) => { useEffect(() => { if (shift) { setRotationName(getShiftName(shift)); - const shiftStart = getDateTime(shift.shift_start); + // use shiftStart as rotationStart for existing shifts // (original rotationStart defaulted to the shift creation timestamp) + const shiftStart = toDateWithTimezoneOffset(dayjs(shift.shift_start), store.timezoneStore.selectedTimezoneOffset); + setRotationStart(shiftStart); - setRotationEnd(shift.until ? getDateTime(shift.until) : getDateTime(shift.shift_start).add(1, 'month')); + setRotationEnd( + toDateWithTimezoneOffset( + // always keep the date offseted + shift.until ? getDateTime(shift.until) : getDateTime(shift.shift_start).add(1, 'month'), + store.timezoneStore.selectedTimezoneOffset + ) + ); setShiftStart(shiftStart); - const shiftEnd = getDateTime(shift.shift_end); + + const shiftEnd = toDateWithTimezoneOffset(dayjs(shift.shift_end), store.timezoneStore.selectedTimezoneOffset); setShiftEnd(shiftEnd); - setEndless(!shift.until); setRepeatEveryValue(shift.interval); setRepeatEveryPeriod(shift.frequency); @@ -475,6 +501,10 @@ export const RotationForm = observer((props: RotationFormProps) => { useEffect(() => { if (shift) { + // for existing rotations + handleRotationStartChange(toDateWithTimezoneOffset(rotationStart, store.timezoneStore.selectedTimezoneOffset)); + setRotationEnd(toDateWithTimezoneOffset(rotationEnd, store.timezoneStore.selectedTimezoneOffset)); + setSelectedDays( getSelectedDays({ dayOptions: store.scheduleStore.byDayOptions, @@ -482,6 +512,14 @@ export const RotationForm = observer((props: RotationFormProps) => { moment: store.timezoneStore.getDateInSelectedTimezone(shiftStart), }) ); + } else { + // for new rotations + handleRotationStartChange(toDateWithTimezoneOffset(rotationStart, store.timezoneStore.selectedTimezoneOffset)); + + setShiftEnd(toDateWithTimezoneOffset(shiftEnd, store.timezoneStore.selectedTimezoneOffset)); + + // not behind an "if" such that it will reflect correct value after toggle gets switched + setRotationEnd(toDateWithTimezoneOffset(rotationEnd, store.timezoneStore.selectedTimezoneOffset)); } }, [store.timezoneStore.selectedTimezoneOffset]); @@ -561,14 +599,11 @@ export const RotationForm = observer((props: RotationFormProps) => { )} {!hasUpdatedShift && ended && ( - +
- - - This rotation is over - + This rotation is over) as unknown as string} /> - +
)}
{ > { ) : ( = (props) => { const handleChange = useDebouncedCallback(updatePreview, 200); - useEffect(handleChange, [params, store.timezoneStore.calendarStartDate]); + useEffect(handleChange, [params, store.timezoneStore.calendarStartDate, store.timezoneStore.selectedTimezoneOffset]); const isFormValid = useMemo(() => !Object.keys(errors).length, [errors]); diff --git a/src/containers/RotationForm/ShiftSwapForm.tsx b/src/containers/RotationForm/ShiftSwapForm.tsx index 7795b4ddd7..dee6d5f65b 100644 --- a/src/containers/RotationForm/ShiftSwapForm.tsx +++ b/src/containers/RotationForm/ShiftSwapForm.tsx @@ -87,7 +87,7 @@ export const ShiftSwapForm = (props: ShiftSwapFormProps) => { ...shiftSwap, }); } - }, [shiftSwap, store.timezoneStore.calendarStartDate]); + }, [shiftSwap, store.timezoneStore.calendarStartDate, store.timezoneStore.selectedTimezoneOffset]); const handleDescriptionChange = useCallback( (event) => { diff --git a/src/containers/RotationForm/parts/DateTimePicker.tsx b/src/containers/RotationForm/parts/DateTimePicker.tsx index 6dc88a2687..3c90b836b9 100644 --- a/src/containers/RotationForm/parts/DateTimePicker.tsx +++ b/src/containers/RotationForm/parts/DateTimePicker.tsx @@ -8,8 +8,8 @@ import dayjs from 'dayjs'; import { observer } from 'mobx-react'; import { Text } from 'components/Text/Text'; -import { getDateForDatePicker } from 'containers/RotationForm/RotationForm.helpers'; -import { useStore } from 'state/useStore'; +import { toDatePickerDate } from 'containers/Rotations/Rotations.helpers'; +import { toDateWithTimezoneOffset } from 'pages/schedule/Schedule.helpers'; import styles from 'containers/RotationForm/RotationForm.module.css'; @@ -17,6 +17,7 @@ const cx = cn.bind(styles); interface DateTimePickerProps { value: dayjs.Dayjs; + utcOffset?: number; onChange: (value: dayjs.Dayjs) => void; disabled?: boolean; onFocus?: () => void; @@ -25,39 +26,37 @@ interface DateTimePickerProps { } export const DateTimePicker = observer( - ({ value: propValue, onChange, disabled, onFocus, onBlur, error }: DateTimePickerProps) => { + ({ value: propValue, utcOffset, onChange, disabled, onFocus, onBlur, error }: DateTimePickerProps) => { const styles = useStyles2(getStyles); - const { - timezoneStore: { getDateInSelectedTimezone }, - } = useStore(); - const valueInSelectedTimezone = getDateInSelectedTimezone(propValue); - const valueAsDate = valueInSelectedTimezone.toDate(); - const handleDateChange = (newDate: Date) => { - const localMoment = getDateInSelectedTimezone(dayjs(newDate)); - const newValue = localMoment - .set('year', newDate.getFullYear()) - .set('month', newDate.getMonth()) - .set('date', newDate.getDate()) - .set('hour', valueAsDate.getHours()) - .set('minute', valueAsDate.getMinutes()) - .set('second', valueAsDate.getSeconds()); + const handleDateChange = (value: Date) => { + const newDate = toDateWithTimezoneOffset(dayjs(value), utcOffset) + .set('date', 1) + .set('months', value.getMonth()) + .set('date', value.getDate()) + .set('hours', propValue.hour()) + .set('minutes', propValue.minute()) + .set('second', 0) + .set('milliseconds', 0); - onChange(newValue); + onChange(newDate); }; - const handleTimeChange = (newMoment: DateTime) => { - const selectedHour = newMoment.hour(); - const selectedMinute = newMoment.minute(); - const newValue = valueInSelectedTimezone.set('hour', selectedHour).set('minute', selectedMinute); - onChange(newValue); + const handleTimeChange = (timeMoment: DateTime) => { + const newDate = toDateWithTimezoneOffset(propValue, utcOffset) + .set('hour', timeMoment.hour()) + .set('minute', timeMoment.minute()); + + onChange(newDate); }; const getTimeValueInSelectedTimezone = () => { - const time = dateTime(valueInSelectedTimezone.format()); - time.set('hour', valueInSelectedTimezone.hour()); - time.set('minute', valueInSelectedTimezone.minute()); - time.set('second', valueInSelectedTimezone.second()); + const dateInOffset = toDateWithTimezoneOffset(propValue, utcOffset); + + const time = dateTime(dateInOffset.format()); + time.set('hour', dateInOffset.hour()); + time.set('minute', dateInOffset.minute()); + time.set('seconds', dateInOffset.second()); return time; }; @@ -73,7 +72,7 @@ export const DateTimePicker = observer(
diff --git a/src/containers/Rotations/Rotations.helpers.ts b/src/containers/Rotations/Rotations.helpers.ts index 9d2a47d2cc..ac3bc85795 100644 --- a/src/containers/Rotations/Rotations.helpers.ts +++ b/src/containers/Rotations/Rotations.helpers.ts @@ -3,6 +3,38 @@ import dayjs from 'dayjs'; import { getColor, getOverrideColor } from 'models/schedule/schedule.helpers'; import { Layer, Shift } from 'models/schedule/schedule.types'; import { ApiSchemas } from 'network/oncall-api/api.types'; +import { toDateWithTimezoneOffset } from 'pages/schedule/Schedule.helpers'; + +// DatePickers will convert the date passed to local timezone, instead we want to use the date in the given timezone +export const toDatePickerDate = (value: dayjs.Dayjs, timezoneOffset: number) => { + const date = toDateWithTimezoneOffset(value, timezoneOffset); + + return dayjs() + .set('hour', 0) + .set('minute', 0) + .set('second', 0) + .set('millisecond', 0) + .set('date', 1) + .set('month', date.month()) + .set('date', date.date()) + .set('year', date.year()) + .toDate(); +}; + +export const getCalendarStartDateInTimezone = (calendarStartDate: dayjs.Dayjs, utcOffset: number) => { + const offsetedDate = dayjs(calendarStartDate.toDate()) + .utcOffset(utcOffset) + .set('date', 1) + .set('months', calendarStartDate.month()) + .set('date', calendarStartDate.date()) + .set('year', calendarStartDate.year()) + .set('hours', 0) + .set('minutes', 0) + .set('second', 0) + .set('milliseconds', 0); + + return offsetedDate; +}; export const findColor = (shiftId: Shift['id'], layers: Layer[], overrides?) => { let color = undefined; diff --git a/src/containers/Rotations/Rotations.tsx b/src/containers/Rotations/Rotations.tsx index 5ba47b9243..81383ffa7f 100644 --- a/src/containers/Rotations/Rotations.tsx +++ b/src/containers/Rotations/Rotations.tsx @@ -17,14 +17,14 @@ import { WithPermissionControlTooltip } from 'containers/WithPermissionControl/W import { getColor, getLayersFromStore, scheduleViewToDaysInOneRow } from 'models/schedule/schedule.helpers'; import { Schedule, ScheduleType, Shift, ShiftSwap, Event, Layer } from 'models/schedule/schedule.types'; import { ApiSchemas } from 'network/oncall-api/api.types'; -import { getCurrentTimeX } from 'pages/schedule/Schedule.helpers'; +import { getCurrentTimeX, toDateWithTimezoneOffset } from 'pages/schedule/Schedule.helpers'; import { WithStoreProps } from 'state/types'; import { withMobXProviderContext } from 'state/withStore'; import { HTML_ID } from 'utils/DOM'; import { UserActions } from 'utils/authorization/authorization'; import { DEFAULT_TRANSITION_TIMEOUT } from './Rotations.config'; -import { findColor } from './Rotations.helpers'; +import { findColor, getCalendarStartDateInTimezone } from './Rotations.helpers'; import { getRotationsStyles } from './Rotations.styles'; import animationStyles from './Rotations.module.css'; @@ -77,6 +77,8 @@ class _Rotations extends Component { const { shiftStartToShowRotationForm, shiftEndToShowRotationForm } = this.state; + const { selectedTimezoneOffset } = store.timezoneStore; + const currentTimeX = getCurrentTimeX( store.timezoneStore.currentDateInSelectedTimezone, store.timezoneStore.calendarStartDate, @@ -140,7 +142,16 @@ class _Rotations extends Component { @@ -250,8 +261,8 @@ class _Rotations extends Component { shiftColor={findColor(shiftIdToShowRotationForm, layers)} scheduleId={scheduleId} layerPriority={layerPriorityToShowRotationForm} - shiftStart={shiftStartToShowRotationForm} - shiftEnd={shiftEndToShowRotationForm} + shiftStart={toDateWithTimezoneOffset(shiftStartToShowRotationForm, selectedTimezoneOffset)} + shiftEnd={toDateWithTimezoneOffset(shiftEndToShowRotationForm, selectedTimezoneOffset)} onHide={() => { this.hideRotationForm(); @@ -298,9 +309,15 @@ class _Rotations extends Component { return; } - this.setState({ shiftStartToShowRotationForm: shiftStart, shiftEndToShowRotationForm: shiftEnd }, () => { - this.onShowRotationForm('new', layerPriority); - }); + this.setState( + { + shiftStartToShowRotationForm: shiftStart, + shiftEndToShowRotationForm: shiftEnd, + }, + () => { + this.onShowRotationForm('new', layerPriority); + } + ); }; handleAddRotation = (option: SelectableValue) => { diff --git a/src/containers/Rotations/ScheduleFinal.tsx b/src/containers/Rotations/ScheduleFinal.tsx index 723f217bac..e169df9de7 100644 --- a/src/containers/Rotations/ScheduleFinal.tsx +++ b/src/containers/Rotations/ScheduleFinal.tsx @@ -56,7 +56,7 @@ const _ScheduleFinal: FC = observer( scheduleView: propsScheduleView, }) => { const { - timezoneStore: { currentDateInSelectedTimezone, calendarStartDate }, + timezoneStore: { selectedTimezoneOffset, currentDateInSelectedTimezone, calendarStartDate }, scheduleStore: { scheduleView: storeScheduleView }, } = store; @@ -85,7 +85,7 @@ const _ScheduleFinal: FC = observer( }); } return rows; - }, [calendarStartDate, scheduleView]); + }, [calendarStartDate, scheduleView, currentDateInSelectedTimezone, selectedTimezoneOffset]); return (
= observer(({ userPk, onSlotC }; const handleTodayClick = () => { - timezoneStore.setCalendarStartDate(getStartOfWeekBasedOnCurrentDate(timezoneStore.currentDateInSelectedTimezone)); + // TODAY + timezoneStore.setCalendarStartDate(getStartOfWeekBasedOnCurrentDate(dayjs())); }; const handleLeftClick = () => { diff --git a/src/models/timezone/timezone.ts b/src/models/timezone/timezone.ts index e8561c7955..10995b0b72 100644 --- a/src/models/timezone/timezone.ts +++ b/src/models/timezone/timezone.ts @@ -3,7 +3,7 @@ import { observable, action, computed, makeObservable } from 'mobx'; // TODO: move utils from Schedule.helpers to common place import { ScheduleView } from 'models/schedule/schedule.types'; -import { getCalendarStartDate } from 'pages/schedule/Schedule.helpers'; +import { getCalendarStartDate, toDateWithTimezoneOffsetAtMidnight } from 'pages/schedule/Schedule.helpers'; import { RootStore } from 'state/rootStore'; import { getOffsetOfCurrentUser, getGMTTimezoneLabelBasedOnOffset } from './timezone.helpers'; @@ -20,19 +20,18 @@ export class TimezoneStore { @observable selectedTimezoneOffset = getOffsetOfCurrentUser(); - /* @observable - calendarStartDate = getStartOfWeekBasedOnCurrentDate(this.currentDateInSelectedTimezone); */ - @observable - calendarStartDate = getCalendarStartDate(this.currentDateInSelectedTimezone, ScheduleView.OneWeek); + calendarStartDate = getCalendarStartDate( + this.currentDateInSelectedTimezone, + ScheduleView.OneWeek, + this.selectedTimezoneOffset + ); @action.bound setSelectedTimezoneOffset(offset: number) { this.selectedTimezoneOffset = offset; - this.calendarStartDate = getCalendarStartDate( - this.currentDateInSelectedTimezone, - this.rootStore.scheduleStore.scheduleView - ); + + this.calendarStartDate = toDateWithTimezoneOffsetAtMidnight(this.calendarStartDate, offset); } @action.bound diff --git a/src/pages/schedule/Schedule.helpers.ts b/src/pages/schedule/Schedule.helpers.ts index 440960570e..0f2a277131 100644 --- a/src/pages/schedule/Schedule.helpers.ts +++ b/src/pages/schedule/Schedule.helpers.ts @@ -1,7 +1,7 @@ import { config } from '@grafana/runtime'; import dayjs from 'dayjs'; -import { findColor } from 'containers/Rotations/Rotations.helpers'; +import { findColor, getCalendarStartDateInTimezone } from 'containers/Rotations/Rotations.helpers'; import { getLayersFromStore, getOverridesFromStore, @@ -40,13 +40,15 @@ export const getStartOfWeekBasedOnCurrentDate = (date: dayjs.Dayjs) => { return date.startOf('isoWeek'); // it's Monday always }; -export const getCalendarStartDate = (date: dayjs.Dayjs, scheduleView: ScheduleView) => { +export const getCalendarStartDate = (date: dayjs.Dayjs, scheduleView: ScheduleView, timezoneOffset: number) => { + const offsetedDate = getCalendarStartDateInTimezone(date, timezoneOffset); + switch (scheduleView) { case ScheduleView.OneMonth: - const startOfMonth = date.startOf('month'); + const startOfMonth = offsetedDate.startOf('month'); return startOfMonth.startOf('isoWeek'); default: - return date.startOf('isoWeek'); + return offsetedDate.startOf('isoWeek'); } }; @@ -69,8 +71,8 @@ export const getCurrentTimeX = (currentDate: dayjs.Dayjs, startDate: dayjs.Dayjs return diff / baseInMinutes; }; -export const getUTCString = (moment: dayjs.Dayjs) => { - return moment.utc().format('YYYY-MM-DDTHH:mm:ss.000Z'); +export const getUTCString = (date: dayjs.Dayjs) => { + return date.utc().format('YYYY-MM-DDTHH:mm:ss.000Z'); }; export const getDateTime = (date: string) => { @@ -204,3 +206,24 @@ export const getColorSchemeMappingForUsers = ( }); } }; + +export const toDateWithTimezoneOffset = (date: dayjs.Dayjs, timezoneOffset?: number) => { + if (!date) { + return undefined; + } + if (timezoneOffset === undefined) { + return date; + } + return date.utcOffset() === timezoneOffset ? date : date.tz().utcOffset(timezoneOffset); +}; + +export const toDateWithTimezoneOffsetAtMidnight = (date: dayjs.Dayjs, timezoneOffset?: number) => { + return toDateWithTimezoneOffset(date, timezoneOffset) + .set('date', 1) + .set('year', date.year()) + .set('month', date.month()) + .set('date', date.date()) + .set('hour', 0) + .set('minute', 0) + .set('second', 0); +}; diff --git a/src/pages/schedule/Schedule.tsx b/src/pages/schedule/Schedule.tsx index 80fb1bdb3a..3ed6623b97 100644 --- a/src/pages/schedule/Schedule.tsx +++ b/src/pages/schedule/Schedule.tsx @@ -29,7 +29,7 @@ import { Text } from 'components/Text/Text'; import { WithConfirm } from 'components/WithConfirm/WithConfirm'; import { ShiftSwapForm } from 'containers/RotationForm/ShiftSwapForm'; import { Rotations } from 'containers/Rotations/Rotations'; -import { findClosestUserEvent } from 'containers/Rotations/Rotations.helpers'; +import { findClosestUserEvent, toDatePickerDate } from 'containers/Rotations/Rotations.helpers'; import { ScheduleFinal } from 'containers/Rotations/ScheduleFinal'; import { ScheduleOverrides } from 'containers/Rotations/ScheduleOverrides'; import { ScheduleForm } from 'containers/ScheduleForm/ScheduleForm'; @@ -336,10 +336,17 @@ class _SchedulePage extends React.Component { store.timezoneStore.setCalendarStartDate( - getCalendarStartDate(dayjs(newDate), scheduleView) + getCalendarStartDate( + dayjs(newDate), + scheduleView, + store.timezoneStore.selectedTimezoneOffset + ) ); this.handleDateRangeUpdate(); this.setState({ calendarStartDatePickerIsOpen: false }); @@ -362,7 +369,8 @@ class _SchedulePage extends React.Component { - const { store } = this.props; - store.timezoneStore.setCalendarStartDate( - getCalendarStartDate(store.timezoneStore.currentDateInSelectedTimezone, store.scheduleStore.scheduleView) + const { + store: { scheduleStore, timezoneStore }, + } = this.props; + + timezoneStore.setCalendarStartDate( + // TODAY + getCalendarStartDate(dayjs(), scheduleStore.scheduleView, timezoneStore.selectedTimezoneOffset) ); this.handleDateRangeUpdate(); };