Source: RouterEvents/eventJobTemplate/service.js

/**
 * Event Job Template Service Layer
 * 
 * Manages job templates for events. Job templates define roles/functions
 * that can be assigned to people in events, including qualifications required,
 * permissions, and display settings.
 * 
 * Key concepts:
 * - Job Templates: Reusable role definitions for events
 * - Qualifications: Achievement requirements for job assignments
 * - Template hierarchy: Templates belong to event templates
 * 
 * @module EventJobTemplateService
 */

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

import { query, UUID2hex, HEX2uuid, pool } from '@commtool/sql-query';
import { renderObject } from '../../utils/renderTemplates.js';
import { Templates } from '../../utils/compileTemplates.js';
import { getUID } from '../../utils/UUIDs.js';
import { queueAdd } from '../../tree/treeQueue/treeQueue.js';
import { updateTemplate } from '../../server.ws.js';

/**
 * Extract achievement UIDs recursively from qualification filter object
 * 
 * @param {Object|Array|string} filter - Qualification filter object
 * @returns {Array<string>} Array of achievement UIDs
 */
export const extractAchievements = (filter) => {
    const qualies = [];
    
    const logic = (filters) => {
        if (!Array.isArray(filters)) {
            match(filters);
        } else {
            for (const filter of filters) {
                match(filter);
            }
        }
        return true;
    };

    const match = (filter) => {
        if (!filter) {
            return;
        }
        if (typeof filter === 'string') {
            qualies.push(filter);
            return;
        }
        const [func] = Object.keys(filter);
        logic(filter[func]);
    };

    if (filter) {
        try {
            match(filter);
        } catch (e) {
            console.log({ error: e, filter: filter });
        }
    }
    return qualies;
};

/**
 * Create or update a job template
 * 
 * @param {Buffer} UIDeventTemplate - Event template UID
 * @param {Object} templateData - Template data from request body
 * @param {string} rootUID - Organization root UID
 * @param {string} userUID - User UID
 * @param {Object} req - Request object for renderObject
 * @param {boolean} requalify - Whether to requalify existing jobs
 * @returns {Promise<Object>} Created/updated template object
 * @throws {Error} When event template is invalid
 */
export const createOrUpdateJobTemplate = async (
    UIDeventTemplate,
    templateData,
    rootUID,
    userUID,
    req,
    requalify = false
) => {
    const result = await query(
        `SELECT UID, Data FROM ObjectBase WHERE UID=? AND Type='eventT'`,
        [UIDeventTemplate]
    );

    if (!result || result.length === 0) {
        throw new Error('invalid event Template UID');
    }

    const eventTemplateData = JSON.parse(result[0].Data);
    const template = Templates[rootUID].eventJobTemplate;
    const UIDfunction = await getUID(req);
    
    const object = await renderObject(
        template,
        { ...templateData, template: eventTemplateData },
        req,
        'events'
    );

    object.Data = {
        ...templateData,
        hierarchie: eventTemplateData.hierarchie,
        stage: eventTemplateData.stage,
        gender: eventTemplateData.gender,
        UID: undefined
    };

    const functions = await query(
        `SELECT Data FROM ObjectBase WHERE UID=? AND Type='eventJobT'`,
        [UIDfunction]
    );

    await query(
        `INSERT INTO ObjectBase(UID, Type, UIDBelongsTo, Title, Display, SortName, FullTextIndex, Data)
         VALUES (?, 'eventJobT', ?, ?, ?, ?, ?, ?)
         ON DUPLICATE KEY UPDATE 
            UIDBelongsTo=VALUE(UIDBelongsTo), Title=VALUE(Title), Display=VALUE(Display),
            SortName=VALUE(SortName), FullTextIndex=VALUE(FullTextIndex), dindex=VALUE(dindex), Data=VALUE(Data)`,
        [
            object.UID,
            UIDeventTemplate,
            object.Title,
            object.Display,
            object.SortIndex,
            object.FullTextIndex,
            JSON.stringify(object.Data)
        ]
    );

    // Handle modifications to existing function
    if (functions.length > 0) {
        // Delete old qualification links
        await query(
            `DELETE FROM Links WHERE Type='achievement' AND UID=?`,
            [UIDfunction]
        );

        if (requalify) {
            // Add requalification job to queue
            queueAdd(
                UUID2hex(rootUID),
                UUID2hex(userUID),
                'function',
                object.UID,
                UUID2hex(rootUID),
                null,
                UUID2hex(rootUID)
            );
        }
    }

    // Create qualification links
    const qualies = extractAchievements(templateData.qualification);
    if (qualies.length > 0) {
        await query(
            `INSERT IGNORE INTO Links(UID, Type, UIDTarget) VALUES (?, 'achievement', ?)`,
            qualies.map(q => [UIDfunction, UUID2hex(q)]),
            { batch: true }
        );
    }

    return { ...object, UID: HEX2uuid(object.UID) };
};

/**
 * Delete a job template
 * 
 * @param {Buffer} UID - Template UID to delete
 * @param {string} rootUID - Organization root UID
 * @returns {Promise<boolean>} Success status
 */
export const deleteJobTemplate = async (UID, rootUID) => {
    await query(
        `DELETE FROM ObjectBase WHERE UID=? AND Type='eventJobT'`,
        [UID]
    );

    // Delete qualification links
    await query(
        `DELETE FROM Links WHERE Type='achievement' AND UIDTarget=?`,
        [UID]
    );

    updateTemplate(rootUID);
    return true;
};

/**
 * Get all jobs for a function template
 * 
 * @param {Buffer} UID - Function template UID
 * @param {Buffer} UIDUser - User UID for visibility check
 * @param {Object} options - Query options
 * @param {string} [options.timestamp] - Historical timestamp
 * @param {Array} [options.dataFields] - Additional data fields to retrieve
 * @param {boolean} [options.groupBanner] - Include group banner
 * @returns {Promise<Array>} Array of job objects
 */
export const getEventJobs = async (UID, UIDUser, options = {}) => {
    const time = options.timestamp
        ? `FOR SYSTEM_TIME AS OF TIMESTAMP'${options.timestamp}'`
        : '';

    let dataFields = '';
    
    if (options.dataFields && Array.isArray(options.dataFields)) {
        for (const field of options.dataFields) {
            dataFields += `,JSON_VALUE(Member.Data,${pool.escape(field.path)}) AS ${pool.escape(field.alias)}`;
        }
    }

    if (options.groupBanner) {
        dataFields += `,JSON_VALUE(pgroup.Data,'$.banner') AS groupBanner`;
    }

    const result = await query(
        `SELECT
            Job.UID, Job.Type, Job.UIDBelongsTo, Job.Title, Member.Display, Member.SortName,
            pgroup.UID AS UIDgroup, 
            CONCAT(pgroup.Title, ' ', pgroup.Display) AS pGroup,
            Job.hierarchie, Job.stage, Job.gender, Job.dindex
            ${dataFields}
         FROM ObjectBase ${time} AS Job
         INNER JOIN Member ON (Member.UID=Job.UIDBelongsTo)
         LEFT JOIN (ObjectBase AS pgroup 
             INNER JOIN Links AS GLink ON (GLink.UIDTarget = pgroup.UID)
         )
         ON (GLink.UID=Job.UID AND GLink.Type='memberA') 
         INNER JOIN Links ON (Links.UIDTarget=Job.UID AND Links.Type='function')
         INNER JOIN Visible ON (Visible.UID=Job.UID)
         WHERE Links.UID=? AND Job.Type='eventJob' AND Visible.UIDUser=?
         GROUP BY Job.UID
         ORDER BY Job.SortName, Member.SortName`,
        [UID, UIDUser]
    );

    return result;
};

/**
 * Get all function template names and UIDs for an event template
 * 
 * @param {Buffer} UID - Event template UID
 * @returns {Promise<Array>} Array of template objects with UID, Display, SortName
 */
export const getJobTemplates = async (UID) => {
    const result = await query(
        `SELECT Main.UID, Main.Display, Main.SortName 
         FROM ObjectBase AS Main
         WHERE Main.UIDBelongsTo=? AND Main.Type='eventJobT'
         GROUP BY Main.UID
         ORDER BY Main.SortName`,
        [UID],
        { cast: ['UUID'] }
    );

    return result;
};

/**
 * Get all function template data for an event template
 * 
 * @param {Buffer} UID - Event template UID
 * @returns {Promise<Array>} Array of template objects with full data
 */
export const getJobTemplatesWithData = async (UID) => {
    const result = await query(
        `SELECT Main.UID, Main.Display, Main.SortName, Main.Data
         FROM ObjectBase AS Main
         WHERE Main.UIDBelongsTo=? AND Main.Type='eventJobT'
         GROUP BY Main.UID
         ORDER BY Main.SortName`,
        [UID],
        { cast: ['UUID', 'json'] }
    );

    return result;
};