bug fixes

This commit is contained in:
Matthias Hochmeister
2026-03-03 11:45:08 +01:00
parent 3101f1a9c5
commit d91f757f34
12 changed files with 313 additions and 47 deletions

View File

@@ -4,6 +4,7 @@ import {
VeranstaltungKategorie,
Veranstaltung,
VeranstaltungListItem,
WiederholungConfig,
CreateKategorieData,
UpdateKategorieData,
CreateVeranstaltungData,
@@ -31,6 +32,8 @@ function rowToListItem(row: any): VeranstaltungListItem {
zielgruppen: row.zielgruppen ?? [],
abgesagt: row.abgesagt,
anmeldung_erforderlich: row.anmeldung_erforderlich,
wiederholung: row.wiederholung ?? null,
wiederholung_parent_id: row.wiederholung_parent_id ?? null,
};
}
@@ -62,6 +65,9 @@ function rowToVeranstaltung(row: any): Veranstaltung {
kategorie_farbe: row.kategorie_farbe ?? null,
kategorie_icon: row.kategorie_icon ?? null,
erstellt_von_name: row.erstellt_von_name ?? null,
// Recurrence fields
wiederholung: row.wiederholung ?? null,
wiederholung_parent_id: row.wiederholung_parent_id ?? null,
};
}
@@ -193,16 +199,10 @@ class EventsService {
/**
* Deletes an event category.
* Throws if any events still reference this category.
* The DB schema uses ON DELETE SET NULL, so related events will have
* their kategorie_id set to NULL automatically.
*/
async deleteKategorie(id: string): Promise<void> {
const refCheck = await pool.query(
`SELECT COUNT(*) AS cnt FROM veranstaltungen WHERE kategorie_id = $1`,
[id]
);
if (Number(refCheck.rows[0].cnt) > 0) {
throw new Error('Kategorie kann nicht gelöscht werden, da sie noch Veranstaltungen enthält');
}
const result = await pool.query(
`DELETE FROM veranstaltung_kategorien WHERE id = $1`,
[id]
@@ -306,8 +306,8 @@ class EventsService {
datum_von, datum_bis, ganztaegig,
zielgruppen, alle_gruppen,
max_teilnehmer, anmeldung_erforderlich, anmeldung_bis,
erstellt_von
) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14)
erstellt_von, wiederholung
) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15)
RETURNING *`,
[
data.titel,
@@ -324,11 +324,110 @@ class EventsService {
data.anmeldung_erforderlich,
data.anmeldung_bis ?? null,
userId,
data.wiederholung ?? null,
]
);
// Generate recurrence instances if wiederholung is specified
if (data.wiederholung) {
const occurrenceDates = this.generateRecurrenceDates(data.datum_von, data.datum_bis, data.wiederholung);
if (occurrenceDates.length > 0) {
const duration = data.datum_bis.getTime() - data.datum_von.getTime();
const instanceParams: any[][] = [];
for (const occDate of occurrenceDates) {
const occBis = new Date(occDate.getTime() + duration);
instanceParams.push([
result.rows[0].id, // wiederholung_parent_id
data.titel,
data.beschreibung ?? null,
data.ort ?? null,
data.ort_url ?? null,
data.kategorie_id ?? null,
occDate,
occBis,
data.ganztaegig,
data.zielgruppen,
data.alle_gruppen,
data.max_teilnehmer ?? null,
data.anmeldung_erforderlich,
userId,
]);
}
// Insert instances in a loop (simpler than building dynamic bulk insert)
for (const params of instanceParams) {
await pool.query(
`INSERT INTO veranstaltungen (
wiederholung_parent_id, titel, beschreibung, ort, ort_url, kategorie_id,
datum_von, datum_bis, ganztaegig, zielgruppen, alle_gruppen,
max_teilnehmer, anmeldung_erforderlich, erstellt_von
) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14)`,
params
);
}
logger.info(`Created ${instanceParams.length} recurrence instances for event ${result.rows[0].id}`);
}
}
return rowToVeranstaltung(result.rows[0]);
}
/** Returns all future occurrence dates for a recurring event (excluding the base occurrence).
* Capped at 100 instances and 2 years from the start date. */
private generateRecurrenceDates(startDate: Date, _endDate: Date, config: WiederholungConfig): Date[] {
const dates: Date[] = [];
const limitDate = new Date(config.bis);
const interval = config.intervall ?? 1;
// Cap at 100 instances max, and 2 years
const maxDate = new Date(startDate);
maxDate.setFullYear(maxDate.getFullYear() + 2);
const effectiveLimit = limitDate < maxDate ? limitDate : maxDate;
let current = new Date(startDate);
while (dates.length < 100) {
// Advance to next occurrence
switch (config.typ) {
case 'wöchentlich':
current = new Date(current);
current.setDate(current.getDate() + 7 * interval);
break;
case 'zweiwöchentlich':
current = new Date(current);
current.setDate(current.getDate() + 14);
break;
case 'monatlich_datum':
current = new Date(current);
current.setMonth(current.getMonth() + 1);
break;
case 'monatlich_erster_wochentag': {
const targetWeekday = config.wochentag ?? 0; // 0=Mon
current = new Date(current);
current.setMonth(current.getMonth() + 1);
current.setDate(1);
// Convert JS Sunday=0 to Monday=0: (getDay()+6)%7
while ((current.getDay() + 6) % 7 !== targetWeekday) {
current.setDate(current.getDate() + 1);
}
break;
}
case 'monatlich_letzter_wochentag': {
const targetWeekday = config.wochentag ?? 0;
current = new Date(current);
// Go to last day of next month
current.setMonth(current.getMonth() + 2);
current.setDate(0);
while ((current.getDay() + 6) % 7 !== targetWeekday) {
current.setDate(current.getDate() - 1);
}
break;
}
}
if (current > effectiveLimit) break;
dates.push(new Date(current));
}
return dates;
}
/**
* Updates an existing event.
* Returns the updated record or null if not found.
@@ -460,9 +559,13 @@ class EventsService {
};
const result = await pool.query(
`SELECT DISTINCT unnest(authentik_groups) AS group_name
FROM users
WHERE is_active = true
`SELECT DISTINCT group_name
FROM (
SELECT unnest(authentik_groups) AS group_name
FROM users
WHERE is_active = true
) g
WHERE group_name LIKE 'dashboard_%'
ORDER BY group_name`
);