From a380a465ce02059a630ef678fecd6666cbdf8f30 Mon Sep 17 00:00:00 2001
From: alex <alex@alexloehr.net>
Date: Fri, 28 Nov 2025 10:21:00 +0000
Subject: [PATCH] GS-2333

---
 lib/db.js |  332 ++++++++++++++++++++++++++++++++++++++++++++++++------
 1 files changed, 293 insertions(+), 39 deletions(-)

diff --git a/lib/db.js b/lib/db.js
index 13c40a1..dd4dc76 100644
--- a/lib/db.js
+++ b/lib/db.js
@@ -1,6 +1,8 @@
 const mysql = require("mysql2/promise")
 const dayjs = require("dayjs")
+const _ = require("lodash")
 
+const log = require("../logger")
 const searchLib = require("./search")
 const {host, port, user, database, password} = require("./../settings").db
 
@@ -8,7 +10,7 @@
 
 let poolP = initPool()
 
-async function initPool () {
+async function initPool() {
    return mysql.createPool({
       host,
       port,
@@ -42,17 +44,27 @@
    getSingleKursTeilnehmer,
    getKursTeilnehmerCount,
 
+   getKursLp,
+   getKursUnterobjektLp,
+
+   getKursOffline,
+   setKursOffline,
+
    getUdf,
 
    getKursTeilnehmerRolle,
    getKursTeilnehmerByRole,
+   getKursRoles,
 
    setStatus,
+
+   getCourseAdminRoles,
+   getCourseWithoutAdminRoles,
 }
 
 /////////////////////////////////////////////////////////////////////////
 
-async function getUserSearchQuery (offset, limit, search) {
+async function getUserSearchQuery(offset, limit, search) {
    const sel = `usr_id, login, firstname, lastname, gender, email, institution, street, city, zipcode, country, department, active`
    if (!search || search === '') {
       return `SELECT ${sel}
@@ -61,8 +73,7 @@
               LIMIT ${limit}
               OFFSET ${offset}
       `
-   }
-   else {
+   } else {
       const ids = await searchLib.search(search)
       if (!ids.length) {
          throw "nothing found"
@@ -78,8 +89,8 @@
    }
 }
 
-async function getUsers (offset = 0, limit = 10, search = null) {
-   console.log("++++++++++ get users", offset, limit, search)
+async function getUsers(offset = 0, limit = 10, search = null) {
+   // log.info("++++++++++ get users", offset, limit, search)
    limit = Number(limit) || 10
    offset = Number(offset) || 0
    // TODO check args for SQL Injection
@@ -88,8 +99,9 @@
 
    try {
       let userSearchQuery = await getUserSearchQuery(offset, limit, search)
-      // console.log(userSearchQuery)
+      // log.info(userSearchQuery)
       const [results, fields] = await pool.query(userSearchQuery)
+      // console.log(results, fields)
       const count = await getUserCount(offset, limit, search)
       return {
          total: count,
@@ -106,7 +118,7 @@
    }
 }
 
-async function getUserCount (offset, limit, search) {
+async function getUserCount(offset, limit, search) {
    const pool = await poolP
    // const q = getUserSearchQuery(offset, limit, search)
    const q = await getUserSearchQuery(0, 1000000, search) // hier darf kein Limit sein, offset=0
@@ -123,7 +135,7 @@
    // return results[0]["COUNT(*)"]
 }
 
-async function getUserByLogin (login) {
+async function getUserByLogin(login) {
    const sel = `usr_id, login, firstname, lastname, gender, email, institution, street, city, zipcode, country, department, active`
    // TODO check args for SQL Injection
 
@@ -136,7 +148,7 @@
    return joinUDF(results[0])
 }
 
-async function getUserByUserId (userId) {
+async function getUserByUserId(userId) {
    const sel = `usr_id, login, firstname, lastname, gender, email, institution, street, city, zipcode, country, department, active`
    // TODO check args for SQL Injection
 
@@ -149,7 +161,7 @@
    return joinUDF(results[0])
 }
 
-async function getUserDefinedFields () {
+async function getUserDefinedFields() {
    const pool = await poolP
    const [results] = await pool.query(
       `SELECT ut.usr_id, ud.field_name, ut.value
@@ -159,7 +171,7 @@
    return results
 }
 
-async function getUserDefinedField (usr_id) {
+async function getUserDefinedField(usr_id) {
    const pool = await poolP
    const [results] = await pool.query(
       `SELECT ut.usr_id, ud.field_name, ut.value
@@ -177,7 +189,7 @@
  * @param refId
  * @returns {Promise<{ref_id,obj_id}|undefined>}
  */
-async function getObjIdFromRefId (refId) {
+async function getObjIdFromRefId(refId) {
    const pool = await poolP
    const [results] = await pool.query(
       `SELECT ref_id, obj_id
@@ -188,7 +200,7 @@
    return results.length ? results[0] : undefined
 }
 
-async function getRefIdFromObjId (objId) {
+async function getRefIdFromObjId(objId) {
    const pool = await poolP
    const [results] = await pool.query(
       `SELECT ref_id, obj_id
@@ -201,11 +213,11 @@
 
 /////////////////////////////////////////////////////////////////////////
 
-async function promiseDelay (ms) {
+async function promiseDelay(ms) {
    return new Promise(resolve => setTimeout(resolve, ms))
 }
 
-async function joinUDF (user) {
+async function joinUDF(user) {
    if (!user) return user
    const fields = await getUserDefinedField(user.usr_id)
    for (const field of fields) {
@@ -216,9 +228,9 @@
 
 /////// Kurs ////////////////////////////////////////////////////////////////
 
-async function getKurse () {
+async function getKurse() {
    const pool = await poolP
-   const q = `SELECT or2.ref_id, or2.obj_id, od.title, od.description, od.type
+   const q = `SELECT or2.ref_id, or2.obj_id, od.title, od.description, od.type, od.offline
               FROM ${database}.object_reference or2
                        INNER JOIN ${database}.object_data od ON od.obj_id = or2.obj_id
               WHERE od.type = 'crs'
@@ -233,19 +245,20 @@
 //    return getKursByObjId(obj_id)
 // }
 
-async function getKurs (ref_id) {
+async function getKurs(ref_id) {
    const pool = await poolP
-   const q = `SELECT or2.ref_id, or2.obj_id, od.title, od.description, od.type, od.create_date
+   const q = `SELECT or2.ref_id, or2.obj_id, od.title, od.description, od.type, od.create_date, od.offline
               FROM ${database}.object_reference or2
                        INNER JOIN ${database}.object_data od ON od.obj_id = or2.obj_id
               WHERE or2.ref_id = '${ref_id}'
+                AND or2.deleted IS NULL
    `
    let [results] = await pool.query(q)
    results = results.length ? results[0] : undefined
    return results
 }
 
-async function getKursItems (ref_id) {
+async function getKursItems(ref_id) {
    const pool = await poolP
    // const q = `SELECT or2.ref_id, or2.obj_id, od.title, od.description, od.type
    //            FROM ${database}.object_reference or2
@@ -264,7 +277,7 @@
    return results
 }
 
-async function getKursItems2 (ref_id) {
+async function getKursItems2(ref_id) {
    const pool = await poolP
    const q = `
        WITH RECURSIVE tree (parent_id, obj_id, ref_id, title, type) AS
@@ -299,7 +312,7 @@
    return results
 }
 
-async function getKursTeilnehmer (ref_id) {
+async function getKursTeilnehmer(ref_id) {
    const pool = await poolP
    const q = `SELECT ci.parent_id,
                      or2.ref_id,
@@ -321,14 +334,15 @@
                        INNER JOIN ${database}.usr_data ud ON ud.usr_id = om.usr_id
                        LEFT JOIN ${database}.ut_lp_marks ulm ON ulm.obj_id = or2.obj_id AND ud.usr_id = ulm.usr_id
               WHERE (or2.ref_id = ${ref_id} OR parent_id = ${ref_id})
+                AND or2.deleted IS NULL
               ORDER BY usr_id
    `
-   // console.log(q)
+   // log.info(q)
    const [results] = await pool.query(q)
    return results
 }
 
-async function getSingleKursTeilnehmer (ref_id, usr_id) {
+async function getSingleKursTeilnehmer(ref_id, usr_id) {
    const pool = await poolP
    const q = `SELECT ci.parent_id,
                      or2.ref_id,
@@ -354,12 +368,12 @@
                 AND om.usr_id = ${usr_id}
               ORDER BY usr_id
    `
-   // console.log(q)
+   // log.info(q)
    const [results] = await pool.query(q)
    return results[0]
 }
 
-async function getKursTeilnehmerCount (ref_id) {
+async function getKursTeilnehmerCount(ref_id) {
    const pool = await poolP
    const q = `SELECT COUNT(*) as count
               FROM ${database}.crs_items ci
@@ -378,7 +392,7 @@
    return {ref_id, count: results.count}
 }
 
-async function getUserTeilnahmen (usr_id) {
+async function getUserTeilnahmen(usr_id) {
    const pool = await poolP
    const q = `SELECT om.obj_id, or2.ref_id, om.usr_id, od.title, ulm.status, om.passed, ulm.status_changed
               FROM ${database}.obj_members om
@@ -390,14 +404,154 @@
               WHERE om.usr_id = ${usr_id}
                 AND om.member = 1
    `
-   console.log(q)
+   // log.info(q).catch(console.error)
    const [results] = await pool.query(q)
    return results
 }
 
+async function getKursLp(obj_id, raw = false) {
+   const {ref_id} = await getRefIdFromObjId(obj_id)
+   const tnUnter = await getKursUnterobjektLp(obj_id)
+
+   if (raw) {
+      return tnUnter
+   } else { // LP aller Unterobjekte zusammenfassen
+      const libLp = require("../lib/libLp")
+      const data = libLp.alleAuswerten(tnUnter)
+      return data
+   }
+
+   // const teilnehmer = await getKursTeilnehmer(ref_id)
+   // const tnUnter = await getKursUnterobjektLp(obj_id)
+   // console.table(teilnehmer)
+   // console.table(tnUnter)
+   //
+   // /**
+   //  * Die beiden Datensätze mergen
+   //  * NEIN - es reicht der
+   //  *
+   //  */
+   //
+   // const idx = _.groupBy(tnUnter, "usr_id")
+   // for (const tn of teilnehmer) {
+   //     delete tn.parent_id
+   //     delete tn.type
+   //     delete tn.active
+   //
+   //     /** @type Array */
+   //     const unter = idx[tn.usr_id]
+   //     if (!unter) continue
+   //
+   //     /**
+   //      * wenn unter.status_changed neuer, wird unter tn.status_changed vorgezogen
+   //      * wenn unter.status neuer, überscheibt es tn.status
+   //      * tn.passed muss zurückgesetzt werden wenn tn.status überschrieben wird
+   //      */
+   //     tn.status_overwrite = false
+   //
+   //     // max unter status_changed finden
+   //     const unterStatusChanged = _.max(unter.map(u => u.status_changed))
+   //
+   //     // es muss nur überschrieben werden wenn das Unterdatum größer ist
+   //     if (unterStatusChanged > tn.status_changed) {
+   //         // unter Status auswerten
+   //         // 0 = noch nicht bearbeitet
+   //         // 1 = in Bearbeitung
+   //         // 2 = bestanden
+   //         // 3 = nicht bestanden
+   //
+   //         /** @type Array */
+   //         const unterStatusse = unter.map(u => u.status)
+   //         let newStatus = tn.status
+   //         const allSame = function () {
+   //             if (!unterStatusse.length) return false
+   //             const first = unterStatusse[0]
+   //             return unterStatusse.every(it => it === first)
+   //         }()
+   //         console.log({unterStatusse})
+   //         // Fall 1: keine Unterstatussse vorhanden -> status vom Kurs
+   //         if (!unterStatusse.length) {
+   //             newStatus = tn.status
+   //         }
+   //         // Fall 2: eines nicht bestanden -> nicht bestanden // 0,1,2,3 -> 3
+   //         if (unterStatusse.some(u => u === 3)) {
+   //             newStatus = 3
+   //         }
+   //         // Fall 3: alle statusse gleich -> status // 0,0,0 1,1,1 2,2,2 3,3,3
+   //         else if (allSame) {
+   //             newStatus = unterStatusse[0]
+   //         }
+   //         // Fall 4: wenn eines in Bearbeitung -> in Bearbeitung // 0,1,0 2,1,2
+   //         else if (unterStatusse.some(u => u === 1)) {
+   //             newStatus = 1
+   //         }
+   //         // Fall 5: sonst in Bearbeitung
+   //         else {
+   //             // newStatus = Math.max.apply(this, unterStatusse)
+   //             newStatus = 1
+   //         }
+   //         tn.status_changed = unterStatusChanged
+   //         tn.status = newStatus
+   //         tn.status_overwrite = true
+   //     }
+   // }
+   // return teilnehmer
+}
+
+async function getKursUnterobjektLp(obj_id) {
+   const pool = await poolP
+   const q = `SELECT ud.usr_id,
+                     ud.login,
+                     ud.firstname,
+                     ud.lastname,
+                     ulc.obj_id,
+                     ulc.item_id,
+                     ulc.lpmode,
+                     t.obj_id as item_obj_id,
+                     od.type,
+                     ulm.status,
+                     ulm.status_changed,
+                     ulm.percentage,
+                     ulm.completed
+              FROM ${database}.ut_lp_collections ulc
+                       INNER JOIN ${database}.object_reference t ON t.ref_id = ulc.item_id
+                       INNER JOIN ${database}.ut_lp_marks ulm ON ulm.obj_id = t.obj_id
+                       INNER JOIN ${database}.usr_data ud ON ud.usr_id = ulm.usr_id
+                       INNER JOIN ${database}.object_data od ON od.obj_id = t.obj_id
+              WHERE ulc.obj_id = ${obj_id} # obj_id Kurs
+                    AND ulc.active = 1
+                    AND ulc.lpmode = 5  # nur mode 5
+              ORDER BY ud.usr_id, ulc.item_id
+   `
+   const [results] = await pool.query(q)
+   return results
+}
+
+
+async function getKursOffline(obj_id) {
+   const pool = await poolP
+   const q = `SELECT offline
+              FROM ${database}.object_data
+              WHERE obj_id = ${obj_id}
+   `
+   const [results] = await pool.query(q)
+   return results[0]
+}
+
+async function setKursOffline(isOffline, obj_id) {
+   const pool = await poolP
+   const q = `UPDATE ${database}.object_data
+              SET offline = ${isOffline}
+              WHERE obj_id = ${obj_id}
+   `
+   const [results] = await pool.query(q)
+   // return results
+   return {offline: isOffline}
+}
+
 /////// UDF ////////////////////////////////////////////////////////////////
 
-async function getUdf () {
+async function getUdf() {
    const pool = await poolP
    const q = `SELECT field_id, field_name, field_type
               from ${database}.udf_definition;`
@@ -407,7 +561,7 @@
 
 /////// ROLLEN ////////////////////////////////////////////////////////////////
 
-async function getKursTeilnehmerRolle (obj_id) {
+async function getKursTeilnehmerRolle(obj_id) {
    const pool = await poolP
    const q = `SELECT obj_id, title, description
               from object_data od
@@ -419,13 +573,12 @@
    if (results.length) {
       const {obj_id, title, description} = results[0]
       return obj_id
-   }
-   else {
+   } else {
       return null
    }
 }
 
-async function getKursTeilnehmerByRole (obj_id) {
+async function getKursTeilnehmerByRole(obj_id) {
    const pool = await poolP
    const q = `
        SELECT obj_id as role_id, ru.usr_id, ud.firstname, ud.lastname
@@ -439,6 +592,19 @@
    return results
 }
 
+async function getKursRoles(ref_id) {
+   const pool = await poolP
+   const q = `
+       SELECT pa.rol_id, or2.ref_id, or2.obj_id, od2.type, od2.title, od2.description
+       FROM ${database}.rbac_pa pa
+                INNER JOIN ${database}.object_reference or2 ON or2.ref_id = pa.ref_id
+                INNER JOIN ${database}.object_data od ON od.obj_id = or2.obj_id
+                INNER JOIN ${database}.object_data od2 ON od2.obj_id = pa.rol_id
+       WHERE pa.ref_id = ${ref_id}
+   `
+   const [results] = await pool.query(q)
+   return results
+}
 
 
 /////// STATUS ////////////////////////////////////////////////////////////////
@@ -456,12 +622,15 @@
  *
  * @param courseId
  * @param userId
- * @param passed
- * @param status
+ * @param {Number} passed
+ * @param {Number} status
  * @returns {Promise<{status: string}>}
  */
-async function setStatus (courseId, userId, passed = null, status = null) {
+async function setStatus(courseId, userId, passed = null, status = null) {
    const pool = await poolP
+   log.info(`------------setStatus----- courseId=${courseId}  userId=${userId}  passed=${passed}  status=${status}`)
+   passed = isNaN(Number(passed)) ? passed : Number(passed)
+   status = isNaN(Number(status)) ? status : Number(status)
 
    // ACHTUNG Transactions funktioneren so nicht, erst mal ohne machen...
    const q = `
@@ -500,11 +669,96 @@
 
    if (affectedRows1 && affectedRows2) {
       return {status: "ok"}
-   }
-   else {
+   } else {
       throw {
          status: "error",
          reason: {affectedRows1, affectedRows2}
       }
    }
 }
+
+
+/////// ADMINS ////////////////////////////////////////////////////////////////
+
+
+async function getCourseAdminRoles() {
+   const pool = await poolP
+   const q = `
+       SELECT od.obj_id as crs_obj_id, t.ref_id as crs_ref_id, od.title as crs_title, rf.rol_id, od2.title as role
+       FROM ${database}.object_data od
+                INNER JOIN ${database}.object_reference t ON t.obj_id = od.obj_id
+                INNER JOIN ${database}.rbac_fa rf ON rf.parent = t.ref_id
+                INNER JOIN ${database}.object_data od2 ON od2.obj_id = rf.rol_id
+       WHERE od.\`type\` = "crs"
+         AND od2.title LIKE "%admin%"
+   `
+   const [results] = await pool.query(q)
+   return results
+}
+
+async function getCourseWithoutAdminRoles() {
+   const pool = await poolP
+   const q = `
+       SELECT adminRoles.crs_obj_id, adminRoles.crs_ref_id, adminRoles.crs_title, adminRoles.rol_id, adminRoles.role
+       FROM (SELECT od.obj_id as crs_obj_id, t.ref_id as crs_ref_id, od.title as crs_title, rf.rol_id, od2.title as role
+             FROM ${database}.object_data od
+                      INNER JOIN ${database}.object_reference t ON t.obj_id = od.obj_id
+                      INNER JOIN ${database}.rbac_fa rf ON rf.parent = t.ref_id
+                      INNER JOIN ${database}.object_data od2 ON od2.obj_id = rf.rol_id
+             WHERE od.\`type\` = "crs"
+               AND od2.title LIKE "%admin%") adminRoles
+       WHERE adminRoles.rol_id NOT IN (SELECT ru2.rol_id
+                                       FROM ${database}.rbac_ua ru2)
+   `
+   const [results] = await pool.query(q)
+   return results
+}
+
+// !! wird nicht gebraucht - Admin Erkennung in ILIAS läuft anders ab - über die Rolle
+// async function getCourseAdmins() {
+//    const pool = await poolP
+//    const q = `SELECT om.obj_id as kurs_obj_id,
+//                      t.ref_id  as kurs_ref_id,
+//                      om.usr_id,
+//                      ud.login,
+//                      om.admin,
+//                      ud.firstname,
+//                      ud.lastname,
+//                      od2.title
+//               FROM ${database}.obj_members om
+//                        INNER JOIN ${database}.usr_data ud ON ud.usr_id = om.usr_id
+//                        INNER JOIN ${database}.object_data od2 ON od2.obj_id = om.obj_id
+//                        INNER JOIN ${database}.object_reference t ON t.obj_id = om.obj_id
+//               WHERE om.admin = 1
+//    `
+//    const [results] = await pool.query(q)
+//    return results
+// }
+//
+// /**
+//  * Liefert die Kurse ohne Admins
+//  * Admins hier definiert als Einträge in obj_members wo admin==1
+//  * Darüber hinaus gibt es offenbar noch einen anderen Mechanismus über die Rolle.
+//  * Denn ein Kurs ohne Admin (z.B. lokal Fliesenratgeber ref_id=88) hat in ILIAS
+//  * trotzdem einen Admin im Screen "Members".
+//  * Dort wird wohl über die Rolle zugeordnet.
+//  *
+//  * Die Frage ist wo der Fehler GS-2333 auftritt.
+//  * Bei obj_members oder bei fehlender Rolle.
+//  * @return {Promise<*>}
+//  */
+// async function getCoursesWithNoAdmins() {
+//    const pool = await poolP
+//    const q = `
+//        SELECT asdf.obj_id, t.ref_id, asdf.numTn, asdf.title
+//        FROM (SELECT om.obj_id, COUNT(*) as numTn, od.title, MAX(om.admin) as maxAdmin
+//              FROM ${database}.obj_members om
+//                       INNER JOIN ${database}.object_data od ON od.obj_id = om.obj_id
+//              GROUP by om.obj_id
+//              ORDER BY numTn DESC) asdf
+//                 INNER JOIN ${database}.object_reference t ON t.obj_id = asdf.obj_id
+//        WHERE asdf.maxAdmin = 0
+//    `
+//    const [results] = await pool.query(q)
+//    return results
+// }

--
Gitblit v1.8.0