new features

This commit is contained in:
Matthias Hochmeister
2026-03-23 15:07:17 +01:00
parent 34ee80b8c1
commit bfcf1556da
22 changed files with 397 additions and 75 deletions

View File

@@ -174,13 +174,14 @@ async function getOrderById(id: number) {
}
}
async function createOrder(data: { bezeichnung: string; lieferant_id?: number; notizen?: string; budget?: number }, userId: string) {
async function createOrder(data: { bezeichnung: string; lieferant_id?: number; besteller_id?: string; notizen?: string; budget?: number }, userId: string) {
try {
const bestellerId = data.besteller_id && data.besteller_id.trim() ? data.besteller_id.trim() : null;
const result = await pool.query(
`INSERT INTO bestellungen (bezeichnung, lieferant_id, notizen, budget, erstellt_von)
VALUES ($1, $2, $3, $4, $5)
`INSERT INTO bestellungen (bezeichnung, lieferant_id, besteller_id, notizen, budget, erstellt_von)
VALUES ($1, $2, $3, $4, $5, $6)
RETURNING *`,
[data.bezeichnung, data.lieferant_id || null, data.notizen || null, data.budget || null, userId]
[data.bezeichnung, data.lieferant_id || null, bestellerId, data.notizen || null, data.budget || null, userId]
);
const order = result.rows[0];
await logAction(order.id, 'Bestellung erstellt', `Bestellung "${data.bezeichnung}" erstellt`, userId);

View File

@@ -248,7 +248,8 @@ class EventsService {
k.farbe AS kategorie_farbe,
k.icon AS kategorie_icon,
v.datum_von, v.datum_bis, v.ganztaegig,
v.alle_gruppen, v.zielgruppen, v.abgesagt, v.anmeldung_erforderlich
v.alle_gruppen, v.zielgruppen, v.abgesagt, v.anmeldung_erforderlich,
v.wiederholung, v.wiederholung_parent_id
FROM veranstaltungen v
LEFT JOIN veranstaltung_kategorien k ON k.id = v.kategorie_id
WHERE (v.datum_von BETWEEN $1 AND $2 OR v.datum_bis BETWEEN $1 AND $2 OR (v.datum_von <= $1 AND v.datum_bis >= $2))
@@ -274,7 +275,8 @@ class EventsService {
k.farbe AS kategorie_farbe,
k.icon AS kategorie_icon,
v.datum_von, v.datum_bis, v.ganztaegig,
v.alle_gruppen, v.zielgruppen, v.abgesagt, v.anmeldung_erforderlich
v.alle_gruppen, v.zielgruppen, v.abgesagt, v.anmeldung_erforderlich,
v.wiederholung, v.wiederholung_parent_id
FROM veranstaltungen v
LEFT JOIN veranstaltung_kategorien k ON k.id = v.kategorie_id
WHERE v.datum_von > NOW()
@@ -454,6 +456,8 @@ class EventsService {
/**
* Updates an existing event.
* If the event is a recurrence parent and wiederholung is provided,
* it deletes all future instances and regenerates them.
* Returns the updated record or null if not found.
*/
async updateEvent(id: string, data: UpdateVeranstaltungData): Promise<Veranstaltung | null> {
@@ -475,6 +479,7 @@ class EventsService {
max_teilnehmer: data.max_teilnehmer,
anmeldung_erforderlich: data.anmeldung_erforderlich,
anmeldung_bis: data.anmeldung_bis,
wiederholung: data.wiederholung,
};
for (const [col, val] of Object.entries(fieldMap)) {
@@ -494,7 +499,58 @@ class EventsService {
values
);
if (result.rows.length === 0) return null;
return rowToVeranstaltung(result.rows[0]);
const updated = result.rows[0];
// If this is a recurrence parent and wiederholung was updated, regenerate instances
if (data.wiederholung !== undefined && updated.wiederholung_parent_id === null) {
// Delete all existing children of this parent
await pool.query(
`DELETE FROM veranstaltungen WHERE wiederholung_parent_id = $1`,
[id]
);
if (data.wiederholung) {
// Regenerate instances from the (possibly new) dates and config
const datumVon = data.datum_von ? new Date(data.datum_von) : new Date(updated.datum_von);
const datumBis = data.datum_bis ? new Date(data.datum_bis) : new Date(updated.datum_bis);
const occurrenceDates = this.generateRecurrenceDates(datumVon, datumBis, data.wiederholung);
if (occurrenceDates.length > 0) {
const duration = datumBis.getTime() - datumVon.getTime();
for (const occDate of occurrenceDates) {
const occBis = new Date(occDate.getTime() + duration);
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)`,
[
id,
updated.titel,
updated.beschreibung ?? null,
updated.ort ?? null,
updated.ort_url ?? null,
updated.kategorie_id ?? null,
occDate,
occBis,
updated.ganztaegig,
updated.zielgruppen,
updated.alle_gruppen,
updated.max_teilnehmer ?? null,
updated.anmeldung_erforderlich,
updated.erstellt_von,
]
);
}
logger.info(`Regenerated ${occurrenceDates.length} recurrence instances for event ${id}`);
}
} else {
logger.info(`Removed recurrence from event ${id}, all instances deleted`);
}
}
return rowToVeranstaltung(updated);
}
/**

View File

@@ -390,6 +390,31 @@ class PermissionService {
]);
return { groupHierarchy, permissionDeps };
}
/**
* Returns users whose Authentik groups grant a specific permission,
* or who are dashboard_admin (always have all permissions).
*/
async getUsersWithPermission(permissionId: string): Promise<Array<{ id: string; name: string }>> {
// Find all groups that have this permission
const groupsWithPerm: string[] = [];
for (const [group, perms] of this.groupPermissions.entries()) {
if (perms.has(permissionId)) {
groupsWithPerm.push(group);
}
}
// Always include dashboard_admin
groupsWithPerm.push('dashboard_admin');
const result = await pool.query(
`SELECT DISTINCT u.id, COALESCE(u.name, u.email) AS name
FROM users u
WHERE u.authentik_groups && $1::text[]
ORDER BY name ASC`,
[groupsWithPerm]
);
return result.rows;
}
}
export const permissionService = new PermissionService();

View File

@@ -37,16 +37,15 @@ async function createItem(
beschreibung?: string;
kategorie?: string;
geschaetzter_preis?: number;
url?: string;
aktiv?: boolean;
},
userId: string,
) {
const result = await pool.query(
`INSERT INTO shop_artikel (bezeichnung, beschreibung, kategorie, geschaetzter_preis, url, aktiv, erstellt_von)
VALUES ($1, $2, $3, $4, $5, COALESCE($6, true), $7)
`INSERT INTO shop_artikel (bezeichnung, beschreibung, kategorie, geschaetzter_preis, aktiv, erstellt_von)
VALUES ($1, $2, $3, $4, COALESCE($5, true), $6)
RETURNING *`,
[data.bezeichnung, data.beschreibung || null, data.kategorie || null, data.geschaetzter_preis || null, data.url || null, data.aktiv ?? true, userId],
[data.bezeichnung, data.beschreibung || null, data.kategorie || null, data.geschaetzter_preis || null, data.aktiv ?? true, userId],
);
return result.rows[0];
}
@@ -58,10 +57,9 @@ async function updateItem(
beschreibung?: string;
kategorie?: string;
geschaetzter_preis?: number;
url?: string;
aktiv?: boolean;
},
userId: string,
_userId: string,
) {
const fields: string[] = [];
const params: unknown[] = [];
@@ -82,10 +80,6 @@ async function updateItem(
params.push(data.geschaetzter_preis);
fields.push(`geschaetzter_preis = $${params.length}`);
}
if (data.url !== undefined) {
params.push(data.url);
fields.push(`url = $${params.length}`);
}
if (data.aktiv !== undefined) {
params.push(data.aktiv);
fields.push(`aktiv = $${params.length}`);
@@ -95,8 +89,6 @@ async function updateItem(
return getItemById(id);
}
params.push(userId);
fields.push(`aktualisiert_von = $${params.length}`);
params.push(new Date());
fields.push(`aktualisiert_am = $${params.length}`);