diff --git a/src/app/features/schedule/map-schedule-data/create-schedule-days.spec.ts b/src/app/features/schedule/map-schedule-data/create-schedule-days.spec.ts index 9e72eb089..1e4984500 100644 --- a/src/app/features/schedule/map-schedule-data/create-schedule-days.spec.ts +++ b/src/app/features/schedule/map-schedule-data/create-schedule-days.spec.ts @@ -2,6 +2,7 @@ import { createScheduleDays } from './create-schedule-days'; import { DEFAULT_TASK, TaskWithoutReminder } from '../../tasks/task.model'; import { PlannerDayMap } from '../../planner/planner.model'; import { BlockedBlockByDayMap } from '../schedule.model'; +import { parseDbDateStr } from '../../../util/parse-db-date-str'; // Helper function to create test tasks with required properties const createTestTask = ( @@ -386,7 +387,7 @@ describe('createScheduleDays - Task Filtering', () => { }); // Viewing two days in next week - const secondDayNextWeek = new Date(nextWeekStr); + const secondDayNextWeek = parseDbDateStr(nextWeekStr); secondDayNextWeek.setDate(secondDayNextWeek.getDate() + 1); const secondDayStr = secondDayNextWeek.toISOString().split('T')[0]; @@ -451,7 +452,7 @@ describe('createScheduleDays - Task Filtering', () => { describe('Tasks with dueWithTime', () => { it('should appear when viewing a week that includes the dueWithTime', () => { // Arrange - const nextWeekDate = new Date(nextWeekStr); + const nextWeekDate = parseDbDateStr(nextWeekStr); nextWeekDate.setHours(14, 0, 0, 0); // 2 PM next week const taskWithDueTime = createTestTask('task13', 'Task With Due Time', { dueWithTime: nextWeekDate.getTime(), @@ -482,7 +483,7 @@ describe('createScheduleDays - Task Filtering', () => { it('should NOT appear when dueWithTime is before the viewed week', () => { // Arrange - const todayDate = new Date(todayStr); + const todayDate = parseDbDateStr(todayStr); todayDate.setHours(14, 0, 0, 0); const taskWithDueTime = createTestTask('task14', 'Task With Due Time Today', { dueWithTime: todayDate.getTime(), diff --git a/src/app/features/schedule/schedule-month/schedule-month.component.spec.ts b/src/app/features/schedule/schedule-month/schedule-month.component.spec.ts index ca14c4793..232e66b42 100644 --- a/src/app/features/schedule/schedule-month/schedule-month.component.spec.ts +++ b/src/app/features/schedule/schedule-month/schedule-month.component.spec.ts @@ -2,6 +2,7 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { ScheduleMonthComponent } from './schedule-month.component'; import { ScheduleService } from '../schedule.service'; import { DateTimeFormatService } from '../../../core/date-time-format/date-time-format.service'; +import { parseDbDateStr } from '../../../util/parse-db-date-str'; describe('ScheduleMonthComponent', () => { let component: ScheduleMonthComponent; @@ -120,7 +121,7 @@ describe('ScheduleMonthComponent', () => { // Assert // Middle index = floor(35/2) = 17 - const middleDay = new Date(days[17]); + const middleDay = parseDbDateStr(days[17]); expect(result.getFullYear()).toBe(middleDay.getFullYear()); expect(result.getMonth()).toBe(middleDay.getMonth()); expect(result.getDate()).toBe(middleDay.getDate()); @@ -143,7 +144,7 @@ describe('ScheduleMonthComponent', () => { // Assert // Middle index = floor(42/2) = 21 - const middleDay = new Date(days[21]); + const middleDay = parseDbDateStr(days[21]); expect(result.getFullYear()).toBe(middleDay.getFullYear()); expect(result.getMonth()).toBe(middleDay.getMonth()); }); @@ -190,7 +191,7 @@ describe('ScheduleMonthComponent', () => { it('should handle "other-month" class for padding days', () => { // Arrange mockScheduleService.getDayClass.and.callFake((day: string, ref?: Date) => { - const dayDate = new Date(day); + const dayDate = parseDbDateStr(day); if (ref && dayDate.getMonth() !== ref.getMonth()) { return 'other-month'; } diff --git a/src/app/features/schedule/schedule-month/schedule-month.component.ts b/src/app/features/schedule/schedule-month/schedule-month.component.ts index d2f303c88..db9ea9980 100644 --- a/src/app/features/schedule/schedule-month/schedule-month.component.ts +++ b/src/app/features/schedule/schedule-month/schedule-month.component.ts @@ -12,6 +12,7 @@ import { T } from '../../../t.const'; import { ScheduleService } from '../schedule.service'; import { LocaleDatePipe } from 'src/app/ui/pipes/locale-date.pipe'; import { DateTimeFormatService } from 'src/app/core/date-time-format/date-time-format.service'; +import { parseDbDateStr } from 'src/app/util/parse-db-date-str'; @Component({ selector: 'schedule-month', @@ -59,7 +60,7 @@ export class ScheduleMonthComponent { // Use the middle day as reference (around day 14-15 of the month) // This ensures we get a day that's actually in the target month const middleIndex = Math.floor(days.length / 2); - return new Date(days[middleIndex]); + return parseDbDateStr(days[middleIndex]); }); T: typeof T = T; diff --git a/src/app/features/schedule/schedule-week/schedule-week.component.ts b/src/app/features/schedule/schedule-week/schedule-week.component.ts index 5aa323753..0d4141ce4 100644 --- a/src/app/features/schedule/schedule-week/schedule-week.component.ts +++ b/src/app/features/schedule/schedule-week/schedule-week.component.ts @@ -28,6 +28,7 @@ import { DRAG_DELAY_FOR_TOUCH } from '../../../app.constants'; import { MatTooltip } from '@angular/material/tooltip'; import { DateTimeFormatService } from '../../../core/date-time-format/date-time-format.service'; import { LocaleDatePipe } from 'src/app/ui/pipes/locale-date.pipe'; +import { parseDbDateStr } from '../../../util/parse-db-date-str'; import { formatMonthDay } from '../../../util/format-month-day.util'; import { ScheduleWeekDragService } from './schedule-week-drag.service'; import { calculatePlaceholderForGridMove } from './schedule-week-placeholder.util'; @@ -331,7 +332,7 @@ export class ScheduleWeekComponent implements OnInit, AfterViewInit, OnDestroy { if (!dayStr) { return ''; } - const date = new Date(dayStr); + const date = parseDbDateStr(dayStr); if (Number.isNaN(date.getTime())) { return dayStr; } diff --git a/src/app/features/schedule/schedule.service.ts b/src/app/features/schedule/schedule.service.ts index bddfdcae7..2a375992b 100644 --- a/src/app/features/schedule/schedule.service.ts +++ b/src/app/features/schedule/schedule.service.ts @@ -23,6 +23,7 @@ import { CalendarIntegrationService } from '../calendar-integration/calendar-int import { toSignal } from '@angular/core/rxjs-interop'; import { TaskService } from '../tasks/task.service'; import { startWith } from 'rxjs/operators'; +import { parseDbDateStr } from '../../util/parse-db-date-str'; @Injectable({ providedIn: 'root', @@ -272,7 +273,7 @@ export class ScheduleService { } getDayClass(day: string, referenceMonth?: Date): string { - const dayDate = new Date(day); + const dayDate = parseDbDateStr(day); const today = new Date(); // If referenceMonth is provided, use it to determine "current month" diff --git a/src/app/util/parse-db-date-str.ts b/src/app/util/parse-db-date-str.ts new file mode 100644 index 000000000..78e206596 --- /dev/null +++ b/src/app/util/parse-db-date-str.ts @@ -0,0 +1,14 @@ +// Parse 'YYYY-MM-DD' string as a local date (not UTC) +// +// ⚠️ Important: new Date('2026-01-12') parses as UTC midnight, which becomes +// the previous day when converted to local timezone (e.g., Europe/Berlin UTC+1). +// This function parses the string as a local date to avoid timezone issues. +// +// Examples: +// - new Date('2026-01-12') in Europe/Berlin → 2026-01-11 23:00 (UTC midnight) +// - parseDbDateStr('2026-01-12') → 2026-01-12 00:00 (local midnight) + +export const parseDbDateStr = (dateStr: string): Date => { + const [year, month, day] = dateStr.split('-').map(Number); + return new Date(year, month - 1, day); +};