From 6a7c0ef255788b04c5d0a6451869755d2bfc08ce Mon Sep 17 00:00:00 2001 From: Jacob Hagstedt Date: Thu, 17 Sep 2026 18:28:16 +0200 Subject: [PATCH] Draw a leave period as one band, and make red days look red MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A six-week summer used to render as a row of disconnected bars, because bars were derived per week row and grouped by parent and day type. They carried no identity and ignored pointer events, so there was nothing to click. Bands now group by the wish that produced them, bridge the weekends and red days inside a period, and round only the ends that are genuinely the first and last day. Each day still shows what pays for it, and a hand-edited day is marked as one the planner will leave alone. Sundays and public holidays now render red and Saturdays a neutral grey, following the printed almanacka people compare against, and a holiday's name is visible in the cell rather than hidden in a tooltip. This also explains the report that months looked unshaded: weekends had no styling at all, since the day component handled only red days and klämdagar. The year selector follows the range the calendar API actually serves instead of a hardcoded span. --- .../components/common/CalendarDay.test.tsx | 45 + .../src/components/common/CalendarDay.tsx | 29 +- .../components/common/CalendarWeek.test.tsx | 889 ++++++------------ .../src/components/common/CalendarWeek.tsx | 381 +++++--- frontend/src/i18n/en/calendar.ts | 2 + frontend/src/i18n/sv/calendar.ts | 2 + frontend/src/pages/Calendar.test.tsx | 41 +- frontend/src/pages/Calendar.tsx | 29 +- frontend/src/pages/calendar.css | 138 ++- frontend/src/services/api.ts | 4 + frontend/src/types/entities.ts | 8 + frontend/tests/fixtures/mockData.ts | 2 + 12 files changed, 826 insertions(+), 744 deletions(-) diff --git a/frontend/src/components/common/CalendarDay.test.tsx b/frontend/src/components/common/CalendarDay.test.tsx index b58ae1a..6e10ab0 100644 --- a/frontend/src/components/common/CalendarDay.test.tsx +++ b/frontend/src/components/common/CalendarDay.test.tsx @@ -157,6 +157,51 @@ describe('CalendarDay', () => { expect(day).toHaveAttribute('aria-label', '2026-06-15') expect(day).not.toHaveAttribute('title') }) + + it('shows the holiday name as visible text in the cell, not just title/aria-label', () => { + const { getByTestId } = render( + + ) + expect(getByTestId('calendar-day-holiday-name')).toHaveTextContent('Midsommardagen') + }) + + it('does not render holiday-name text for a plain working day', () => { + const { queryByTestId } = render() + expect(queryByTestId('calendar-day-holiday-name')).not.toBeInTheDocument() + }) + }) + + describe('Saturday/Sunday almanacka parity', () => { + it('marks a plain Saturday with the neutral weekend class, not red', () => { + const { getByTestId } = render() + const day = getByTestId('calendar-day') + expect(day).toHaveClass('calendar-day--weekend') + expect(day).not.toHaveClass('calendar-day--red-day') + expect(day.querySelector('[data-testid="calendar-day-type-badge"]')).not.toBeInTheDocument() + }) + + it('marks a plain Sunday as a red day even without a holiday name', () => { + const { getByTestId } = render() + const day = getByTestId('calendar-day') + expect(day).toHaveClass('calendar-day--red-day') + expect(day).not.toHaveClass('calendar-day--weekend') + expect(getByTestId('calendar-day-type-badge')).toHaveTextContent('R') + expect(day.getAttribute('aria-label')).toContain('söndag') + }) + + it('does not show holiday-name text for a bare Sunday (there is no name to show)', () => { + const { queryByTestId } = render() + expect(queryByTestId('calendar-day-holiday-name')).not.toBeInTheDocument() + }) + + it('still treats a holiday landing on Saturday as a full red day, not neutral weekend', () => { + const { getByTestId } = render( + + ) + const day = getByTestId('calendar-day') + expect(day).toHaveClass('calendar-day--red-day') + expect(day).not.toHaveClass('calendar-day--weekend') + }) }) describe('§3.6 workspace day overrides', () => { diff --git a/frontend/src/components/common/CalendarDay.tsx b/frontend/src/components/common/CalendarDay.tsx index 8198deb..00fd56a 100644 --- a/frontend/src/components/common/CalendarDay.tsx +++ b/frontend/src/components/common/CalendarDay.tsx @@ -39,7 +39,20 @@ export function CalendarDay({ overrideIsWorkingDay = null, }: CalendarDayProps): ReactNode { const { t } = useTranslation() - const dayNum = new Date(date + 'T00:00:00Z').getUTCDate() + const dateObj = new Date(date + 'T00:00:00Z') + const dayNum = dateObj.getUTCDate() + + // §3.4 (Swedish almanacka parity) — the backend collapses Saturday and + // Sunday into a single "weekend" day_type (see calendar_service.py), but + // the almanacka the user compares against marks Sunday red and Saturday + // neutral grey. A holiday that happens to land on Sunday already arrives + // as day_type "red_day" (see get_red_days's exact-match on "Söndag" — a + // *bare* Sunday is never "red_day"), so a plain Sunday is detected here + // from the weekday, not from day_type. + const weekday = dateObj.getUTCDay() // 0 = Sunday, 6 = Saturday + const isBareSunday = dayType === 'weekend' && weekday === 0 + const isPlainSaturday = dayType === 'weekend' && weekday === 6 + const isRedDay = dayType === 'red_day' || isBareSunday const handleKeyDown = (e: React.KeyboardEvent) => { if (e.key === 'Enter' || e.key === ' ') { @@ -61,6 +74,8 @@ export function CalendarDay({ descriptors.push(cleanHolidayName) } else if (dayType === 'klamdag') { descriptors.push(t('calendarDay.klamdagAria')) + } else if (isBareSunday) { + descriptors.push(t('calendarDay.sundayAria')) } if (isOverride) { descriptors.push( @@ -78,8 +93,9 @@ export function CalendarDay({ events.length > 0 ? 'calendar-day--has-events' : '', isToday ? 'calendar-day--today' : '', isSelected ? 'calendar-day--selected' : '', - dayType === 'red_day' ? 'calendar-day--red-day' : '', + isRedDay ? 'calendar-day--red-day' : '', dayType === 'klamdag' ? 'calendar-day--klamdag' : '', + isPlainSaturday ? 'calendar-day--weekend' : '', isOverride ? 'calendar-day--override' : '', ] .filter(Boolean) @@ -99,11 +115,16 @@ export function CalendarDay({ onPointerEnter={() => onPointerEnter?.(date)} >
{dayNum}
- {(dayType === 'red_day' || dayType === 'klamdag') && ( + {(isRedDay || dayType === 'klamdag') && ( )} + {cleanHolidayName && ( +
+ {cleanHolidayName} +
+ )} {isOverride && (