bug fixes
This commit is contained in:
@@ -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`
|
||||
);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user