diff --git a/apps/web/src/components/dashboard/widgets/calendar-month.test.ts b/apps/web/src/components/dashboard/widgets/calendar-month.test.ts index 49bc607..1978338 100644 --- a/apps/web/src/components/dashboard/widgets/calendar-month.test.ts +++ b/apps/web/src/components/dashboard/widgets/calendar-month.test.ts @@ -63,6 +63,16 @@ describe('calendar-month', () => { expect(day20?.events.length).toBe(2); }); + it('Test 2b: groupEventsByDate sortiert jede Tagesgruppe nach Start aufsteigend', () => { + const events = [ + ev('e2', new Date(2026, 6, 20, 14, 0), new Date(2026, 6, 20, 15, 0)), + ev('e1', new Date(2026, 6, 20, 9, 0), new Date(2026, 6, 20, 10, 0)), + ]; + + const grouped = groupEventsByDate(events); + expect(grouped.get('2026-07-20')?.map((e) => e.id)).toEqual(['e1', 'e2']); + }); + it('Test 3: resolveCalendarConfig klemmt ungueltige Werte auf die Vorgaben/Grenzen zurueck', () => { expect(resolveCalendarConfig({})).toEqual({ showMonth: true, maxEvents: 3, lookaheadDays: 30 }); expect(resolveCalendarConfig({ showMonth: false }).showMonth).toBe(false); diff --git a/apps/web/src/components/dashboard/widgets/calendar-month.ts b/apps/web/src/components/dashboard/widgets/calendar-month.ts index ceb887e..0dfe2e4 100644 --- a/apps/web/src/components/dashboard/widgets/calendar-month.ts +++ b/apps/web/src/components/dashboard/widgets/calendar-month.ts @@ -14,7 +14,9 @@ * Starttag-Regel: mehrtaegige und ganztaegige Termine werden im Monatsraster * bewusst NUR am Starttag gezaehlt/angezeigt (`groupEventsByDate` gruppiert * ausschliesslich nach `event.start`) — eine Terminleiste ueber mehrere Tage - * ist nicht Teil dieses Auftrags. + * ist nicht Teil dieses Auftrags. Jede Tagesgruppe ist zusaetzlich nach Start + * aufsteigend sortiert, damit die Plakettenfarbe (Farbe des ERSTEN Termins) + * und die Tooltip-Reihenfolge deterministisch uebereinstimmen (quick-260916-jvj). */ import type { CalendarEvent } from '@/lib/calendar-api'; @@ -101,7 +103,9 @@ export interface CalendarDay { /** * Gruppiert Termine nach lokalem Startdatum. Mehrtaegige/ganztaegige Termine - * zaehlen bewusst nur am Starttag (siehe Kopfkommentar). + * zaehlen bewusst nur am Starttag (siehe Kopfkommentar). Jede Tagesgruppe + * wird nach Start aufsteigend sortiert (quick-260916-jvj), damit der erste + * Eintrag zuverlaessig der fruehesten Termin des Tages ist. */ export function groupEventsByDate(events: CalendarEvent[]): Map { const groupedEvents = new Map(); @@ -110,6 +114,9 @@ export function groupEventsByDate(events: CalendarEvent[]): Map new Date(a.start).getTime() - new Date(b.start).getTime()); + } return groupedEvents; } diff --git a/apps/web/src/components/dashboard/widgets/calendar-widget.test.tsx b/apps/web/src/components/dashboard/widgets/calendar-widget.test.tsx index aa72430..88d6b85 100644 --- a/apps/web/src/components/dashboard/widgets/calendar-widget.test.tsx +++ b/apps/web/src/components/dashboard/widgets/calendar-widget.test.tsx @@ -146,6 +146,67 @@ describe('CalendarWidget', () => { expect(within(day22).queryByTestId('calendar-day-count')).toBeNull(); }); + it('Test 3b: Plakette traegt die Kalenderfarbe des fruehesten Termins, Tooltip-Zeilen mit Farbpunkt', async () => { + mockFetchEvents.mockResolvedValue([ + ev('Lunch', new Date(2026, 6, 20, 14, 0), new Date(2026, 6, 20, 15, 0), { + color: '#4060c4', + }), + ev('Team Meeting', new Date(2026, 6, 20, 9, 0), new Date(2026, 6, 20, 10, 0), { + color: '#c44040', + }), + ]); + + const { CalendarWidget } = await import('./calendar-widget'); + render(); + + await waitFor(() => { + expect(screen.getByText('Juli 2026')).toBeInTheDocument(); + }); + + const day20 = document.querySelector('[data-date="2026-07-20"]') as HTMLElement; + const badge = within(day20).getByTestId('calendar-day-count'); + expect(badge).toHaveTextContent('2'); + expect(badge).toHaveStyle({ backgroundColor: '#c44040' }); + expect(badge).toHaveClass('text-white'); + expect(badge).not.toHaveClass('bg-primary'); + + fireEvent.mouseEnter(day20); + const tooltip = screen.getByTestId('calendar-day-tooltip'); + const dots = within(tooltip).getAllByTestId('tooltip-color-dot'); + expect(dots).toHaveLength(2); + expect(dots[0]).toHaveStyle({ backgroundColor: '#c44040' }); + expect(dots[1]).toHaveStyle({ backgroundColor: '#4060c4' }); + expect(tooltip.textContent?.indexOf('Team Meeting')).toBeLessThan( + tooltip.textContent?.indexOf('Lunch') ?? -1, + ); + }); + + it('Test 3c: Plakette ohne Kalenderfarbe behaelt bg-primary', async () => { + mockFetchEvents.mockResolvedValue([ + ev('Ohne Farbe', new Date(2026, 6, 21, 9, 0), new Date(2026, 6, 21, 10, 0), { + color: undefined, + }), + ]); + + const { CalendarWidget } = await import('./calendar-widget'); + render(); + + await waitFor(() => { + expect(screen.getByText('Juli 2026')).toBeInTheDocument(); + }); + + const day21 = document.querySelector('[data-date="2026-07-21"]') as HTMLElement; + const badge = within(day21).getByTestId('calendar-day-count'); + expect(badge).toHaveClass('bg-primary'); + expect(badge).toHaveClass('text-primary-foreground'); + expect(badge).not.toHaveClass('text-white'); + expect((badge as HTMLElement).style.backgroundColor).toBe(''); + + fireEvent.mouseEnter(day21); + const tooltip = screen.getByTestId('calendar-day-tooltip'); + expect(within(tooltip).getAllByTestId('tooltip-color-dot')).toHaveLength(1); + }); + it('Test 4: Tooltip erscheint per Portal im body und zeigt bis zu 5 Termine plus Hinweis', async () => { mockFetchEvents.mockResolvedValue([ ev('Team Meeting', new Date(2026, 6, 20, 9, 0), new Date(2026, 6, 20, 10, 0)), diff --git a/apps/web/src/components/dashboard/widgets/calendar-widget.tsx b/apps/web/src/components/dashboard/widgets/calendar-widget.tsx index 9b3d181..85c81f4 100644 --- a/apps/web/src/components/dashboard/widgets/calendar-widget.tsx +++ b/apps/web/src/components/dashboard/widgets/calendar-widget.tsx @@ -30,7 +30,9 @@ const TOOLTIP_EDGE_PX = 4; * * Oben ein Monatsraster (Nav-Zeile Zurueck/Monat/Weiter, Wochentagskopf, * 42 Zellen ab Montag, Faehrmonatstage gedaempft, heutiger Tag hervorgehoben, - * Zaehl-Plakette an Tagen mit Terminen, Tooltip beim Ueberfahren), darunter + * Zaehl-Plakette an Tagen mit Terminen in der Farbe des Kalenders des + * fruehesten Termins des Tages (sonst Akzentfarbe), Tooltip beim Ueberfahren + * mit Farbpunkt je Zeile (quick-260916-jvj)), darunter * der Block "Naechste Termine". Beide Bloecke lassen sich unabhaengig * ein-/ausblenden (Config `showMonth`/`maxEvents`, siehe calendar-month.ts). * @@ -241,14 +243,25 @@ export function CalendarWidget({ config }: WidgetProps) { onMouseLeave={() => setHover(null)} > {day.date.getDate()} - {hasEvents && ( - - {day.events.length} - - )} + {hasEvents && + (() => { + const badgeColor = day.events[0]?.color; + const badgeClass = [ + 'absolute bottom-px right-px flex h-[clamp(10px,3cqw,16px)] min-w-[clamp(10px,3cqw,16px)] items-center justify-center rounded-full px-0.5 text-[clamp(7px,1.8cqw,10px)] font-semibold leading-none', + badgeColor ? 'text-white' : 'bg-primary text-primary-foreground', + ] + .filter(Boolean) + .join(' '); + return ( + + {day.events.length} + + ); + })()} ); })} @@ -313,6 +326,12 @@ export function CalendarWidget({ config }: WidgetProps) { > {hoverEvents.slice(0, 5).map((event) => (
+