Source: Router/group.js

// @ts-check
/**
 * @import {ExpressRequestAuthorized, ExpressResponse} from '../types.js'
 */

/**
 * Group Management Router
 *
 * Provides CRUD endpoints for managing hierarchical group structures
 * within an organisation.  Groups form the backbone of the member
 * management system, supporting multi-level hierarchies with optional
 * gender and age-stage restrictions.
 *
 * Key rules:
 * - `hierarchie`, `stage`, and `gender` are immutable once a group is created.
 * - Banner inheritance propagates recursively down the child tree.
 * - Sister-group links (`memberS`) are managed automatically via `?belongsTo`.
 * - Groups with remaining members cannot be deleted.
 *
 * @swagger
 * tags:
 *   - name: Groups
 *     description: |
 *       Hierarchical group management within an organisation.
 *
 *       **Gender values**
 *       | Code | Meaning |
 *       |------|---------|
 *       | `M`  | Male    |
 *       | `F`  | Female  |
 *       | `B`  | Mixed (both) |
 *       | `C`  | Combined (virtual cross-gender view) |
 *
 * components:
 *   schemas:
 *     GroupData:
 *       type: object
 *       description: The JSON `Data` blob stored in the `Member` table for a group.
 *       properties:
 *         name:
 *           type: string
 *           description: Human-readable group name
 *           example: "Junior Team A"
 *         gender:
 *           type: string
 *           enum: [M, F, B, C]
 *           description: Gender restriction for membership
 *           example: M
 *         stage:
 *           type: string
 *           description: Age-stage restriction (org-specific values, e.g. "Junioren")
 *           example: Junioren
 *         hierarchie:
 *           type: string
 *           description: Organisation hierarchy level (org-specific, e.g. "Verein", "Abteilung")
 *           example: Abteilung
 *         location:
 *           type: string
 *           description: Geographic location label
 *           example: Sporthalle West
 *         banner:
 *           type: string
 *           description: URL or path of the group banner image
 *           example: "https://cdn.example.com/banners/jugend.jpg"
 *         fees:
 *           type: array
 *           description: Fee configuration for this group
 *           items:
 *             type: object
 *         accounts:
 *           type: array
 *           description: Account configuration (protected for non-account-managers)
 *           items:
 *             type: object
 *
 *     GroupObject:
 *       type: object
 *       description: Full group record as returned by GET /group/:UID
 *       properties:
 *         UID:
 *           type: string
 *           format: uuid
 *           example: "aefe03ad-3d74-11f0-b612-b2b0f54f4439"
 *         UIDBelongsTo:
 *           type: string
 *           format: uuid
 *           description: UID of the root/organisation object this group belongs to
 *         Type:
 *           type: string
 *           enum: [group, ggroup]
 *         Title:
 *           type: string
 *           description: Rendered display title
 *         Display:
 *           type: string
 *           description: Short display name from the Member table
 *         gender:
 *           type: string
 *           enum: [M, F, B, C]
 *         hierarchie:
 *           type: string
 *         stage:
 *           type: string
 *         dindex:
 *           type: integer
 *           description: Sort index within its parent
 *         validFrom:
 *           type: number
 *           description: Unix timestamp (seconds) when the group became active
 *         Data:
 *           $ref: '#/components/schemas/GroupData'
 *         parent:
 *           $ref: '#/components/schemas/GroupObject'
 *           description: Present only when ?parent=true is requested
 *         parentS:
 *           $ref: '#/components/schemas/GroupObject'
 *           description: Sister group of the parent; present only when ?parent=true
 *         sibling:
 *           $ref: '#/components/schemas/GroupObject'
 *           description: Present only when ?sibling=true is requested
 *
 *     GroupUpsertBody:
 *       type: object
 *       required:
 *         - UID
 *         - name
 *         - gender
 *         - stage
 *         - hierarchie
 *       properties:
 *         UID:
 *           type: string
 *           format: uuid
 *           description: |
 *             Client-generated UID for the group.
 *             If this UID already exists the request becomes an update.
 *           example: "3fa85f64-5717-4562-b3fc-2c963f66afa6"
 *         name:
 *           type: string
 *           example: "Junior Team A"
 *         gender:
 *           type: string
 *           enum: [M, F, B, C]
 *           example: M
 *         stage:
 *           type: string
 *           example: Junioren
 *         hierarchie:
 *           type: string
 *           example: Abteilung
 *         location:
 *           type: string
 *           example: Sporthalle West
 *         banner:
 *           type: string
 *           example: "https://cdn.example.com/banners/jugend.jpg"
 *         fees:
 *           type: array
 *           items:
 *             type: object
 *         accounts:
 *           type: array
 *           items:
 *             type: object
 *
 *     GroupUpdateBody:
 *       type: object
 *       description: |
 *         Partial update payload — only supplied fields are changed.
 *         `hierarchie`, `stage`, and `gender` are immutable and will be rejected
 *         if they differ from the stored values.
 *       properties:
 *         name:
 *           type: string
 *         location:
 *           type: string
 *         banner:
 *           type: string
 *         fees:
 *           type: array
 *           items:
 *             type: object
 *         accounts:
 *           type: array
 *           items:
 *             type: object
 *         extraConfig:
 *           type: object
 *           description: Extra organisation-level config (only applied when UID === root UID)
 *
 *     GroupMemberSummary:
 *       type: object
 *       description: Compact member info returned when a delete is blocked by remaining members
 *       properties:
 *         UID:
 *           type: string
 *           format: uuid
 *         Display:
 *           type: string
 *         Title:
 *           type: string
 *
 *     GroupTreeResult:
 *       type: object
 *       properties:
 *         krokiUrl:
 *           type: string
 *           description: Complete Kroki render URL for the diagram
 *         encoded:
 *           type: string
 *           description: Base64-encoded Graphviz payload
 *         renderedContent:
 *           type: string
 *           description: Raw rendered diagram bytes (when redirect=true)
 *         nodeCount:
 *           type: integer
 *           description: Number of group nodes in the diagram
 *         edgeCount:
 *           type: integer
 *           description: Number of edges (links) in the diagram
 */

import express from 'express';
import { checkAdmin, checkObjectAdmin, checkVisible } from '../utils/authChecks.js';
import { requestUpdateLogger, readLogger } from '../utils/requestLogger.js';
import { validateSingleUID, validateUUID } from '../utils/uuidValidation.js';
import * as groupController from './group/controller.js';

/** @type {express.Express} */
const api = express();

// ---------------------------------------------------------------------------
// Mutation routes
// ---------------------------------------------------------------------------

/**
 * @swagger
 * /api/kpe20/group/{UIDparent}:
 *   put:
 *     summary: Create or update a group
 *     description: |
 *       Inserts a new group as a direct child of `UIDparent`, or updates the
 *       existing group identified by `body.UID` if it already exists.
 *
 *       **Creation rules**
 *       - `gender` must be compatible with the parent group's gender setting.
 *       - `stage` must match the parent's stage (unless the parent has stage 0).
 *       - Banner is inherited from the parent according to the org's banner rules.
 *
 *       **Update rules (group already exists)**
 *       - `hierarchie`, `stage`, and `gender` cannot be changed after creation.
 *       - Group migration (moving to a different parent) is not yet supported.
 *
 *       Requires **object-admin** rights on the parent group.
 *     tags:
 *       - Groups
 *     security:
 *       - bearerAuth: []
 *     parameters:
 *       - in: path
 *         name: UIDparent
 *         required: true
 *         schema:
 *           type: string
 *           format: uuid
 *         description: UUID of the parent group
 *       - in: query
 *         name: timestamp
 *         required: false
 *         schema:
 *           type: string
 *         description: |
 *           Backdate timestamp for the creation (ISO 8601 or Unix ms).
 *           Clamped to the minimum allowed backdate of the parent group.
 *       - in: query
 *         name: belongsTo
 *         required: false
 *         schema:
 *           type: string
 *           format: uuid
 *         description: UUID of a sister group to link via `memberS`.
 *     requestBody:
 *       required: true
 *       content:
 *         application/json:
 *           schema:
 *             $ref: '#/components/schemas/GroupUpsertBody'
 *     responses:
 *       200:
 *         description: Group created or updated successfully
 *         content:
 *           application/json:
 *             schema:
 *               type: object
 *               properties:
 *                 success:
 *                   type: boolean
 *                   example: true
 *                 result:
 *                   $ref: '#/components/schemas/GroupObject'
 *       300:
 *         description: Validation error (invalid parent UID, gender mismatch, stage mismatch, invalid UID format)
 *         content:
 *           application/json:
 *             schema:
 *               type: object
 *               properties:
 *                 success:
 *                   type: boolean
 *                   example: false
 *                 message:
 *                   type: string
 *       403:
 *         description: User does not have admin rights on the parent group
 */
// @ts-ignore
api.put('/:UIDparent', validateUUID(['UIDparent']), requestUpdateLogger, checkObjectAdmin, groupController.putGroup);

/**
 * @swagger
 * /api/kpe20/group/{UID}:
 *   post:
 *     summary: Partially update an existing group
 *     description: |
 *       Merges the request body over the existing group data.
 *       Only the supplied fields are updated — absent fields are kept as-is.
 *
 *       **Immutable fields** — rejected with an error if they differ from stored values:
 *       `hierarchie`, `stage`, `gender`.
 *
 *       If `banner` changes, the new banner is propagated recursively to all
 *       child groups and members that inherit banners.
 *
 *       When the group UID equals the organisation root UID and `extraConfig`
 *       is present, the organisation-level extra config is also updated.
 *
 *       Requires **object-admin** rights on the group.
 *     tags:
 *       - Groups
 *     security:
 *       - bearerAuth: []
 *     parameters:
 *       - in: path
 *         name: UID
 *         required: true
 *         schema:
 *           type: string
 *           format: uuid
 *         description: UUID of the group to update
 *       - in: query
 *         name: timestamp
 *         required: false
 *         schema:
 *           type: string
 *         description: Backdate timestamp for the update (ISO 8601 or Unix ms).
 *       - in: query
 *         name: belongsTo
 *         required: false
 *         schema:
 *           type: string
 *           format: uuid
 *         description: UUID of a sister group to link/update via `memberS`.
 *     requestBody:
 *       required: true
 *       content:
 *         application/json:
 *           schema:
 *             $ref: '#/components/schemas/GroupUpdateBody'
 *     responses:
 *       201:
 *         description: Group updated successfully
 *         content:
 *           application/json:
 *             schema:
 *               type: object
 *               properties:
 *                 success:
 *                   type: boolean
 *                   example: true
 *                 result:
 *                   $ref: '#/components/schemas/GroupObject'
 *       300:
 *         description: Group not found or immutable-field change attempted
 *         content:
 *           application/json:
 *             schema:
 *               type: object
 *               properties:
 *                 success:
 *                   type: boolean
 *                   example: false
 *                 message:
 *                   type: string
 */
// @ts-ignore
api.post('/:UID', validateSingleUID, requestUpdateLogger, checkObjectAdmin, groupController.updateGroup);

/**
 * @swagger
 * /api/kpe20/group/{UID}:
 *   delete:
 *     summary: Delete a group
 *     description: |
 *       Permanently deletes the group identified by `UID` along with all its
 *       Links and Visible records.
 *
 *       **Blocked** — returns 300 with the list of remaining members when the
 *       group still has direct members (`person`, `extern`, `guest`, `ggroup`,
 *       `group`, or `job` objects linked via `member` or `memberA`).
 *
 *       Fires a `/remove/group/group/{parentUID}` event on success.
 *
 *       Requires **object-admin** rights on the group.
 *     tags:
 *       - Groups
 *     security:
 *       - bearerAuth: []
 *     parameters:
 *       - in: path
 *         name: UID
 *         required: true
 *         schema:
 *           type: string
 *           format: uuid
 *         description: UUID of the group to delete
 *     responses:
 *       200:
 *         description: Group deleted successfully
 *         content:
 *           application/json:
 *             schema:
 *               type: object
 *               properties:
 *                 success:
 *                   type: boolean
 *                   example: true
 *       300:
 *         description: Group still has members — deletion blocked
 *         content:
 *           application/json:
 *             schema:
 *               type: object
 *               properties:
 *                 success:
 *                   type: boolean
 *                   example: false
 *                 message:
 *                   type: string
 *                   example: has still members
 *                 result:
 *                   type: array
 *                   items:
 *                     $ref: '#/components/schemas/GroupMemberSummary'
 */
// @ts-ignore
api.delete('/:UID', validateSingleUID, requestUpdateLogger, checkObjectAdmin, groupController.deleteGroup);

// ---------------------------------------------------------------------------
// Read routes
// ---------------------------------------------------------------------------

/**
 * @swagger
 * /api/kpe20/group/tree/{UID}:
 *   get:
 *     summary: Get group tree graph (Kroki/Graphviz)
 *     description: |
 *       Builds a Graphviz diagram of the group hierarchy anchored at `UID`
 *       using `memberA` (parent-child) and `memberS` (sister) links and
 *       renders it through the internal Kroki proxy.
 *
 *       **Rendering modes**
 *       - `redirect=true` (default): fetches the diagram from Kroki and serves
 *         the raw image bytes with the correct `Content-Type`.
 *       - `redirect=false`: returns a JSON object with the public Kroki URL and
 *         the encoded Graphviz payload.
 *
 *       **React integration** — rendered nodes include `href` attributes usable
 *       for click-navigation (see `REACT-KROKI-GROUP-TREE.md`).
 *     tags:
 *       - Groups
 *     security:
 *       - bearerAuth: []
 *     parameters:
 *       - in: path
 *         name: UID
 *         required: true
 *         schema:
 *           type: string
 *           format: uuid
 *         description: UUID of the root group for the diagram
 *       - in: query
 *         name: displayLevels
 *         required: false
 *         schema:
 *           type: integer
 *           default: 6
 *         description: Number of visible hierarchy levels from the top (alias `levels`)
 *       - in: query
 *         name: includeOutOfHierarchy
 *         required: false
 *         schema:
 *           type: boolean
 *           default: false
 *         description: |
 *           Include out-of-hierarchy groups, keeping them visible regardless of
 *           `displayLevels`. Aliases: `includeOutOfHierarchie`, `outOfHierarchy`, `outOfHierarchie`
 *       - in: query
 *         name: type
 *         required: false
 *         schema:
 *           type: string
 *           enum: [svg, png, pdf]
 *           default: svg
 *         description: Kroki output format
 *       - in: query
 *         name: redirect
 *         required: false
 *         schema:
 *           type: boolean
 *           default: false
 *         description: |
 *           When true, serves the rendered image directly.
 *           When false, returns JSON with the Kroki URL.
 *     responses:
 *       200:
 *         description: Diagram rendered or URL returned
 *         content:
 *           application/json:
 *             schema:
 *               type: object
 *               properties:
 *                 success:
 *                   type: boolean
 *                   example: true
 *                 result:
 *                   $ref: '#/components/schemas/GroupTreeResult'
 *           image/svg+xml:
 *             schema:
 *               type: string
 *               format: binary
 *           image/png:
 *             schema:
 *               type: string
 *               format: binary
 */
// @ts-ignore
api.get('/tree/:UID', validateSingleUID, checkVisible, readLogger, groupController.getGroupTreeGraph);

/**
 * @swagger
 * /api/kpe20/group/admin/{UID}:
 *   get:
 *     summary: Check admin rights for a group
 *     description: |
 *       Returns whether the currently authenticated user has object-admin
 *       privileges for the specified group.
 *
 *       Useful for conditionally rendering edit controls in the UI without
 *       sending a full mutation request first.
 *     tags:
 *       - Groups
 *     security:
 *       - bearerAuth: []
 *     parameters:
 *       - in: path
 *         name: UID
 *         required: true
 *         schema:
 *           type: string
 *           format: uuid
 *         description: UUID of the group to check
 *     responses:
 *       200:
 *         description: Admin check result
 *         content:
 *           application/json:
 *             schema:
 *               type: object
 *               properties:
 *                 success:
 *                   type: boolean
 *                   example: true
 *                 result:
 *                   type: boolean
 *                   description: True if the user has admin rights for this group
 *                   example: true
 */
// @ts-ignore
api.get('/admin/:UID', validateSingleUID, checkVisible, groupController.checkGroupAdmin);

/**
 * @swagger
 * /api/kpe20/group/{UID}:
 *   get:
 *     summary: Get group details
 *     description: |
 *       Retrieves the full record for a group including its `Data` JSON blob.
 *
 *       Optional expansions via query parameters:
 *       - `?parent=true` — includes the direct parent group and its sister group (`parentS`).
 *       - `?sibling=true` — includes the sister group (`memberS` link) of this group.
 *
 *       Requires **visible** access to the group.
 *     tags:
 *       - Groups
 *     security:
 *       - bearerAuth: []
 *     parameters:
 *       - in: path
 *         name: UID
 *         required: true
 *         schema:
 *           type: string
 *           format: uuid
 *         description: UUID of the group to retrieve
 *       - in: query
 *         name: parent
 *         required: false
 *         schema:
 *           type: boolean
 *         description: Set to `true` to include the parent group object
 *       - in: query
 *         name: sibling
 *         required: false
 *         schema:
 *           type: boolean
 *         description: Set to `true` to include the sister group object
 *     responses:
 *       200:
 *         description: Group details
 *         content:
 *           application/json:
 *             schema:
 *               type: object
 *               properties:
 *                 success:
 *                   type: boolean
 *                   example: true
 *                 result:
 *                   $ref: '#/components/schemas/GroupObject'
 *       200-not-found:
 *         description: Group not found (success=false, no HTTP error code)
 *         content:
 *           application/json:
 *             schema:
 *               type: object
 *               properties:
 *                 success:
 *                   type: boolean
 *                   example: false
 *                 message:
 *                   type: string
 *                   example: "group aefe03ad-… does not exist"
 */
// @ts-ignore
api.get('/:UID', validateSingleUID, checkVisible, readLogger, groupController.getGroup);

export default api;