new features

This commit is contained in:
Matthias Hochmeister
2026-03-23 14:01:39 +01:00
parent d2dc64d54a
commit 3326156b15
35 changed files with 1341 additions and 257 deletions

View File

@@ -390,60 +390,63 @@ class EventsService {
* 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 limitDate = config.bis ? new Date(config.bis + 'T23:59:59Z') : new Date(0);
const interval = config.intervall ?? 1;
// Cap at 100 instances max, and 2 years
const maxDate = new Date(startDate);
maxDate.setFullYear(maxDate.getFullYear() + 2);
maxDate.setUTCFullYear(maxDate.getUTCFullYear() + 2);
const effectiveLimit = limitDate < maxDate ? limitDate : maxDate;
let current = new Date(startDate);
const originalDay = startDate.getDate();
// Work in UTC to avoid timezone shifts
let currentMs = startDate.getTime();
const originalDay = startDate.getUTCDate();
const startHours = startDate.getUTCHours();
const startMinutes = startDate.getUTCMinutes();
while (dates.length < 100) {
let current = new Date(currentMs);
// Advance to next occurrence
switch (config.typ) {
case 'wöchentlich':
current = new Date(current);
current.setDate(current.getDate() + 7 * interval);
current.setUTCDate(current.getUTCDate() + 7 * interval);
break;
case 'zweiwöchentlich':
current = new Date(current);
current.setDate(current.getDate() + 14);
current.setUTCDate(current.getUTCDate() + 14);
break;
case 'monatlich_datum': {
current = new Date(current);
const targetMonth = current.getMonth() + 1;
current.setDate(1);
current.setMonth(targetMonth);
const lastDay = new Date(current.getFullYear(), current.getMonth() + 1, 0).getDate();
current.setDate(Math.min(originalDay, lastDay));
const targetMonth = current.getUTCMonth() + interval;
current.setUTCDate(1);
current.setUTCMonth(targetMonth);
const lastDay = new Date(Date.UTC(current.getUTCFullYear(), current.getUTCMonth() + 1, 0)).getUTCDate();
current.setUTCDate(Math.min(originalDay, lastDay));
current.setUTCHours(startHours, startMinutes, 0, 0);
break;
}
case 'monatlich_erster_wochentag': {
const targetWeekday = config.wochentag ?? 0; // 0=Mon
current = new Date(current);
current.setMonth(current.getMonth() + 1);
current.setDate(1);
current.setUTCMonth(current.getUTCMonth() + 1);
current.setUTCDate(1);
// Convert JS Sunday=0 to Monday=0: (getDay()+6)%7
while ((current.getDay() + 6) % 7 !== targetWeekday) {
current.setDate(current.getDate() + 1);
while ((current.getUTCDay() + 6) % 7 !== targetWeekday) {
current.setUTCDate(current.getUTCDate() + 1);
}
current.setUTCHours(startHours, startMinutes, 0, 0);
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);
current.setUTCMonth(current.getUTCMonth() + 2);
current.setUTCDate(0);
while ((current.getUTCDay() + 6) % 7 !== targetWeekday) {
current.setUTCDate(current.getUTCDate() - 1);
}
current.setUTCHours(startHours, startMinutes, 0, 0);
break;
}
}
if (current > effectiveLimit) break;
currentMs = current.getTime();
dates.push(new Date(current));
}
return dates;
@@ -515,16 +518,63 @@ class EventsService {
* Hard-deletes an event (and any recurrence children) from the database.
* Returns true if the event was found and deleted, false if not found.
*/
async deleteEvent(id: string): Promise<boolean> {
logger.info('Hard-deleting event', { id });
// Delete recurrence children first (wiederholung_parent_id references)
await pool.query(
`DELETE FROM veranstaltungen WHERE wiederholung_parent_id = $1`,
async deleteEvent(id: string, mode: 'all' | 'single' | 'future' = 'all'): Promise<boolean> {
logger.info('Hard-deleting event', { id, mode });
if (mode === 'single') {
// Delete only this single instance
const result = await pool.query(
`DELETE FROM veranstaltungen WHERE id = $1`,
[id]
);
return (result.rowCount ?? 0) > 0;
}
if (mode === 'future') {
// Delete this instance and all later instances in the same series
const event = await pool.query(
`SELECT id, datum_von, wiederholung_parent_id FROM veranstaltungen WHERE id = $1`,
[id]
);
if (event.rows.length === 0) return false;
const row = event.rows[0];
const parentId = row.wiederholung_parent_id ?? row.id;
const datumVon = new Date(row.datum_von);
// Delete this instance and all siblings/children with datum_von >= this one
await pool.query(
`DELETE FROM veranstaltungen
WHERE (wiederholung_parent_id = $1 OR id = $1)
AND datum_von >= $2
AND id != $1`,
[parentId, datumVon]
);
// Also delete the selected instance itself
await pool.query(
`DELETE FROM veranstaltungen WHERE id = $1`,
[id]
);
return true;
}
// mode === 'all': Delete parent + all children (original behavior)
// First check if this is a child instance — find the parent
const event = await pool.query(
`SELECT id, wiederholung_parent_id FROM veranstaltungen WHERE id = $1`,
[id]
);
if (event.rows.length === 0) return false;
const parentId = event.rows[0].wiederholung_parent_id ?? id;
// Delete all children of the parent
await pool.query(
`DELETE FROM veranstaltungen WHERE wiederholung_parent_id = $1`,
[parentId]
);
// Delete the parent itself
const result = await pool.query(
`DELETE FROM veranstaltungen WHERE id = $1`,
[id]
[parentId]
);
return (result.rowCount ?? 0) > 0;
}
@@ -603,9 +653,9 @@ class EventsService {
FROM (
SELECT unnest(authentik_groups) AS group_name
FROM users
WHERE is_active = true
WHERE authentik_groups IS NOT NULL
) g
WHERE group_name LIKE 'dashboard_%'
WHERE group_name != 'dashboard_admin'
ORDER BY group_name`
);