feat(web): Kalender-Tag nach Kalender gruppieren, Kreise je Kalender berechnen (calendar-month)

- groupDayBySource gruppiert Tagestermine nach sourceId (nicht Farbe), Reihenfolge = erstes Auftreten, Farbe = Farbe des ersten Termins der Gruppe
- buildDayBadges baut daraus bis zu drei Plaketten-Kreise, ab dem vierten Kalender einen grauen Restkreis mit Summe
- Tests 8/9 in calendar-month.test.ts (TDD: RED bestaetigt vor Implementierung)

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-16 14:34:49 +02:00
parent 45b20a9fd4
commit 4ddadc63ab
2 changed files with 168 additions and 0 deletions
@@ -3,12 +3,14 @@ import type { CalendarEvent } from '@/lib/calendar-api';
import {
addDays,
buildCalendarDays,
buildDayBadges,
computeFetchWindow,
dateKey,
formatEventDate,
formatEventTime,
formatMonthLabel,
gridStartFor,
groupDayBySource,
groupEventsByDate,
resolveCalendarConfig,
selectUpcomingEvents,
@@ -149,4 +151,99 @@ describe('calendar-month', () => {
expect(start.getHours()).toBe(0);
expect(addDays(new Date(2026, 6, 30), 2).getDate()).toBe(1);
});
it('Test 8: groupDayBySource gruppiert nach sourceId in Reihenfolge des ersten Auftretens', () => {
const day = new Date(2026, 6, 20, 9, 0);
const hourLater = new Date(2026, 6, 20, 10, 0);
// (a) drei Termine, alle s1 mit derselben Farbe
const sameSource = [
ev('e1', day, hourLater, { sourceId: 's1', color: '#c44040' }),
ev('e2', day, hourLater, { sourceId: 's1', color: '#c44040' }),
ev('e3', day, hourLater, { sourceId: 's1', color: '#c44040' }),
];
expect(groupDayBySource(sameSource)).toEqual([{ sourceId: 's1', color: '#c44040', count: 3 }]);
// (b) Reihenfolge s2, s1, s2 wird NICHT umsortiert
const mixedOrder = [
ev('e1', day, hourLater, { sourceId: 's2' }),
ev('e2', day, hourLater, { sourceId: 's1' }),
ev('e3', day, hourLater, { sourceId: 's2' }),
];
const groupedMixed = groupDayBySource(mixedOrder);
expect(groupedMixed.map((g) => g.sourceId)).toEqual(['s2', 's1']);
expect(groupedMixed[0].count).toBe(2);
expect(groupedMixed[1].count).toBe(1);
// (c) Termine ohne color -> color der Gruppe ist undefined
const noColor = [ev('e1', day, hourLater, { sourceId: 's1', color: undefined })];
expect(groupDayBySource(noColor)[0].color).toBeUndefined();
// (d) zwei Kalender mit derselben Farbe bleiben zwei Gruppen (Gruppierung nach sourceId)
const sameColorDifferentSource = [
ev('e1', day, hourLater, { sourceId: 's1', color: '#123456' }),
ev('e2', day, hourLater, { sourceId: 's2', color: '#123456' }),
];
expect(groupDayBySource(sameColorDifferentSource)).toHaveLength(2);
// (e) Farbe der Gruppe ist die Farbe des ERSTEN Termins der Gruppe
const laterColorChange = [
ev('e1', day, hourLater, { sourceId: 's1', color: '#111111' }),
ev('e2', day, hourLater, { sourceId: 's1', color: '#222222' }),
];
expect(groupDayBySource(laterColorChange)).toEqual([
{ sourceId: 's1', color: '#111111', count: 2 },
]);
// (f) leeres Array
expect(groupDayBySource([])).toEqual([]);
});
it('Test 9: buildDayBadges liefert je Kalender einen Eintrag, ab dem vierten einen grauen Restkreis', () => {
const day = new Date(2026, 6, 20);
const single = [ev('e1', day, day, { sourceId: 's1', color: '#111111' })];
expect(buildDayBadges(single)).toEqual([
{ key: 's1', color: '#111111', count: 1, rest: false },
]);
const two = [
ev('e1', day, day, { sourceId: 's1', color: '#111111' }),
ev('e2', day, day, { sourceId: 's2', color: '#222222' }),
];
expect(buildDayBadges(two)).toEqual([
{ key: 's1', color: '#111111', count: 1, rest: false },
{ key: 's2', color: '#222222', count: 1, rest: false },
]);
const three = [
ev('e1', day, day, { sourceId: 's1', color: '#111111' }),
ev('e2', day, day, { sourceId: 's2', color: '#222222' }),
ev('e3', day, day, { sourceId: 's3', color: '#333333' }),
];
const threeResult = buildDayBadges(three);
expect(threeResult).toHaveLength(3);
expect(threeResult.every((b) => b.rest === false)).toBe(true);
const four = [
ev('e1', day, day, { sourceId: 's1', color: '#111111' }),
ev('e2a', day, day, { sourceId: 's2', color: '#222222' }),
ev('e2b', day, day, { sourceId: 's2', color: '#222222' }),
ev('e3', day, day, { sourceId: 's3', color: '#333333' }),
ev('e4a', day, day, { sourceId: 's4', color: '#444444' }),
ev('e4b', day, day, { sourceId: 's4', color: '#444444' }),
ev('e4c', day, day, { sourceId: 's4', color: '#444444' }),
];
expect(buildDayBadges(four)).toEqual([
{ key: 's1', color: '#111111', count: 1, rest: false },
{ key: 's2', color: '#222222', count: 2, rest: false },
{ key: '__rest__', color: undefined, count: 4, rest: true },
]);
expect(buildDayBadges(four, 2)).toEqual([
{ key: 's1', color: '#111111', count: 1, rest: false },
{ key: '__rest__', color: undefined, count: 6, rest: true },
]);
expect(buildDayBadges([])).toEqual([]);
});
});