diff --git a/packages/devextreme/js/__internal/scheduler/view_model/generate_view_model/options/get_minutes_cell_intervals.ts b/packages/devextreme/js/__internal/scheduler/view_model/generate_view_model/options/get_minutes_cell_intervals.ts index 57ad9082a499..6085f47c2afe 100644 --- a/packages/devextreme/js/__internal/scheduler/view_model/generate_view_model/options/get_minutes_cell_intervals.ts +++ b/packages/devextreme/js/__internal/scheduler/view_model/generate_view_model/options/get_minutes_cell_intervals.ts @@ -1,3 +1,6 @@ +import { dateUtils } from '@ts/core/utils/m_date'; + +import timeZoneUtils from '../../../m_utils_time_zone'; import { splitIntervalByDay } from '../../common/split_interval_by_days'; import type { CellInterval, DateInterval } from '../../types'; @@ -9,6 +12,8 @@ interface Options { skippedDays: number[]; } +const toMs = dateUtils.dateToMilliseconds; + const filterBySkippedDays = ( intervals: T[], skippedDays: number[], @@ -17,6 +22,18 @@ const filterBySkippedDays = ( return !skippedDays.includes(weekday); }); +const adjustDayIntervalMinForMidnightDST = ( + dayIntervalMin: number, + startDayHour: number, +): number => { + const date = timeZoneUtils.createDateFromUTCWithLocalOffset(new Date(dayIntervalMin)); + const isMidnightDST = startDayHour === 0 && timeZoneUtils.isLocalTimeMidnightDST(date); + + return isMidnightDST + ? dayIntervalMin + date.getHours() * toMs('hour') + : dayIntervalMin; +}; + export const getMinutesCellIntervals = ({ intervals, startDayHour, @@ -30,7 +47,11 @@ export const getMinutesCellIntervals = ({ let columnIndex = 0; filterBySkippedDays(dayIntervals, skippedDays).forEach((dayInterval) => { - const date = new Date(dayInterval.min); + const firstAvailableDayTime = adjustDayIntervalMinForMidnightDST( + dayInterval.min, + startDayHour, + ); + const date = new Date(firstAvailableDayTime); while (date.getTime() < dayInterval.max) { const min = date.getTime(); let max = date.setUTCMinutes(date.getUTCMinutes() + durationMinutes); diff --git a/packages/devextreme/js/__internal/scheduler/view_model/generate_view_model/options/get_week_intervals.cairo.test.ts b/packages/devextreme/js/__internal/scheduler/view_model/generate_view_model/options/get_week_intervals.cairo.test.ts new file mode 100644 index 000000000000..7dc05084bfd0 --- /dev/null +++ b/packages/devextreme/js/__internal/scheduler/view_model/generate_view_model/options/get_week_intervals.cairo.test.ts @@ -0,0 +1,48 @@ +/** + * @timezone Africa/Cairo + */ + +import { + describe, expect, it, +} from '@jest/globals'; + +import { getWeekIntervals } from './get_week_intervals'; + +describe('getWeekIntervals', () => { + it('T1327394: should not add a timeline cell before the first available local time on midnight DST', () => { + const intervals = getWeekIntervals({ + startDayHour: 0, + endDayHour: 24, + min: Date.UTC(2026, 3, 24, 1), + max: Date.UTC(2026, 3, 25), + skippedDays: [], + }, 60, 0, true); + + expect(intervals.cells[0]).toEqual({ + min: Date.UTC(2026, 3, 24, 1), + max: Date.UTC(2026, 3, 24, 2), + rowIndex: 0, + columnIndex: 0, + cellIndex: 0, + }); + }); + + it('T1327394: should not add a timeline week cell before the first available local time on midnight DST', () => { + const intervals = getWeekIntervals({ + startDayHour: 0, + endDayHour: 24, + min: Date.UTC(2026, 3, 20), + max: Date.UTC(2026, 3, 27), + skippedDays: [], + }, 60, 0, true); + + expect(intervals.cells.find((cell) => cell.min === Date.UTC(2026, 3, 24))).toBeUndefined(); + expect(intervals.cells.find((cell) => cell.min === Date.UTC(2026, 3, 24, 1))).toEqual({ + min: Date.UTC(2026, 3, 24, 1), + max: Date.UTC(2026, 3, 24, 2), + rowIndex: 0, + columnIndex: 96, + cellIndex: 96, + }); + }); +});