Source: Router/group/controller.js

/**
 * Group Controller
 * 
 * Contains all the business logic for group operations:
 * - Account read-only checks
 * - Sister group link management
 * - Banner recreation and inheritance
 * - Group creation and updates
 * - Group deletion
 * - Group data retrieval
 * - Admin permission checks
 * - Minimum timestamp calculations
 */
// @ts-check
/**
 * @import {ExpressRequestAuthorized, ExpressResponse} from './../../types.js'
 */

import { query, transaction, UUID2hex, HEX2uuid } from '@commtool/sql-query';
import { parseTimestampToSeconds, parseTimestampToSecondsOrDefault } from '../../utils/parseTimestamp.js';
import { renderObject } from '../../utils/renderTemplates.js';
import { Templates, BannerRules, getConfig, addExtraConfigToOrgaData } from '../../utils/compileTemplates.js';
import { getUID, isValidUID } from '../../utils/UUIDs.js';
import { queueAdd } from '../../tree/treeQueue/treeQueue.js';
import { addUpdateList, addUpdateEntry } from '../../server.ws.js';
import { recreateJobsGroup } from '../job/utilities.js';
import { isObjectAdmin } from '../../utils/authChecks.js';
import { publishEvent } from '../../utils/events.js';
import { rebuildFeesGroup } from '../../tree/rebuildFees.js';
import { errorLoggerRead, errorLoggerUpdate } from '../../utils/requestLogger.js';
import { keysEqual } from '../../utils/keyCompare.js';
import { timestampCheck } from '../person/migratePerson.js';

/**
 * Checks if the current user has read-only access to group accounts
 * @param {Object} req - Express request object
 * @returns {Promise<boolean>} True if accounts are read-only for this user
 */
const accountsReadOnly = async (req) => {
    // Holt die Konfiguration für die aktuelle Anfrage (z.B. Einstellungen zu Gebühren und Gruppen)
    const config = await getConfig('db', req);

    // Prüft, ob in der Konfiguration eine spezielle Liste für Gruppen-Kontenmanager existiert
    if (config.fees?.listManagersGroupAccounts) {
        // Führt eine SQL-Abfrage aus, um zu prüfen, ob der aktuelle Benutzer Mitglied in der speziellen Manager-Gruppe ist
        const result = await query(`
            SELECT Main.Title, Member.Display, ObjectBase.Data
            FROM ObjectBase 
            INNER JOIN ObjectBase AS Main ON (Main.UID = ObjectBase.UIDBelongsTo)
            INNER JOIN Member ON (Member.UID = ObjectBase.UIDBelongsTo)
            INNER JOIN Links ON (Links.UID = ObjectBase.UID AND Links.Type IN ('member','memberA'))
            INNER JOIN ObjectBase AS list ON (list.UID = Links.UIDTarget AND list.Type IN ('list','dlist'))
            WHERE Main.Type IN ('person','extern') 
              AND ObjectBase.UIDBelongsTo = ?         -- aktueller Benutzer
              AND Links.UIDTarget = ?                 -- spezielle Manager-Gruppe
        `, [
            UUID2hex(req.session.user),                      // Benutzer-UUID in hex
            UUID2hex(config.fees.listManagersGroupAccounts)   // Manager-Gruppen-UUID in hex
        ]);

        // Gibt true zurück, wenn der Benutzer NICHT in der Manager-Gruppe ist (also nur Leserechte hat)
        return result.length === 0;
    } else {
        // Gibt false zurück, wenn keine spezielle Manager-Gruppe definiert ist (also keine Einschränkung)
        return false;
    }
};

/**
 * Verarbeitet die Verknüpfung einer Gruppe mit einer "Schwestergruppe" (Sister Group).
 * 
 * Diese Funktion prüft, ob die aktuelle Gruppe (UID) mit einer anderen Gruppe (belongsTo) als Schwestergruppe verknüpft werden soll.
 * Falls sich die Verknüpfung geändert hat oder entfernt werden soll, werden die entsprechenden Links in der Datenbank aktualisiert.
 * Zusätzlich werden alle Mitglieder der Gruppe ggf. auch in die Schwestergruppe übernommen.
 * 
 * @param {ExpressRequestAuthorized} req - Die aktuelle Express-Request-Instanz (enthält z.B. belongsTo in req.query)
 * @param {Buffer} UID - Die UID der aktuellen Gruppe
 * @returns {Promise<object|null>} - Gibt die Schwestergruppe zurück, falls eine neue gesetzt wurde, sonst null
 */
const handleSisterLink = async (req, UID) => {
    // Extrahiere die Zielgruppe (belongsTo) aus den Query-Parametern, falls vorhanden
    const authReq = /** @type {ExpressRequestAuthorized} */ (/** @type {unknown} */ (req));
    const belongsTo = req.query.belongsTo ? UUID2hex(req.query.belongsTo) : null;
    const rootHex = UUID2hex(authReq.session.root);
    const userHex = UUID2hex(authReq.session.user);

    // Hole die bisherige Schwestergruppen-Verknüpfung (falls vorhanden)
    const oldBT = await query(`SELECT UIDTarget FROM Links WHERE UID=? AND Type='memberS'`, [UID]);

    // Prüfe, ob sich die Schwestergruppe geändert hat oder entfernt werden soll
    if (oldBT.length === 0 || !belongsTo || !oldBT[0].UIDTarget.equals(belongsTo)) {
        // Entferne alte Schwestergruppen-Verknüpfung
        await query(`DELETE FROM Links WHERE UID=? AND Type='memberS'`, [UID]);

        // Hole alle Mitglieder der aktuellen Gruppe
        const members = await query(`SELECT MLink.UID AS UID, ObjectBase.Type FROM Links AS MLink
            INNER JOIN ObjectBase ON (ObjectBase.UID=MLink.UID AND ObjectBase.Type IN ('person','extern','job','guest','group','ggroup'))
            WHERE MLink.UIDTarget=? AND MLink.Type IN ('member','memberA','memberS')`, [UID]);

        if (belongsTo) {
            // Setze neue Schwestergruppen-Verknüpfung
            await query(`INSERT INTO Links (UID,Type,UIDTarget) VALUES(?,'memberS',?)`, [UID, belongsTo]);

            // Füge alle Mitglieder der aktuellen Gruppe auch der Schwestergruppe hinzu
            if (members.length > 0) {
                await query(`INSERT IGNORE INTO Links (UID,Type,UIDTarget) VALUES(?,'member',?)`,
                    members.map(m => ([m.UID, belongsTo])), { batch: true });
            }

            // Hole die Daten der neuen Schwestergruppe
            const s = await query(`SELECT ObjectBase.UID,ObjectBase.Type,ObjectBase.Title,Member.Display AS Display,
                ObjectBase.dindex, Member.Data AS Data,ObjectBase.gender, ObjectBase.hierarchie,  ObjectBase.stage ,UNIX_TIMESTAMP(ObjectBase.validFrom)  AS validFrom
                FROM ObjectBase 
                INNER JOIN Member ON (Member.UID=ObjectBase.UID)
                WHERE ObjectBase.UID=?  AND ObjectBase.Type ='group'`,
                [belongsTo],
                { cast: ['UUID', 'json'], log: false }
            );

            // Hole die UID der alten Schwestergruppe (falls vorhanden)
            const oldBTUID = oldBT.length > 0 ? oldBT[0].UIDTarget : null;
            if (oldBTUID) {
                // Trage Änderung in die Warteschlange für die alte Schwestergruppe ein
                await queueAdd(UUID2hex(req.session.root), UUID2hex(req.session.user), 'group', UID, UID, oldBTUID, null);
            }
            // Trage Änderung in die Warteschlange für die neue Schwestergruppe ein
            await queueAdd(rootHex, userHex, 'group', UID, UID, null, belongsTo, null);
            // Gib die neue Schwestergruppe zurück
            return s[0];
        } else {
            // Wenn keine neue Schwestergruppe gesetzt wurde, entferne alle Mitgliedschaften aus der alten Schwestergruppe
            const oldBTUID = oldBT.length > 0 ? oldBT[0].UIDTarget : null;

            if (oldBTUID) {
                // Trage Änderung in die Warteschlange für die alte Schwestergruppe ein
                await queueAdd(rootHex, userHex, 'group', UID, UID, oldBTUID, null, null);
            }
        }
    }
    // Wenn keine Änderung, gib null zurück
    return null;
};

/**
 * Recreates and propagates banner to child groups and members
 * @param {Buffer} UID - Group UID
 * @param {string} banner - Banner URL/path
 * @param {Object} bannerRules - Banner inheritance rules
 */
const recreateBanner = async (UID, banner, bannerRules, req) => {
    try {
        // 1. Holt alle direkten Unterobjekte (Kinder) der aktuellen Gruppe (UID), die ein Banner erben könnten.
        const children = await query(`SELECT Member.Data, ObjectBase.Type, ObjectBase.Data AS OData, 
            ObjectBase.UID, gMember.Data AS gData, Links.Type AS LType
            FROM ObjectBase 
            INNER JOIN Member ON (Member.UID = ObjectBase.UIDBelongsTo)
            INNER JOIN Links AS gLink ON (gLink.UID=ObjectBase.UID AND gLink.Type='memberA')
            INNER JOIN Member AS gMember ON (gMember.UID=gLink.UIDTarget)
            INNER JOIN Links ON (Links.UID=ObjectBase.UID AND Links.Type IN ('memberA') )
            WHERE Links.UIDTarget=? AND ObjectBase.Type IN ('job','group','person')
            GROUP BY ObjectBase.UID
        `,
            [UID],
            {
                cast: ['json'],
                filter: (entry) => {
                    // 2. Filtert die Kinder: Nur solche, die das Banner erben sollen oder direkte Mitglieder sind.
                    return (bannerRules.group[entry.Data.hierarchie][0] === 'inherit' && entry.Type === 'group' ||
                        bannerRules.group[entry.gData.hierarchie].includes('inherit') && entry.Type === 'job' ||
                        (bannerRules.group[entry.gData.hierarchie].includes('inherit') || entry.LType === 'memberA') && entry.Type === 'person'
                    );
                }
            });

        const updatedGroups = [];
        for (const child of children) {
            if (child.Type === 'group' || child.Type === 'person') {
                // 3a. Für Gruppen und Personen: Banner im Member-Data aktualisieren.
                const NewData = { ...child.Data, banner: banner };
                await query(`UPDATE Member SET Data = ? WHERE UID=?`,
                    [JSON.stringify(NewData), child.UID]);
                if (child.Type === 'group') {
                    // 4. Rekursiver Aufruf für Untergruppen, damit das Banner weitervererbt wird.
                    recreateBanner(child.UID, banner, bannerRules, req);
                    updatedGroups.push(child.UID);
                    // 5. Event veröffentlichen, dass sich das Banner geändert hat.
                    publishEvent(`/change/group/${req.session.root}/${HEX2uuid(UID)}`, {
                        organization: req.session.root,
                        data: { banner: NewData.banner }
                    });
                }
            } else {
                // 3b. Für Jobs: Banner im ObjectBase-Data aktualisieren (nur wenn sich das Banner geändert hat).
                if (child.OData.banner !== banner) {
                    await query(`UPDATE ObjectBase SET Data = ? WHERE UID=?`,
                        [JSON.stringify({ ...child.OData, banner: banner }), child.UID]);
                }
            }
        }
        // 6. Aktualisiert die Eltern-Objekte und die geänderten Gruppen für die Synchronisation.
        const parents = await query(`SELECT Links.UIDTarget AS UID FROM Links WHERE Links.UID=?  AND Links.Type IN ('member','memberA','memberS','memberG')`, [UID]);
        addUpdateList(parents.map(el => el.UID));
        addUpdateList([UID, ...updatedGroups]);
    } catch (e) {
        // 7. Fehlerbehandlung
        errorLoggerUpdate(e);
    }
};


/**
 * Creates or updates a group
 * @param {ExpressRequestAuthorized} req - Express request object
 * @param {ExpressResponse} res - Express response object
 */
export const putGroup = async ( req,  res) => {
    try {
    const authReq = /** @type {ExpressRequestAuthorized} */ (/** @type {unknown} */ (req));
    const rootHex = UUID2hex(authReq.session.root);
    const userHex = UUID2hex(authReq.session.user);
        // Extrahiere optionalen Zeitstempel aus der Query (für Backdating)
        const timestamp = parseTimestampToSeconds(req.query.timestamp ? String(req.query.timestamp) : undefined);

        // Lade das Gruppentemplate für das aktuelle Root
        const template = Templates[authReq.session.root].group;

        // Ermittle die UID für die Gruppe (neu oder bestehend)
        const UID = await getUID(req);

        // Erzeuge das Gruppenobjekt aus Template und Requestdaten
        const object = await renderObject(template, req.body, req);

        // Hole die Zielgruppen-UID aus den URL-Parametern (hex-codiert)
        const UIDgroup = UUID2hex(req.params.UIDparent);

        // Prüfe, ob der aktuelle User nur Leserechte auf Konten hat
        const accountsProtected = await accountsReadOnly(req);

        // Lade die Daten der Zielgruppe aus der Datenbank
        const mgroups = await query(`
            SELECT Member.Data,ObjectBase.UID,Member.Display,ObjectBase.Title,
                   UNIX_TIMESTAMP(ObjectBase.validFrom) AS validFrom 
            FROM ObjectBase
            INNER JOIN Member ON (Member.UID=ObjectBase.UID)
            WHERE ObjectBase.UID=? AND ObjectBase.Type='group'`,
            [UIDgroup], { cast: ['json'] }
        );

        // Wenn die Zielgruppe nicht existiert, Fehler zurückgeben
        if (!mgroups.length) {
            res.status(300).json({ success: false, message: 'invalid group UID' });
            return;
        }

        // Prüfe, ob das Geschlecht der Gruppe mit dem Request übereinstimmt
        const group = mgroups[0];
        if (group.Data.gender !== req.body.gender && group.Data.gender !== 'C' && group.Data.gender !== 'B' && req.body.gender !== 'B' && req.body.gender !== 'C') {
            res.status(300).json({ success: false, message: 'invalid group gender' });
            return;
        }

        // Prüfe, ob die Stufe der Gruppe mit dem Request übereinstimmt
        if (group.Data.stage !== req.body.stage && group.Data.stage !== 0) {
            res.status(300).json({ success: false, message: 'invalid group stage' });
            return;
        }

        // Banner-Vererbung: Setze ggf. das Banner aus der Eltern-Gruppe
        if (BannerRules[req.session.root].group[req.body.hierarchie] === 'inherit') {
            if (group.Data.banner) {
                req.body.banner = group.Data.banner;
            }
        } else if (BannerRules[req.session.root].group[req.body.hierarchie].includes('inherit') && !req.body.banner) {
            req.body.banner = group.Data.banner;
        }

        // Prüfe, ob die Gruppe bereits existiert (für den aktuellen User)
        const groups = await query(`
            SELECT Member.Data,Links.UIDTarget AS MemberA,UNIX_TIMESTAMP(ObjectBase.validFrom) AS validFrom,
                   Title,UIDBelongsTo
            FROM ObjectBase 
            INNER JOIN Member ON (Member.UID=ObjectBase.UID)
            INNER JOIN Links ON(Links.Type='memberA' AND Links.UID=ObjectBase.UID) 
            WHERE ObjectBase.UID=? AND ObjectBase.Type='group'`, [UID]);
        // Wenn die Gruppe noch nicht existiert, lege sie neu an
        if (!groups.length) {
            // Ermittle das erlaubte Backdate-Limit
            const limitedTimestamp = await timestampCheck(UIDgroup, timestamp);

            // Prüfe, ob die UID im Body gültig ist
            if (!isValidUID(req.body.UID)) {
                res.status(300).json({ success: false, message: 'invalid UID format in body.UID' });
                return;
            }

            // Erzeuge die Daten für die neue Gruppe
            const NewData = { ...req.body, UID: undefined };
            if (accountsProtected) {
                NewData.accounts = [];
            }

            // Schreibe die neue Gruppe in einer Transaktion in die Datenbank
            await transaction(async (connection) => {
                // Objektbasis anlegen
                await query(`
                    INSERT INTO ObjectBase(UID,UIDuser,Type,UIDBelongsTo,Title, dindex,hierarchie,stage,gender)
                    VALUES (?,?,'group',?,?,?,?,?,?)`,
                    [UID, userHex, UID, object.Title, 0,
                        object.hierarchie, object.stage, object.gender],
                    { connection, log: false });

                // Link zur Eltern-Gruppe anlegen
                await query(`INSERT IGNORE INTO Links(UID, Type, UIDTarget,UIDuser) VALUES (?,'memberA',?,?)`,
                    [UID, UIDgroup, userHex],
                    { connection, log: false });

            }, { backDate: limitedTimestamp });

            // Member-Eintrag anlegen oder aktualisieren
            await query(`INSERT INTO Member 
                        (UID,Display,SortName,FullTextIndex, Data)
                        VALUES(?,?,?,?,?)
                        ON DUPLICATE KEY UPDATE Display=VALUE(Display),SortName=VALUE(SortName),FullTextIndex=VALUE(FullTextIndex),Data=VALUE(Data)`,
                [UID, object.Display, object.SortIndex, object.FullTextIndex, JSON.stringify(NewData)]);

            // Änderungen in die Warteschlange eintragen
            queueAdd(rootHex, userHex, 'group', object.UID, object.UID, null, UIDgroup, timestamp);

            // Verknüpfung mit Schwestergruppen, falls vorhanden
            object.sibling = await handleSisterLink(req, UID);

            // Event veröffentlichen, dass eine neue Gruppe angelegt wurde
            publishEvent(`/add/group/group/${authReq.session.root}/${req.params.UIDparent}`, {
                organization: authReq.session.root,
                data: [HEX2uuid(object.UID)]
            });

            // Ergebnis zurückgeben
            res.json({ success: true, result: { ...object, UID: HEX2uuid(object.UID), Data: NewData } });
        } else {
            // Gruppe existiert bereits: Prüfe Adminrechte
            if (!isObjectAdmin(req, UIDgroup)) {
                res.status(403).json({ success: false, message: 'user not authorized to change this group' });
                return;
            }

            // Lade aktuelle Gruppendaten
            const existingGroup = { ...groups[0], Data: JSON.parse(groups[0].Data) };
            const updateTimestamp = parseTimestampToSecondsOrDefault(req.query.timestamp, groups[0].validFrom + 1);

            // Prüfe, ob Hierarchie, Stage oder Gender geändert werden sollen (nicht erlaubt)
            if (existingGroup.Data.hierarchie !== req.body.hierarchie) {
                res.json({ success: false, message: 'you can not update hierarchie of an existing group' });
                return;
            }
            if (existingGroup.Data.stage !== req.body.stage) {
                res.json({ success: false, message: 'you can not update stage of an existing group' });
                return;
            }
            if (existingGroup.Data.gender !== req.body.gender) {
                res.json({ success: false, message: 'you can not update gender of an existing group' });
                return;
            }

            // Wenn sich die Gebühren geändert haben, Gebührenstruktur neu aufbauen
            if (JSON.stringify(existingGroup.Data.fees) !== JSON.stringify(req.body.fees)) {
                rebuildFeesGroup(UID, req.session.root);
            }

            // Neue Daten für das Update erzeugen
            const NewData = { ...req.body, UID: undefined };
            if (accountsProtected) {
                NewData.accounts = existingGroup.Data.accounts;
            }

            // Wenn sich der Titel geändert hat, führe das Update in einer Transaktion durch
            if (existingGroup.Title !== object.Title) {
                await transaction(async (connection) => {
                    await connection.query(`UPDATE ObjectBase SET Title=? WHERE UID=?`,
                        [object.Title, UID]);

                    // Gruppen-Migration ist aktuell nicht unterstützt
                    if (!existingGroup.MemberA.equals(UIDgroup)) {
                        return { result: false, message: 'group migration currently not supported' };
                    }

                    // Aktualisiere alle Jobs der Gruppe
                    recreateJobsGroup(req, UIDgroup, updateTimestamp);
                    return { success: true, result: { ...object, UID: HEX2uuid(object.UID), Data: NewData } };
                }, { backDate: updateTimestamp });
            }

            // Aktualisiere Member-Daten der Gruppe
            await query(`UPDATE Member SET 
                        Display=?,SortName=?,FullTextIndex=?, Data=?
                        WHERE UID=?`,
                [object.Display, object.SortIndex, object.FullTextIndex, JSON.stringify(NewData), UID]);

            // Verknüpfung mit Schwestergruppen, falls vorhanden
            object.sibling = await handleSisterLink(req, UID);

            // Update-Listen pflegen
            addUpdateList(UID);

            // Prüfe, ob sich Daten geändert haben und veröffentliche ggf. ein Event
            const [equal, diff] = keysEqual(NewData, existingGroup.Data);
            if (!equal) {
                publishEvent(`/change/group/${req.session.root}/${HEX2uuid(UID)}`, {
                    organization: req.session.root,
                    data: { UID: HEX2uuid(UID), diff },
                    backDate: updateTimestamp
                });
            }

            // Update-Entry für die Gruppe anlegen
            addUpdateEntry(UID, { group: { ...object, Data: NewData, UID: req.params.UID, parent: existingGroup } });

            // Ergebnis zurückgeben
            res.json({ success: true, result: { ...object, UID: HEX2uuid(UID), Data: NewData } });
        }
    } catch (e) {
        // Fehlerbehandlung: Fehler loggen
        errorLoggerUpdate(e);
    }
};

/**
 * Updates an existing group
 * @param {ExpressRequestAuthorized} req - Express request object
 * @param {ExpressResponse} res - Express response object
 */
export const updateGroup = async ( req, res) => {
    try {
        // Konvertiere die Gruppen-UID aus dem URL-Parameter in das interne Hex-Format
        const UID = UUID2hex(req.params.UID);

        // Prüfe, ob der aktuelle User nur Leserechte auf Konten hat
        const accountsProtected = await accountsReadOnly(req);

        // Lade die aktuellen Daten der Gruppe aus der Datenbank
        const mgroups = await query(`SELECT Member.Data,ObjectBase.Title,
            UNIX_TIMESTAMP(ObjectBase.validFrom) AS validFrom 
            FROM ObjectBase
            INNER JOIN Member ON (ObjectBase.UID=Member.UID)
            WHERE ObjectBase.UID=? `, [UID], { cast: ['json'] });

        // Wenn die Gruppe nicht existiert, Fehler zurückgeben
        if (!mgroups.length) {
            res.status(300).json({ success: false, message: 'invalid group UID' });
            return;
        }

        // Ermittle den Zeitstempel für das Update (entweder aus Query oder aus der Gruppe)
        const timestamp = parseTimestampToSecondsOrDefault(req.query.timestamp, Number(mgroups[0].validFrom));

        const group = mgroups[0];

        // Prüfe, ob Hierarchie, Stage oder Gender geändert werden sollen (nicht erlaubt)
        if (req.body.hierarchie && group.Data.hierarchie !== req.body.hierarchie) {
            res.json({ success: false, message: 'you can not update hierarchie of an existing group' });
            return;
        }
        if (req.body.stage && group.Data.stage !== req.body.stage) {
            res.json({ success: false, message: 'you can not update stage of an existing group' });
            return;
        }
        if (req.body.gender && group.Data.gender !== req.body.gender) {
            res.json({ success: false, message: 'you can not update gender of an existing group' });
            return;
        }

        // Erzeuge die neuen Gruppendaten (alte Daten + neue Werte aus dem Request)
        const NewData = ({ ...group.Data, ...req.body, UID: UID });

        // Wenn Konten geschützt sind, überschreibe Accounts mit den alten Werten
        if (accountsProtected) {
            NewData.accounts = group.Data.accounts;
        }

        // Hole das Gruppentemplate für das aktuelle Root
        const template = Templates[req.session.root].group;

        // Rendere das Gruppenobjekt für die Anzeige und Speicherung
        const object = await renderObject(template, NewData, req);

        // Wenn sich der Titel geändert hat, aktualisiere ihn in ObjectBase
        if (mgroups[0].Title !== object.Title) {
            await query(`UPDATE ObjectBase SET 
                Title=?
                WHERE UID=?`,
                [object.Title, UID], { backDate: timestamp });
        }

        // Aktualisiere die Member-Tabelle mit den neuen Daten
        await query(`UPDATE Member SET 
            Display=?,SortName=?,FullTextIndex=?,Data=?
            WHERE UID=?`,
            [object.Display, object.SortIndex, object.FullTextIndex,
                JSON.stringify({ ...NewData, UID: undefined }), UID]);

        // Falls eine Schwestergruppe angegeben ist, aktualisiere die Verknüpfung
        if (req.query.belongsTo && req.query.belongsTo !== 'undefined') {
            object.sibling = await handleSisterLink(req, UID);
        }

        // Aktualisiere alle Jobs der Gruppe, da sich die Anzeige geändert haben könnte
        recreateJobsGroup(req, UID, timestamp);

        // Prüfe, ob das Banner geändert wurde und vererbe es ggf. weiter
        if (NewData.banner !== group.Data.banner) {
            recreateBanner(UID, NewData.banner, BannerRules[req.session.root], req);
        }

        // Entferne UID aus den Daten für den Vergleich/Event
        delete NewData.UID;

        // Vergleiche die alten und neuen Daten, ermittle Unterschiede
        const [equal, diff] = keysEqual(NewData, group.Data);

        // Wenn sich etwas geändert hat, veröffentliche ein Event
        if (!equal) {
            publishEvent(`/change/group/${req.session.root}/${HEX2uuid(UID)}`, {
                organization: req.session.root,
                data: { UID: HEX2uuid(UID), diff },
                backDate: timestamp
            });
            // wenn es die root gruppe ist, dann sende auch ein change für die organiation
            if(req.params.UID===req.session.root){
                const config= await getConfig('db',req.session.root);
                publishEvent(`/change/orga/${req.session.root}`, {
                    organization: req.session.root,
                    data: {Display: object.Display, OrgaShort: config.OrgaShort },
                    backDate: timestamp
                });

            }
        }

        // Prüfe, ob sich die Gebühren geändert haben und baue sie ggf. neu auf
        if (req.body.fees && (JSON.stringify(group.Data.fees) !== JSON.stringify(req.body.fees))) {
            rebuildFeesGroup(UID, req.session.root);
        }
        
        // ist das für organisations Route
        // enthält es extraConfig
        // ändere config und triggere clients
        if(req.params.UID===req.session.root && req.body.extraConfig){
            addExtraConfigToOrgaData(req.session.root, req.body.extraConfig);
        }
        // Trage die Änderung in die Update-Liste für die Synchronisation ein
        addUpdateEntry(UID, { group: { ...object, Data: NewData, UID: req.params.UID } });

        // Sende das Ergebnis zurück mit Status 201 (Created/Updated)
        res.status(201).json({ success: true, result: { ...object, UID: HEX2uuid(UID), Data: NewData } });
    } catch (e) {
        // Fehlerbehandlung: Fehler loggen
        errorLoggerUpdate(e);
    }
};

/**
 * Deletes a group (only if it has no members)
 * @param {ExpressRequestAuthorized} req - Express request object
 * @param {ExpressResponse} res - Express response object
 */
export const deleteGroup = async ( req, res) => {
    try {
        // Konvertiere die Gruppen-UID aus dem URL-Parameter in das interne Hex-Format
        const UID = UUID2hex(req.params.UID);

        // Prüfe, ob es noch Mitglieder oder Unterobjekte in der Gruppe gibt
        const hasLinks = await query(`
            SELECT ObjectBase.UID, 
                   Member.Display AS Display,
                   ObjectBase.Title
            FROM Links 
            INNER JOIN ObjectBase ON (ObjectBase.UID=Links.UID)
            INNER JOIN Member ON (Member.UID=ObjectBase.UIDbelongsTo)
            WHERE Links.UIDTarget=? 
              AND Links.UID<>?  -- sich selbst nicht zählen
              AND Links.Type IN ('member','memberA') 
              AND ObjectBase.Type IN ('person','extern','guest','ggroup','group','job')
        `, [UID, UID], { cast: ['UUID'] });

        // Wenn noch Mitglieder vorhanden sind, Abbruch und Info zurückgeben
        if (hasLinks.length) {
            res.status(300).json({ success: false, message: 'has still members', result: hasLinks });
            console.log('delete group not empty', hasLinks.map(l => (HEX2uuid(l.UID))));
        } else {
            // Lösche die Gruppe aus allen relevanten Tabellen
            query(`DELETE FROM ObjectBase WHERE UID =?`, [UID]);
            query(`DELETE FROM Member WHERE UID =?`, [UID]);

            // Hole alle Links (z.B. zur Eltern-Gruppe), um später Updates zu triggern
            const update = await query(`SELECT UIDTarget,Type FROM Links WHERE UID = ?  `, [UID]);
            const parentGroup = update.find(l => l.Type === 'memberA')?.UIDTarget;

            // Lösche alle Links und Sichtbarkeiten der Gruppe
            await query(`DELETE FROM Links WHERE UID=?`, [UID]);
            await query(`DELETE FROM Visible WHERE UID=?`, [UID]);

            // Informiere andere Systemteile über die Änderung (z.B. für UI-Updates)
            addUpdateList(update.map(el => el.UIDTarget));

            // Event veröffentlichen, dass die Gruppe entfernt wurde
            publishEvent(`/remove/group/group/${parentGroup}`, {
                organization: req.session.root,
                data: [req.params.UID]
            });

            // Erfolg zurückgeben
            res.json({ success: true });
        }
    } catch (e) {
        // Fehlerbehandlung: Fehler loggen
        errorLoggerUpdate(e);
    }
};

/**
 * Gets group details with optional parent and sibling information
 * @param {ExpressRequestAuthorized} req - Express request object
 * @param {ExpressResponse} res - Express response object
 */
export const getGroup = async ( req, res) => {
    try {
        const UID = UUID2hex(req.params.UID);
        if (!UID) {
            res.status(300).json({ success: false, message: 'no UID supplied' });
            return;
        }

        const resa = await query(`SELECT ObjectBase.UID,ObjectBase.UIDBelongsTo,ObjectBase.Type,ObjectBase.Title,
                                Member.Display AS Display,
                                ObjectBase.dindex,ObjectBase.gender, ObjectBase.hierarchie,  ObjectBase.stage,
                                Member.Data AS Data,UNIX_TIMESTAMP(ObjectBase.validFrom)  AS validFrom
                                FROM ObjectBase 
                                INNER JOIN Member ON (Member.UID=ObjectBase.UID)
                                WHERE ObjectBase.UID=? AND ObjectBase.Type IN('group','ggroup')`,
            [UID],
            { cast: ['UUID', 'json'] }
        );

        if (resa.length === 0) {
            res.json({ success: false, message: `group ${req.params.UID} does not exist` });
            return;
        }

        const result = resa[0];

        if (req.query.parent) {
            const p = await query(`SELECT ObjectBase.UID,ObjectBase.Type,ObjectBase.Title,Member.Display,
                ObjectBase.dindex,Member.Data, ObjectBase.gender, ObjectBase.hierarchie,
                 ObjectBase.stage, UNIX_TIMESTAMP(ObjectBase.validFrom) AS validFrom,
                ObjectBase.UIDBelongsTo ,UNIX_TIMESTAMP(Links.validFrom) AS LvalidFrom
                FROM ObjectBase
                INNER JOIN Member ON (Member.UID=ObjectBase.UID)
                INNER JOIN Links ON (Links.UIDTarget=ObjectBase.UID AND Links.Type='memberA')
                WHERE Links.UID=?`,
                [UID],
                { cast: ['UUID', 'json'] }
            );

            if (p.length > 0) {
                // entry with member and sibling of parent(s)
                const ps = await query(`
                        SELECT Siblings.UID,Siblings.Title,Member.Display, Siblings.dindex,Member.Data,Siblings.gender, Siblings.hierarchie,Siblings.stage,UNIX_TIMESTAMP(Siblings.validFrom) AS validFrom
                        FROM Links AS SLinks
                        INNER JOIN ObjectBase AS Siblings ON (Siblings.UID=SLinks.UIDTarget AND Siblings.Type='group')
                        INNER JOIN Member ON (Member.UID=Siblings.UID) 
                        INNER JOIN Links ON (Links.UIDTarget=Siblings.UID AND Links.Type='member')
                        WHERE Links.UID=? AND SLinks.UID IN (?) AND SLinks.Type='memberS';
                `,
                    [UID, p.map(pp => UUID2hex(pp.UID))],
                    { cast: ['UUID', 'json'], log: false }
                );

                if (p.length > 0) {
                    result.parent = p[0];
                }
                if (ps.length > 0) {
                    result.parentS = ps[0];
                }
            }
        }

        if (req.query.sibling) {
            const s = await query(`SELECT ObjectBase.UID,ObjectBase.Type,ObjectBase.Title,Member.Display AS Display,
            ObjectBase.dindex, Member.Data AS Data,ObjectBase.gender, ObjectBase.hierarchie,  ObjectBase.stage ,UNIX_TIMESTAMP(ObjectBase.validFrom)  AS validFrom
            FROM ObjectBase 
            INNER JOIN Member ON (Member.UID=ObjectBase.UID)
            INNER JOIN Links ON (Links.UIDTarget=ObjectBase.UID AND Links.Type='memberS')
            WHERE Links.UID=?  AND ObjectBase.Type ='group'`,
                [UID],
                { cast: ['UUID', 'json'], log: false }
            );
            result.sibling = s[0];
        }

        res.json({
            success: true,
            result: result,
        });
    } catch (e) {
        errorLoggerRead(e);
    }
};

/**
 * Gets the minimum backdate timestamp for a group
 * @param {ExpressRequestAuthorized} req - Express request object
 * @param {ExpressResponse} res - Express response object
 */
export const minTimestamp = async ( req, res) => {
    // Konvertiere die Gruppen-UID aus dem URL-Parameter in das interne Hex-Format
    const UID = UUID2hex(req.params.UID);
    // Ermittle das kleinste erlaubte Backdate-Datum für die Gruppe und ggf. den angegebenen Timestamp
    const minTimestampValue = await timestampCheck(UID, parseTimestampToSecondsOrDefault(req.query.timestamp, 0));
    // Sende das Ergebnis (in Millisekunden) zurück
    res.json({ success: true, result: minTimestampValue * 1000 });
};

/**
 * Checks if the current user is admin for the group
 * @param {ExpressRequestAuthorized} req - Express request object
 * @param {ExpressResponse} res - Express response object
 */
export const checkGroupAdmin = async ( req, res) => {
    try {
        // Prüfe Adminrechte für die angegebene Gruppe
        const admin = await isObjectAdmin(req, req.params.UID);

        // Sende das Ergebnis zurück
        res.json({ success: true, result: admin });
    } catch (e) {
        // Fehlerbehandlung: Fehler loggen
        errorLoggerRead(e);
    }
};