feat(CalendarView): merge recurring training slots and enhance event filtering
All checks were successful
Deploy tt-tagebuch / deploy (push) Successful in 42s

- Implemented a new method to merge recurring training slots with identical weekdays and time windows, improving calendar event management.
- Updated event filtering logic to exclude cancelled training sessions, ensuring only relevant training events are displayed.
- Enhanced the loading process to handle source errors more effectively, improving user experience in the CalendarView.
This commit is contained in:
Torsten Schulz (local)
2026-05-13 00:07:47 +02:00
parent 54d9b9fc86
commit 61b1f27e5e
6 changed files with 293 additions and 26 deletions

View File

@@ -269,9 +269,10 @@ export default {
.filter(event => event.type === 'trainingCancellation')
.flatMap(event => this.getDateKeysForRange(event.date, event.endDate || event.date))
);
this.events = loadedEvents.filter(event => (
const afterCancellations = loadedEvents.filter(event => (
!event.isRecurringTraining || !cancellationDates.has(this.toDateKey(event.date))
));
this.events = this.mergeRecurringTrainingSlots(afterCancellations);
this.sourceErrors = sources
.filter(result => result.status === 'rejected')
.map(result => result.reason?.source)
@@ -283,6 +284,48 @@ export default {
this.loading = false;
},
/**
* Mehrere regelmäßige Trainingszeiten mit identischem Wochentag und gleichem Uhrzeit-Fenster
* (z. B. parallel genutzte Gruppen / Dubletten) zu einem Kalendereintrag zusammenführen.
*/
mergeRecurringTrainingSlots(events) {
const genericTitle = (t) => !t || /^training$/i.test(String(t).trim());
const slotMap = new Map();
const passthrough = [];
for (const e of events) {
if (e.type !== 'training' || !e.isRecurringTraining || !e.time || !e.date) {
passthrough.push(e);
continue;
}
const dk = this.toDateKey(e.date);
const slotKey = `${dk}|${e.time}`;
if (!slotMap.has(slotKey)) {
slotMap.set(slotKey, []);
}
slotMap.get(slotKey).push(e);
}
const mergedSlots = [];
for (const [slotKey, list] of slotMap) {
if (list.length === 1) {
mergedSlots.push(list[0]);
continue;
}
const sorted = list.slice().sort((a, b) => a.startsAt - b.startsAt);
const base = sorted[0];
const rawTitles = [...new Set(sorted.map((x) => String(x.title || '').trim()).filter(Boolean))];
const specific = rawTitles.filter((t) => !genericTitle(t));
const titleJoined = specific.length ? specific.join(' · ') : (rawTitles.join(' · ') || base.title);
const safeIdKey = slotKey.replace(/\|/g, '-');
mergedSlots.push({
...base,
id: `training-merged-${safeIdKey}`,
title: titleJoined,
subtitle: 'Regelmäßige Trainingszeit',
startsAt: Math.min(...sorted.map((x) => x.startsAt))
});
}
return [...passthrough, ...mergedSlots];
},
async loadSource(source, loader) {
try {
const events = await loader();