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:
@@ -3,12 +3,14 @@ import type { CalendarEvent } from '@/lib/calendar-api';
|
|||||||
import {
|
import {
|
||||||
addDays,
|
addDays,
|
||||||
buildCalendarDays,
|
buildCalendarDays,
|
||||||
|
buildDayBadges,
|
||||||
computeFetchWindow,
|
computeFetchWindow,
|
||||||
dateKey,
|
dateKey,
|
||||||
formatEventDate,
|
formatEventDate,
|
||||||
formatEventTime,
|
formatEventTime,
|
||||||
formatMonthLabel,
|
formatMonthLabel,
|
||||||
gridStartFor,
|
gridStartFor,
|
||||||
|
groupDayBySource,
|
||||||
groupEventsByDate,
|
groupEventsByDate,
|
||||||
resolveCalendarConfig,
|
resolveCalendarConfig,
|
||||||
selectUpcomingEvents,
|
selectUpcomingEvents,
|
||||||
@@ -149,4 +151,99 @@ describe('calendar-month', () => {
|
|||||||
expect(start.getHours()).toBe(0);
|
expect(start.getHours()).toBe(0);
|
||||||
expect(addDays(new Date(2026, 6, 30), 2).getDate()).toBe(1);
|
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([]);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -17,6 +17,12 @@
|
|||||||
* ist nicht Teil dieses Auftrags. Jede Tagesgruppe ist zusaetzlich nach Start
|
* ist nicht Teil dieses Auftrags. Jede Tagesgruppe ist zusaetzlich nach Start
|
||||||
* aufsteigend sortiert, damit die Plakettenfarbe (Farbe des ERSTEN Termins)
|
* aufsteigend sortiert, damit die Plakettenfarbe (Farbe des ERSTEN Termins)
|
||||||
* und die Tooltip-Reihenfolge deterministisch uebereinstimmen (quick-260916-jvj).
|
* und die Tooltip-Reihenfolge deterministisch uebereinstimmen (quick-260916-jvj).
|
||||||
|
*
|
||||||
|
* Liegen an einem Tag Termine aus mehreren Kalendern, gruppiert
|
||||||
|
* `groupDayBySource` sie nach `sourceId` (nicht nach Farbe) in der Reihenfolge
|
||||||
|
* ihres ersten Auftretens in der bereits start-sortierten Tagesgruppe;
|
||||||
|
* `buildDayBadges` macht daraus bis zu drei Kreise, ab dem vierten Kalender
|
||||||
|
* einen grauen Restkreis mit der Summe der uebrigen (quick-260916-k2z).
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { CalendarEvent } from '@/lib/calendar-api';
|
import type { CalendarEvent } from '@/lib/calendar-api';
|
||||||
@@ -120,6 +126,71 @@ export function groupEventsByDate(events: CalendarEvent[]): Map<string, Calendar
|
|||||||
return groupedEvents;
|
return groupedEvents;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface DaySourceGroup {
|
||||||
|
sourceId: string;
|
||||||
|
color?: string;
|
||||||
|
count: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gruppiert die Termine EINES Tages nach `sourceId` (nicht nach Farbe).
|
||||||
|
* Reihenfolge = erstes Auftreten in `events` — die Funktion sortiert selbst
|
||||||
|
* NICHT, sie uebernimmt die Reihenfolge von `groupEventsByDate` (dort bereits
|
||||||
|
* nach Start sortiert), damit Kreis- und Tooltip-Reihenfolge uebereinstimmen.
|
||||||
|
* Die Farbe einer Gruppe ist die `color` ihres ERSTEN Termins.
|
||||||
|
*/
|
||||||
|
export function groupDayBySource(events: CalendarEvent[]): DaySourceGroup[] {
|
||||||
|
const groups = new Map<string, DaySourceGroup>();
|
||||||
|
for (const event of events) {
|
||||||
|
const existing = groups.get(event.sourceId);
|
||||||
|
if (existing) {
|
||||||
|
existing.count += 1;
|
||||||
|
} else {
|
||||||
|
groups.set(event.sourceId, { sourceId: event.sourceId, color: event.color, count: 1 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Array.from(groups.values());
|
||||||
|
}
|
||||||
|
|
||||||
|
export const CALENDAR_DAY_BADGE_MAX = 3;
|
||||||
|
|
||||||
|
export interface DayBadge {
|
||||||
|
key: string;
|
||||||
|
color?: string;
|
||||||
|
count: number;
|
||||||
|
rest: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Baut die Plaketten-Kreise fuer einen Tag: bis `max` Kalender bekommen je
|
||||||
|
* einen Kreis, ab `max` Kalendern fassen die ersten `max - 1` Kreise je einen
|
||||||
|
* Kalender, der letzte Eintrag (`rest: true`) summiert die uebrigen Kalender
|
||||||
|
* grau (kein Farbwert).
|
||||||
|
*/
|
||||||
|
export function buildDayBadges(
|
||||||
|
events: CalendarEvent[],
|
||||||
|
max: number = CALENDAR_DAY_BADGE_MAX,
|
||||||
|
): DayBadge[] {
|
||||||
|
const groups = groupDayBySource(events);
|
||||||
|
if (groups.length <= max) {
|
||||||
|
return groups.map((group) => ({
|
||||||
|
key: group.sourceId,
|
||||||
|
color: group.color,
|
||||||
|
count: group.count,
|
||||||
|
rest: false,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
const visible = groups.slice(0, max - 1).map((group) => ({
|
||||||
|
key: group.sourceId,
|
||||||
|
color: group.color,
|
||||||
|
count: group.count,
|
||||||
|
rest: false,
|
||||||
|
}));
|
||||||
|
const restCount = groups.slice(max - 1).reduce((sum, group) => sum + group.count, 0);
|
||||||
|
return [...visible, { key: '__rest__', color: undefined, count: restCount, rest: true }];
|
||||||
|
}
|
||||||
|
|
||||||
/** 42 Rasterzellen ab `gridStartFor(monthDate)`, Montag-basiert. */
|
/** 42 Rasterzellen ab `gridStartFor(monthDate)`, Montag-basiert. */
|
||||||
export function buildCalendarDays(
|
export function buildCalendarDays(
|
||||||
monthDate: Date,
|
monthDate: Date,
|
||||||
|
|||||||
Reference in New Issue
Block a user