feat(web): Kalender-Plakette in der Farbe des Kalenders, Farbpunkt je Tooltip-Zeile
- groupEventsByDate sortiert jede Tagesgruppe jetzt nach Start aufsteigend - Zaehl-Plakette im Monatsraster traegt die Farbe des Kalenders des fruehesten Termins (weisse Schrift), ohne Farbe unveraendert bg-primary - Tooltip-Zeilen bekommen denselben Farbpunkt wie die Liste "Naechste Termine" (Rueckfall var(--muted-foreground)) - 3 neue Tests: calendar-month Test 2b, calendar-widget Test 3b/3c Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -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);
|
||||
|
||||
@@ -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<string, CalendarEvent[]> {
|
||||
const groupedEvents = new Map<string, CalendarEvent[]>();
|
||||
@@ -110,6 +114,9 @@ export function groupEventsByDate(events: CalendarEvent[]): Map<string, Calendar
|
||||
const existingEvents = groupedEvents.get(key) ?? [];
|
||||
groupedEvents.set(key, [...existingEvents, event]);
|
||||
}
|
||||
for (const dayEvents of groupedEvents.values()) {
|
||||
dayEvents.sort((a, b) => new Date(a.start).getTime() - new Date(b.start).getTime());
|
||||
}
|
||||
return groupedEvents;
|
||||
}
|
||||
|
||||
|
||||
@@ -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(<CalendarWidget instanceId="cal-3b" config={{}} isEditMode={false} />);
|
||||
|
||||
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(<CalendarWidget instanceId="cal-3c" config={{}} isEditMode={false} />);
|
||||
|
||||
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)),
|
||||
|
||||
@@ -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)}
|
||||
>
|
||||
<span>{day.date.getDate()}</span>
|
||||
{hasEvents && (
|
||||
<span
|
||||
data-testid="calendar-day-count"
|
||||
className="absolute bottom-px right-px flex h-[clamp(10px,3cqw,16px)] min-w-[clamp(10px,3cqw,16px)] items-center justify-center rounded-full bg-primary px-0.5 text-[clamp(7px,1.8cqw,10px)] font-semibold leading-none text-primary-foreground"
|
||||
>
|
||||
{day.events.length}
|
||||
</span>
|
||||
)}
|
||||
{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 (
|
||||
<span
|
||||
data-testid="calendar-day-count"
|
||||
className={badgeClass}
|
||||
style={badgeColor ? { backgroundColor: badgeColor } : undefined}
|
||||
>
|
||||
{day.events.length}
|
||||
</span>
|
||||
);
|
||||
})()}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
@@ -313,6 +326,12 @@ export function CalendarWidget({ config }: WidgetProps) {
|
||||
>
|
||||
{hoverEvents.slice(0, 5).map((event) => (
|
||||
<div key={event.id} className="flex gap-2">
|
||||
<span
|
||||
data-testid="tooltip-color-dot"
|
||||
className="mt-1 h-2 w-2 shrink-0 rounded-full"
|
||||
style={{ backgroundColor: event.color || 'var(--muted-foreground)' }}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<span className="shrink-0 tabular-nums text-muted-foreground">
|
||||
{event.allDay ? t('calendar.allDay') : formatEventTime(event.start)}
|
||||
</span>
|
||||
|
||||
Reference in New Issue
Block a user