diff --git a/src/app/features/schedule/schedule-week/schedule-week.component.scss b/src/app/features/schedule/schedule-week/schedule-week.component.scss index 9a124e1e7..3aee179cb 100644 --- a/src/app/features/schedule/schedule-week/schedule-week.component.scss +++ b/src/app/features/schedule/schedule-week/schedule-week.component.scss @@ -3,6 +3,84 @@ :host { --schedule-time-width: 3em; + // Responsive day column widths + --schedule-day-width: 180px; // Default for desktop + + @include mq(md, max) { + // Tablet + --schedule-day-width: 150px; + } + + @include mq(xs, max) { + // Mobile + --schedule-day-width: 120px; + } + + // Enable horizontal scroll when viewport is too small for 7 days + &[data-horizontal-scroll] { + overflow-x: auto; + overflow-y: visible; + display: block; // Change from default to allow proper overflow + + // Show scrollbar on all platforms + &::-webkit-scrollbar { + height: 12px; + -webkit-appearance: none; + } + + &::-webkit-scrollbar-track { + background: var(--bg-darker); + border-radius: 6px; + } + + &::-webkit-scrollbar-thumb { + background: var(--extra-border-color); + border-radius: 6px; + border: 2px solid var(--bg-darker); + + &:hover { + background: var(--text-color-muted); + } + } + + // For Firefox + scrollbar-width: auto; + scrollbar-color: var(--extra-border-color) var(--bg-darker); + + .week-header { + // Remove left/right constraints to allow horizontal scrolling + left: auto; + right: auto; + // Force width to match grid width + width: fit-content; + min-width: 100%; + } + + .grid-container { + // Allow columns to scale up when space available, but enforce minimum for scroll + grid-template-columns: var(--schedule-time-width) repeat( + var(--nr-of-days), + minmax(var(--schedule-day-width), 1fr) + ); + // Force grid to size based on content, not container + width: fit-content; + min-width: 100%; + // Disable centering to allow proper overflow + place-content: start; + } + + .days { + // Match grid-container column widths exactly + grid-template-columns: var(--schedule-time-width) repeat( + var(--nr-of-days), + minmax(var(--schedule-day-width), 1fr) + ); + // Ensure days container matches grid width + width: fit-content; + // Disable centering + place-content: start; + } + } // State-based styles using host classes &.isCtrlKeyPressed { 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 0d4141ce4..5cf1e50df 100644 --- a/src/app/features/schedule/schedule-week/schedule-week.component.ts +++ b/src/app/features/schedule/schedule-week/schedule-week.component.ts @@ -33,6 +33,9 @@ import { formatMonthDay } from '../../../util/format-month-day.util'; import { ScheduleWeekDragService } from './schedule-week-drag.service'; import { calculatePlaceholderForGridMove } from './schedule-week-placeholder.util'; import { truncate } from '../../../util/truncate'; +import { fromEvent } from 'rxjs'; +import { debounceTime, map, startWith } from 'rxjs/operators'; +import { toSignal } from '@angular/core/rxjs-interop'; const D_HOURS = 24; @@ -58,6 +61,7 @@ const D_HOURS = 24; '[class.is-not-dragging]': '!isDragging()', '[class.is-resizing-event]': 'isAnyEventResizing()', '[class]': 'dragEventTypeClass()', + '[attr.data-horizontal-scroll]': 'shouldEnableHorizontalScroll()', }, }) export class ScheduleWeekComponent implements OnInit, AfterViewInit, OnDestroy { @@ -65,6 +69,20 @@ export class ScheduleWeekComponent implements OnInit, AfterViewInit, OnDestroy { private _dateTimeFormatService = inject(DateTimeFormatService); private _translateService = inject(TranslateService); + private _windowSize = toSignal( + fromEvent(window, 'resize').pipe( + startWith({ width: window.innerWidth }), + debounceTime(50), + map(() => ({ width: window.innerWidth })), + ), + { initialValue: { width: window.innerWidth } }, + ); + + shouldEnableHorizontalScroll = computed(() => { + // Enable scroll when viewport is smaller than what's needed for 7 days + return this._windowSize().width < 1900; + }); + isInPanel = input(false); events = input([]); beyondBudget = input([]); diff --git a/src/app/features/schedule/schedule.service.spec.ts b/src/app/features/schedule/schedule.service.spec.ts index a09bfb101..27eeeaea0 100644 --- a/src/app/features/schedule/schedule.service.spec.ts +++ b/src/app/features/schedule/schedule.service.spec.ts @@ -75,10 +75,24 @@ describe('ScheduleService', () => { expect(result[0]).toBe(expectedTodayStr); }); - it('should return days starting from referenceDate when provided', () => { + it('should return days starting from referenceDate', () => { // Arrange const nrOfDaysToShow = 3; - const referenceDate = new Date(2026, 0, 25); // Jan 25, 2026 + const referenceDate = new Date(2028, 5, 15); // June 15, 2028 + const expectedFirstDay = dateService.todayStr(referenceDate.getTime()); + + // Act + const result = service.getDaysToShow(nrOfDaysToShow, referenceDate); + + // Assert + expect(result[0]).toBe(expectedFirstDay); + expect(result.length).toBe(3); + }); + + it('should return days starting from provided date even if today', () => { + // Arrange + const nrOfDaysToShow = 3; + const referenceDate = new Date(); const expectedFirstDay = dateService.todayStr(referenceDate.getTime()); // Act @@ -92,7 +106,8 @@ describe('ScheduleService', () => { it('should return consecutive days from referenceDate', () => { // Arrange const nrOfDaysToShow = 7; - const referenceDate = new Date(2026, 0, 20); // Jan 20, 2026 + // Use a future date to ensure not in current week + const referenceDate = new Date(2028, 0, 20); // Jan 20, 2028 // Act const result = service.getDaysToShow(nrOfDaysToShow, referenceDate); @@ -112,7 +127,8 @@ describe('ScheduleService', () => { it('should handle transition across months', () => { // Arrange const nrOfDaysToShow = 5; - const referenceDate = new Date(2026, 0, 30); // Jan 30, 2026 + // Use a future date to ensure not in current week + const referenceDate = new Date(2028, 0, 30); // Jan 30, 2028 // Act const result = service.getDaysToShow(nrOfDaysToShow, referenceDate); diff --git a/src/app/features/schedule/schedule/schedule.component.spec.ts b/src/app/features/schedule/schedule/schedule.component.spec.ts index a74f35fbb..be9e633c4 100644 --- a/src/app/features/schedule/schedule/schedule.component.spec.ts +++ b/src/app/features/schedule/schedule/schedule.component.spec.ts @@ -213,7 +213,8 @@ describe('ScheduleComponent', () => { // Assert const newDate = component['_selectedDate'](); - expect(newDate?.getDate()).toBe(13); // Jan 13, 2026 + expect(newDate?.getDate()).toBe(13); // Jan 13, 2026 (20 - 7) + expect(newDate?.getHours()).toBe(0); // Normalized to midnight }); it('should subtract 7 days from today when _selectedDate is null in week view', () => { @@ -228,6 +229,7 @@ describe('ScheduleComponent', () => { // Assert const newDate = component['_selectedDate'](); expect(newDate?.getDate()).toBe(expectedDate.getDate()); + expect(newDate?.getHours()).toBe(0); // Normalized to midnight }); it('should go to previous month in month view', () => { @@ -272,12 +274,15 @@ describe('ScheduleComponent', () => { // Assert const newDate = component['_selectedDate'](); - expect(newDate?.getDate()).toBe(27); // Jan 27, 2026 + expect(newDate?.getDate()).toBe(27); // Jan 27, 2026 (20 + 7) + expect(newDate?.getHours()).toBe(0); // Normalized to midnight }); it('should add 7 days from today when _selectedDate is null in week view', () => { // Arrange component['_selectedDate'].set(null); + + // Calculate expected date (today + 7 days) const expectedDate = new Date(); expectedDate.setDate(expectedDate.getDate() + 7); @@ -287,6 +292,7 @@ describe('ScheduleComponent', () => { // Assert const newDate = component['_selectedDate'](); expect(newDate?.getDate()).toBe(expectedDate.getDate()); + expect(newDate?.getHours()).toBe(0); // Normalized to midnight }); it('should go to next month in month view', () => { diff --git a/src/app/features/schedule/schedule/schedule.component.ts b/src/app/features/schedule/schedule/schedule.component.ts index e394db935..4567da7e7 100644 --- a/src/app/features/schedule/schedule/schedule.component.ts +++ b/src/app/features/schedule/schedule/schedule.component.ts @@ -109,17 +109,8 @@ export class ScheduleComponent { } } - if (width < 600) { - return 3; - } else if (width < 900) { - return 4; - } else if (width < 1900) { - return 5; - } else if (width < 2200) { - return 7; - } else { - return 10; - } + // Week view: always 7 days + return 7; }); daysToShow = computed(() => { @@ -220,12 +211,10 @@ export class ScheduleComponent { ); this._selectedDate.set(previousMonth); } else { - // Week view: subtract 7 days (create fresh Date to avoid mutations) - const previousWeek = new Date( - currentDate.getFullYear(), - currentDate.getMonth(), - currentDate.getDate() - 7, - ); + // Week view: always subtract 7 days (full week) + const previousWeek = new Date(currentDate); + previousWeek.setDate(currentDate.getDate() - 7); + previousWeek.setHours(0, 0, 0, 0); this._selectedDate.set(previousWeek); } } @@ -243,12 +232,10 @@ export class ScheduleComponent { ); this._selectedDate.set(nextMonth); } else { - // Week view: add 7 days (create fresh Date to avoid mutations) - const nextWeek = new Date( - currentDate.getFullYear(), - currentDate.getMonth(), - currentDate.getDate() + 7, - ); + // Week view: always add 7 days (full week) + const nextWeek = new Date(currentDate); + nextWeek.setDate(currentDate.getDate() + 7); + nextWeek.setHours(0, 0, 0, 0); this._selectedDate.set(nextWeek); } }