From c765c3c40a7159e0399c2bafc3ca0dabafd6311e Mon Sep 17 00:00:00 2001 From: chfhtw Date: Mon, 11 May 2026 09:20:42 +0200 Subject: [PATCH 01/42] allow object as argument for drop directive to enable dynamic specification of allowed dragtypes --- public/js/directives/drop.js | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/public/js/directives/drop.js b/public/js/directives/drop.js index 4c50e1b8e..f3c1889af 100644 --- a/public/js/directives/drop.js +++ b/public/js/directives/drop.js @@ -9,6 +9,26 @@ const EFFECTS = [ export default { mounted(el, binding) { + if (!binding.arg) { + binding.arg = 'none'; + } else if (typeof binding.arg === 'object' && !Array.isArray(binding.arg)) { + // NOTE(chris): allow object as arg and map it to arg and + // modifiers to allow dynamic modifiers. + if (binding.arg.allowed) { + binding.modifiers = binding.arg.allowed.reduce((a, c) => { + a[c] = true; + return a; + }, {}); + } + if (!binding.arg.effect) + binding.arg.effect = 'none'; + + if (binding.arg.strict) + binding.arg = binding.arg.effect + '-strict'; + else + binding.arg = binding.arg.effect; + } + const allowedTypes = Object.keys(binding.modifiers); allowedTypes.forEach(type => { if (type.substr(-11) == '-collection') { From e91f8e5ca4e6fada3ada5917b78814955bf89445 Mon Sep 17 00:00:00 2001 From: chfhtw Date: Mon, 11 May 2026 09:27:54 +0200 Subject: [PATCH 02/42] Replace component and endpoint for Verband --- application/config/menubuilder.php | 22 ++ .../controllers/api/frontend/v1/Menu.php | 58 +++ public/js/api/factory/menu.js | 41 ++ public/js/components/Base/Menu.js | 362 ++++++++++++++++++ public/js/components/Base/Menu/Entry.js | 50 +++ .../js/components/Stv/Studentenverwaltung.js | 4 +- .../Stv/Studentenverwaltung/Verband.js | 358 +---------------- 7 files changed, 555 insertions(+), 340 deletions(-) create mode 100644 application/config/menubuilder.php create mode 100644 application/controllers/api/frontend/v1/Menu.php create mode 100644 public/js/api/factory/menu.js create mode 100644 public/js/components/Base/Menu.js create mode 100644 public/js/components/Base/Menu/Entry.js diff --git a/application/config/menubuilder.php b/application/config/menubuilder.php new file mode 100644 index 000000000..64337a691 --- /dev/null +++ b/application/config/menubuilder.php @@ -0,0 +1,22 @@ +. + */ + +if (!defined('BASEPATH')) exit('No direct script access allowed'); + +$config['stv'] = "menu/StvMenuLib"; diff --git a/application/controllers/api/frontend/v1/Menu.php b/application/controllers/api/frontend/v1/Menu.php new file mode 100644 index 000000000..5ad97f403 --- /dev/null +++ b/application/controllers/api/frontend/v1/Menu.php @@ -0,0 +1,58 @@ +. + */ + +if (! defined('BASEPATH')) exit('No direct script access allowed'); + +/** + * This controller operates between (interface) the JS (GUI) and the back-end + * Provides data to the ajax get calls about menues + * This controller works with JSON calls on the HTTP GET or POST and the output is always JSON + */ +class Menu extends FHCAPI_Controller +{ + public function __construct() + { + $permissions = []; + $router = load_class('Router'); + // TODO(chris): permission + $permissions[$router->method] = ['admin:r', 'assistenz:r']; + parent::__construct($permissions); + + // Load Config + $this->config->load('menubuilder'); + } + + /** + * @param string $method + * @param array $params (optional) + * + * @return void + */ + public function _remap($method, $params = []) + { + $this->load->library($this->config->item($method), null, 'menulib'); + + if (!$this->menulib) + show_404(); + + $submenu = $this->menulib->build($params); + + $this->terminateWithSuccess($submenu); + } +} diff --git a/public/js/api/factory/menu.js b/public/js/api/factory/menu.js new file mode 100644 index 000000000..d8b1b8635 --- /dev/null +++ b/public/js/api/factory/menu.js @@ -0,0 +1,41 @@ +/** + * Copyright (C) 2026 fhcomplete.org + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +export default { + get(config, path = '') { + return { + method: 'get', + url: '/api/frontend/v1/menu/' + config + '/' + path + }; + }, + // TODO(chris): handle favorites per config + favorites: { + get() { + return { + method: 'get', + url: 'api/frontend/v1/stv/favorites' + }; + }, + set(favorites) { + return { + method: 'post', + url: 'api/frontend/v1/stv/favorites/set', + params: { favorites } + }; + } + } +}; \ No newline at end of file diff --git a/public/js/components/Base/Menu.js b/public/js/components/Base/Menu.js new file mode 100644 index 000000000..64f1bc4c9 --- /dev/null +++ b/public/js/components/Base/Menu.js @@ -0,0 +1,362 @@ +import MenuEntry from './Menu/Entry.js'; + +import dragClick from '../../directives/dragClick.js'; + +import ApiMenu from '../../api/factory/menu.js'; + +export default { + components: { + PvTreetable: primevue.treetable, + PvColumn: primevue.column, + MenuEntry + }, + directives: { + dragClick + }, + emits: [ + 'selectEntry', + 'drop' + ], + props: { + config: { + type: String, + required: true, + }, + preselectedKey: { + type: String, + default: null + } + }, + data() { + return { + loading: true, + nodes: [], + selectedKey: [], + expandedKeys: {}, + filters: {}, // TODO(chris): filter only 1st level? + favorites: {on: false, list: []} + } + }, + computed: { + filteredNodes() { + if (this.favorites.on) + return this.nodes.filter(node => this.favorites.list.includes(node.data.path)); + + return this.nodes; + } + }, + watch: { + preselectedKey(newVal, oldVal) { + if (newVal !== oldVal) { + this.setPreselection(); + } + } + }, + methods: { + reloadNodesWithProp(prop, nodes = undefined) { + if (!nodes) + nodes = this.nodes; + + nodes.forEach(node => { + if (node.data[prop]) { + // reload + delete node.children; + this.onExpandTreeNode(node); + } else if (node.children) { + this.reloadNodesWithProp(prop, node.children); + } + }); + }, + findNodeByKey(key, arr) { + if (!arr) + arr = this.nodes; + let res = arr.filter(n => n.key == key); + if (res.length) + return res.pop(); + res = arr.map(n => n.children ? this.findNodeByKey(key, n.children) : null).filter(a => a); + if (res.length) + return res.pop(); + return null; + }, + async onExpandTreeNode(node) { + if (!node.children) { + if (node.data.path) { + /** + * NOTE(chris): activeEl is for keyboard navigation to + * prevent the focus jumping down to the next parent + * instead of the current submenu entry (which is not yet + * loaded) + */ + let activeEl = null; + this.$nextTick(() => { + this.$nextTick(() => { + activeEl = document.activeElement; + }); + }); + this.loading = true; + + return this.$api + .call(ApiMenu.get(this.config, node.data.path)) + .then(result => { + const subNodes = result.data.map(this.mapResultToTreeData); + const realNode = this.findNodeByKey(node.key); + if (realNode) + realNode.children = subNodes; + else + node.children = subNodes; // NOTE(chris): fallback should never be the case + + this.$nextTick(() => { + if (activeEl != document.activeElement) + return; + + let treeitem = this.$refs.tree.$el.querySelector('[data-tree-item-key="' + node.key + '"]'); + if (!treeitem) + return; + + treeitem = treeitem.closest('[role="row"]'); + + if (!treeitem) + return; + + treeitem.dispatchEvent(new KeyboardEvent('keydown', { + code: 'ArrowDown', + key: 'ArrowDown' + })); + }); + + this.loading = false; + }) + .catch(this.$fhcAlert.handleSystemError); + } + } + }, + onSelectTreeNode(node) { + this.$emit('selectEntry', node.data); + }, + mapNodesToNoSemReloadNodes(result, node) { + if (node.data.no_sem_reload) + result.push(node); + if (node.children) + result = node.children.reduce(this.mapNodesToNoSemReloadNodes, result); + return result; + }, + mapResultToTreeData(el) { + const cp = { + key: ("" + el.path).replace(/\//g, '-'), + data: el, + label: el.name // TODO(chris): phrase + }; + + if (el.children) + cp.children = el.children.map(this.mapResultToTreeData); + else + cp.leaf = el.leaf || false; + + return cp; + }, + async setPreselection() + { + if (!this.preselectedKey) + { + this.selectedKey = null; + return; + } + + let rawKey = this.preselectedKey + + if (!rawKey || typeof rawKey !== 'string') + return; + + const parts = this.preselectedKey.split('/'); + let currentKey = parts[0]; + let currentNode = this.findNodeByKey(currentKey); + + if (!currentNode) + return; + + if(this.selectedKey) + { + const currentSelectedKey = Object.keys(this.selectedKey).find(Boolean); + if (currentSelectedKey) { + if (currentSelectedKey == currentKey) + return; + /** + * Do not select a new entry if the current is a child of the new one. + * This happens if a child entry of a new stg is selected and the router + * tries to select the stg root entry (because subtrees do not have + * routes yet) + */ + const isChild = this.findNodeByKey( + currentSelectedKey, + currentNode.children || [] + ); + if (isChild) + return; + } + } + + for (let i = 1; i < parts.length; i++) + { + this.expandedKeys[currentNode.key] = true; + + await this.onExpandTreeNode(currentNode); + + currentKey += '-' + parts[i]; + currentNode = this.findNodeByKey(currentKey); + + if (!currentNode) + { + return; + } + } + + this.selectedKey = {[currentNode.key]: true}; + this.onSelectTreeNode(currentNode); + }, + async toggleTreeNode(node) { + if (this.expandedKeys[node.key]) { + delete this.expandedKeys[node.key]; + } else if (!node.leaf) { + await this.onExpandTreeNode(node); + this.expandedKeys[node.key] = true; + } + }, + filterFav() { + this.favorites.on = !this.favorites.on; + this.$api + .call(ApiMenu.favorites.set( + JSON.stringify(this.favorites) + )); + }, + markFav(key) { + let index = this.favorites.list.indexOf(key.data.path + ''); + + if (index != -1) { + this.favorites.list.splice(index, 1); + } else { + this.favorites.list.push(key.data.path + ''); + } + + this.$api + .call(ApiMenu.favorites.set( + JSON.stringify(this.favorites) + )); + }, + unsetFavFocus(e) { + if (e.target.dataset?.linkFavAdd !== undefined) { + e.target.tabIndex = -1; + } else { + let items = e.target.querySelectorAll('[data-link-fav-add]:not([tabindex="-1"])'); + items.forEach(el => el.tabIndex = document.activeElement == el ? 0 : -1); + } + }, + setFavFocus(e) { + if (e.target.dataset?.linkFavAdd !== undefined) { + e.target.tabIndex = 0; + } else { + let items = e.target.querySelectorAll('[data-link-fav-add][tabindex="-1"]'); + items.forEach(el => el.tabIndex = 0); + } + } + }, + mounted() { + this.$api + .call(ApiMenu.get(this.config)) + .then(result => { + this.nodes = result.data.map(el => { + el.root = true; + return this.mapResultToTreeData(el); + }); + this.setPreselection(); + this.loading = false; + }) + .catch(this.$fhcAlert.handleSystemError); + + this.$api + .call(ApiMenu.favorites.get()) + .then(result => { + if (result.data) { + this.favorites = JSON.parse(result.data); + } + }) + .catch(this.$fhcAlert.handleSystemError); + }, + template: /* html */` + + + + + + + + + + + ` +}; diff --git a/public/js/components/Base/Menu/Entry.js b/public/js/components/Base/Menu/Entry.js new file mode 100644 index 000000000..b12145356 --- /dev/null +++ b/public/js/components/Base/Menu/Entry.js @@ -0,0 +1,50 @@ +import drop from '../../../directives/drop.js'; + +export default { + directives: { + drop + }, + emits: [ + 'drop' + ], + props: { + node: { + type: Object, + required: true + } + }, + computed: { + name() { + if (Array.isArray(this.node.data.name)) + return this.$p.t(this.node.data.name); + + return this.node.data.name; + }, + title() { + if (!this.node.data.title) + return this.name; + + if (Array.isArray(this.node.data.title)) + return this.$p.t(this.node.data.title); + + return this.node.data.title; + }, + dropConfig() { + if (!this.node.data?.droplink) + return null; + + const allowed = [ ...this.node.data.droplink ]; + const effect = allowed.shift(); + + return { effect, allowed }; + } + }, + template: /* html */` + + {{ name }} + ` +}; diff --git a/public/js/components/Stv/Studentenverwaltung.js b/public/js/components/Stv/Studentenverwaltung.js index 66ff7f891..25fee8a2e 100644 --- a/public/js/components/Stv/Studentenverwaltung.js +++ b/public/js/components/Stv/Studentenverwaltung.js @@ -273,10 +273,10 @@ export default { }, onSelectVerband({ link, studiengang_kz, semester, orgform_kurzbz }) { let urlpath = String(link); - if (!urlpath.match(/\/prestudent/)) + /*if (!urlpath.match(/\/prestudent/)) { urlpath = 'CURRENT_SEMESTER' + '/' + urlpath; - } + }*/ this.$refs.stvList.updateUrl(ApiStv.students.verband(urlpath)); this.studiengangKz = studiengang_kz; diff --git a/public/js/components/Stv/Studentenverwaltung/Verband.js b/public/js/components/Stv/Studentenverwaltung/Verband.js index 1ff9d2ed9..e93bc3224 100644 --- a/public/js/components/Stv/Studentenverwaltung/Verband.js +++ b/public/js/components/Stv/Studentenverwaltung/Verband.js @@ -1,17 +1,11 @@ -import drop from '../../../directives/drop.js'; -import dragClick from '../../../directives/dragClick.js'; +import BaseMenu from '../../Base/Menu.js'; import ApiStvGroups from '../../../api/factory/stv/group.js'; import ApiStvDetails from '../../../api/factory/stv/details.js'; export default { components: { - PvTreetable: primevue.treetable, - PvColumn: primevue.column - }, - directives: { - drop, - dragClick + BaseMenu }, inject: { $reloadList: { @@ -33,230 +27,22 @@ export default { 'selectVerband' ], props: { - endpoint: { - type: Object, - required: true, - }, preselectedKey: { type: String, default: null } }, - data() { - return { - loading: true, - nodes: [], - selectedKey: [], - expandedKeys: {}, - filters: {}, // TODO(chris): filter only 1st level? - favorites: {on: false, list: []} - } - }, - computed: { - filteredNodes() { - if (this.favorites.on) - return this.nodes.filter(node => this.favorites.list.includes(node.key)); - - return this.nodes; - }, - noSemReloadNodes() { - return this.nodes.reduce(this.mapNodesToNoSemReloadNodes, []); - } - }, watch: { - 'preselectedKey': function (newVal, oldVal) { - if (newVal !== oldVal) { - this.setPreselection(); - } - }, 'appConfig.number_displayed_past_studiensemester'(newVal, oldVal) { - if (oldVal !== undefined) { - this.noSemReloadNodes.forEach(node => { - delete node.children; - this.onExpandTreeNode(node); - }); + if (oldVal !== undefined && this.$refs.menu) { + this.$refs.menu.reloadNodesWithProp('no_sem_reload'); } } }, methods: { - findNodeByKey(key, arr) { - if (!arr) - arr = this.nodes; - let res = arr.filter(n => n.key == key); - if (res.length) - return res.pop(); - res = arr.map(n => n.children ? this.findNodeByKey(key, n.children) : null).filter(a => a); - if (res.length) - return res.pop(); - return null; - }, - async onExpandTreeNode(node) { - if (!node.children) { - if (node.data.link) { - let activeEl = null; - this.$nextTick(() => { - this.$nextTick(() => { - activeEl = document.activeElement; - }); - }); - this.loading = true; - - return this.$api - .call(this.endpoint.get(node.data.link)) - .then(result => result.data) - .then(result => { - const subNodes = result.map(this.mapResultToTreeData); - const realNode = this.findNodeByKey(node.key); - if (realNode) - realNode.children = subNodes; - else - node.children = subNodes; // NOTE(chris): fallback should never be the case - - let treeitem = this.$refs.tree.$el.querySelector('[data-tree-item-key="' + node.key + '"]'); - treeitem = treeitem.closest('[role="row"]'); - - this.$nextTick(() => { - if (activeEl == document.activeElement) - treeitem.dispatchEvent(new KeyboardEvent('keydown', { - code: 'ArrowDown', - key: 'ArrowDown' - })); - }); - - this.loading = false; - }) - .catch(this.$fhcAlert.handleSystemError); - } - } - }, onSelectTreeNode(node) { - if (node.data.link) - this.$emit('selectVerband', {link: node.data.link, studiengang_kz: node.data.stg_kz, semester: node.data.semester, orgform_kurzbz: node.data.orgform_kurzbz}); - }, - mapNodesToNoSemReloadNodes(result, node) { - if (node.data.no_sem_reload) - result.push(node); - if (node.children) - result = node.children.reduce(this.mapNodesToNoSemReloadNodes, result); - return result; - }, - mapResultToTreeData(el) { - const cp = { - key: ("" + el.link).replace('/', '-'), - data: el, - label: el.name - }; - - if (el.children) - cp.children = el.children.map(this.mapResultToTreeData); - else - cp.leaf = el.leaf || false; - - return cp; - }, - filterFav() { - this.favorites.on = !this.favorites.on; - this.$api - .call(this.endpoint.favorites.set( - JSON.stringify(this.favorites) - )); - }, - markFav(key) { - let index = this.favorites.list.indexOf(key.data.link + ''); - - if (index != -1) { - this.favorites.list.splice(index, 1); - } else { - this.favorites.list.push(key.data.link + ''); - } - - this.$api - .call(this.endpoint.favorites.set( - JSON.stringify(this.favorites) - )); - }, - unsetFavFocus(e) { - if (e.target.dataset?.linkFavAdd !== undefined) { - e.target.tabIndex = -1; - } else { - let items = e.target.querySelectorAll('[data-link-fav-add]:not([tabindex="-1"])'); - items.forEach(el => el.tabIndex = document.activeElement == el ? 0 : -1); - } - }, - setFavFocus(e) { - if (e.target.dataset?.linkFavAdd !== undefined) { - e.target.tabIndex = 0; - } else { - let items = e.target.querySelectorAll('[data-link-fav-add][tabindex="-1"]'); - items.forEach(el => el.tabIndex = 0); - } - }, - async setPreselection() - { - if (!this.preselectedKey) - { - this.selectedKey = null; - return; - } - - let rawKey = this.preselectedKey - - if (!rawKey || typeof rawKey !== 'string') - return; - - const parts = this.preselectedKey.split('/'); - let currentKey = parts[0]; - let currentNode = this.findNodeByKey(currentKey); - - if (!currentNode) - return; - - if(this.selectedKey) - { - const currentSelectedKey = Object.keys(this.selectedKey).find(Boolean); - if (currentSelectedKey) { - if (currentSelectedKey == currentKey) - return; - /** - * Do not select a new entry if the current is a child of the new one. - * This happens if a child entry of a new stg is selected and the router - * tries to select the stg root entry (because subtrees do not have - * routes yet) - */ - const isChild = this.findNodeByKey( - currentSelectedKey, - currentNode.children || [] - ); - if (isChild) - return; - } - } - - for (let i = 1; i < parts.length; i++) - { - this.expandedKeys[currentNode.key] = true; - - await this.onExpandTreeNode(currentNode); - - currentKey += '-' + parts[i]; - currentNode = this.findNodeByKey(currentKey); - - if (!currentNode) - { - return; - } - } - - this.selectedKey = {[currentNode.key]: true}; - this.onSelectTreeNode(currentNode); - }, - async toggleTreeNode(node) { - if (this.expandedKeys[node.key]) { - delete this.expandedKeys[node.key]; - } else if (!node.leaf) { - await this.onExpandTreeNode(node); - this.expandedKeys[node.key] = true; - } + if (node.link) + this.$emit('selectVerband', {link: node.link, studiengang_kz: node.stg_kz, semester: node.semester, orgform_kurzbz: node.orgform_kurzbz}); }, getStudentAjaxId(student) { let res = student.id; @@ -264,23 +50,22 @@ export default { res += ' (' + student.vorname + ' ' + student.nachname + ')'; return res; }, - dropStudents(node, students) { - const data = node.data; - + onDrop({ drag, drop }) { let endpoint; - if (data.gruppe_kurzbz) { - endpoint = students.map(student => [ + + if (drop.gruppe_kurzbz) { + endpoint = drag.map(student => [ this.getStudentAjaxId(student), ApiStvGroups.add( student.id, - data.gruppe_kurzbz, + drop.gruppe_kurzbz, this.currentSemester ) ]); } else { - const { semester, verband, gruppe } = data; + const { semester, verband, gruppe } = drop; const params = { semester, verband, gruppe }; - endpoint = students.map(student => [ + endpoint = drag.map(student => [ this.getStudentAjaxId(student), ApiStvDetails.saveStudent( student.id, @@ -296,117 +81,14 @@ export default { .catch(this.$fhcAlert.handleSystemError); } }, - mounted() { - this.$api - .call(this.endpoint.get()) - .then(result => { - this.nodes = result.data.map(el => { - el.root = true; - return this.mapResultToTreeData(el); - }); - this.setPreselection(); - this.loading = false; - }) - .catch(this.$fhcAlert.handleSystemError); - - this.$api - .call(this.endpoint.favorites.get()) - .then(result => { - if (result.data) { - this.favorites = JSON.parse(result.data); - } - }) - .catch(this.$fhcAlert.handleSystemError); - }, template: /* html */`
- - - - - - - - - - - +
` }; From a7cc07fe15ff341f2082373305a64a6bd696f41e Mon Sep 17 00:00:00 2001 From: chfhtw Date: Mon, 11 May 2026 09:31:08 +0200 Subject: [PATCH 03/42] Add basic library and config for studvw --- application/config/menubuilder.php | 193 ++++++++++ application/libraries/MenuBuilderLib.php | 222 ++++++++++++ .../libraries/menu/StgBasedMenuLib.php | 340 ++++++++++++++++++ application/libraries/menu/StvMenuLib.php | 78 ++++ 4 files changed, 833 insertions(+) create mode 100644 application/libraries/MenuBuilderLib.php create mode 100644 application/libraries/menu/StgBasedMenuLib.php create mode 100644 application/libraries/menu/StvMenuLib.php diff --git a/application/config/menubuilder.php b/application/config/menubuilder.php index 64337a691..b63b4f079 100644 --- a/application/config/menubuilder.php +++ b/application/config/menubuilder.php @@ -20,3 +20,196 @@ if (!defined('BASEPATH')) exit('No direct script access allowed'); $config['stv'] = "menu/StvMenuLib"; + +$config_part_stg_orgform = [ + 'prestudent' => [ + 'name' => ['stv', 'prestudent'], + 'no_sem_reload' => true, + 'children' => [ + 'stdsem' => [ + 'generationFunc' => 'getStdSemester', + 'children' => [ + 'interessenten' => [ + 'name' => ['stv', 'interessenten'], + 'children' => [ + 'bewerbungnichtabgeschickt' => [ + 'name' => 'Bewerbung nicht abgeschickt' + ], + 'bewerbungabgeschickt' => [ + 'name' => 'Bewerbung abgeschickt, Status unbestätigt' + ], + 'zgv' => [ + 'name' => 'ZGV erfüllt' + ], + 'statusbestaetigt' => [ + 'name' => 'Status bestätigt', + 'children' => [ + 'statusbestaetigtrtnichtangemeldet' => [ + 'name' => 'Nicht zum Reihungstest angemeldet' + ], + 'statusbestaetigtrtangemeldet' => [ + 'name' => 'Reihungstest angemeldet' + ] + ] + ], + 'reihungstestnichtangemeldet' => [ + 'name' => 'Nicht zum Reihungstest angemeldet' + ], + 'reihungstestangemeldet' => [ + 'name' => 'Reihungstest angemeldet' + ] + + ] + ], + 'bewerber' => [ + 'name' => ['stv', 'bewerber'], + 'children' => [ + 'bewerberrtnichtangemeldet' => [ + 'name' => 'Nicht zum Reihungstest angemeldet' + ], + 'bewerberrtangemeldet' => [ + 'name' => 'Reihungstest angemeldet', + 'children' => [ + 'bewerberrtangemeldetteilgenommen' => [ + 'name' => 'Teilgenommen' + ], + 'bewerberrtangemeldetnichtteilgenommen' => [ + 'name' => 'Nicht teilgenommen' + ] + ] + ] + ] + ], + 'aufgenommen' => [ + 'name' => ['stv', 'aufgenommen'] + ], + 'warteliste' => [ + 'name' => ['stv', 'warteliste'] + ], + 'absage' => [ + 'name' => ['stv', 'absage'] + ], + 'incoming' => [ + 'name' => ['stv', 'incoming'] + ] + ] + ] + ] + ], + 'semester' => [ + 'generationFunc' => 'getSemester', + 'children' => [ + 'group' => [ + 'generationFunc' => 'getGroups' + ], + 'verband' => [ + 'generationFunc' => 'getVerbaende', + 'children' => [ + 'group' => [ + 'generationFunc' => 'getVerbandGroups' + ] + ] + ] + ] + ] +]; +$config['config_stg_based'] = [ + 'stg' => [ + 'generationFunc' => 'getStgs', + 'children' => array_merge( + $config_part_stg_orgform, + [ + 'orgform' => [ + 'generationFunc' => 'getOrgforms', + 'children' => $config_part_stg_orgform + ] + ] + ) + ], + 'inout' => [ + 'name' => ['stv', 'inout'], + 'children' => [ + 'incoming' => [ + 'name' => ['stv', 'incoming'] + ], + 'outgoing' => [ + 'name' => ['stv', 'outgoing'] + ], + 'shared_studies' => [ + 'name' => ['stv', 'shared_studies'] + ] + ] + ] +]; +/*$config['config_stg_based'] = [ + 'stgLib' => array_merge( + $config_part_stg_orgform, + [ + 'orgformLib' => [ + 'prestudentLib' => [ + 'stdsemLib' => [ + 'interessentenLib' + ], + 'bewerber' => [ + 'name' => ['stv', 'bewerber'], + 'children' => [ + 'bewerberrtnichtangemeldet' => [ + 'name' => 'Nicht zum Reihungstest angemeldet' + ], + 'bewerberrtangemeldet' => [ + 'name' => 'Reihungstest angemeldet', + 'children' => [ + 'bewerberrtangemeldetteilgenommen' => [ + 'name' => 'Teilgenommen' + ], + 'bewerberrtangemeldetnichtteilgenommen' => [ + 'name' => 'Nicht teilgenommen' + ] + ] + ] + ] + ], + 'aufgenommen' => [ + 'name' => ['stv', 'aufgenommen'] + ], + 'warteliste' => [ + 'name' => ['stv', 'warteliste'] + ], + 'absage' => [ + 'name' => ['stv', 'absage'] + ], + 'incoming' => [ + 'name' => ['stv', 'incoming'] + ] + ] + ] + ] + ], + 'semester' => [ + 'generationFunc' => 'getSemester', + 'children' => [ + 'group' => [ + 'generationFunc' => 'getGroups' + ], + 'verband' => [ + 'generationFunc' => 'getVerbaende', + 'children' => [ + 'group' => [ + 'generationFunc' => 'getVerbandGroups' + ] + ] + ] + ] + ] + + ] + ] + ) + ], + 'inoutLib' => [ + 'incomingLib', + 'outgoingLib', + 'shared_studiesLib' + ] +]; +*/ \ No newline at end of file diff --git a/application/libraries/MenuBuilderLib.php b/application/libraries/MenuBuilderLib.php new file mode 100644 index 000000000..0509c98aa --- /dev/null +++ b/application/libraries/MenuBuilderLib.php @@ -0,0 +1,222 @@ +. + */ + +if (! defined('BASEPATH')) exit('No direct script access allowed'); + +/** + * MenuBuilder library + * TODO(chris): docu + */ +class MenuBuilderLib +{ + const GENERATOR_FUNC_KEY = 'generationFunc'; + + protected $_ci; + + protected $config = []; + + /** + * Constructor + */ + public function __construct() + { + // Get code igniter instance + $this->_ci =& get_instance(); + } + + // TODO(chris): abstract + + /** + * TODO(chris): comment + * + * @param string $key + * @param mixed $value + * + * @return array + */ + protected function mapUrlPartToVars($key, $value) + { + return [ $key, $value ]; + } + + protected function getPathTemplate($vars) + { + return implode('/', $vars['_url']) . '/%s'; + } + + protected function getLinkTemplate($vars) + { + return implode('/', $vars['_url']) . '/%s'; + } + + protected function buildMenu() + { + return $this->buildMenuRecursive($this->config, [ + '_url' => [], + '_path' => [] + ]); + } + + private function buildMenuRecursive($config, $vars) + { + $result = []; + + foreach ($config as $key => $conf) { + $res = $this->buildConfigItem($key, $conf, $vars); + if (isset($conf['children'])) { + foreach ($res as $k => $menuitem) { + // convert stdClass to associative array if necessary + if (!is_array($menuitem)) { + $menuitem = get_object_vars($menuitem); + $res[$k] = $menuitem; + } + + $child_vars = $menuitem; + unset($child_vars['name']); + $child_vars['_url'] = explode('/', $child_vars['path']); + unset($child_vars['path']); + $child_vars = array_merge($vars, $child_vars); + $child_vars['_path'][] = $key; + + $res[$k]['children'] = $this->buildMenuRecursive($conf['children'], $child_vars); + } + } + $result = array_merge($result, $res); + } + + return $result; + } + + protected function buildSubmenu($url_segments) + { + $vars = $this->convertUrlPathToVars($url_segments); + if ($vars === null) + return null; + + $config = $this->getSubconfig($vars['_path']); + if ($config === null) + return null; + + $result = []; + + foreach ($config as $key => $conf) { + $res = $this->buildConfigItem($key, $conf, $vars); + $result = array_merge($result, $res); + } + + return $result; + } + + /** + * @param array $path + * + * @return array|null + */ + private function getSubconfig($path) + { + $config = $this->config; + + while (count($path)) { + $part = array_shift($path); + + if (!isset($config[$part])) + return null; + if (!isset($config[$part]['children'])) + return null; + + $config = $config[$part]['children']; + } + + return $config; + } + + /** + * @param array $url_segments + * @return array|null + */ + private function convertUrlPathToVars($url_segments) + { + $config = $this->config; + + $result = [ + '_url' => $url_segments, + '_path' => [] + ]; + while (count($url_segments)) { + if (!$config) + return null; + + $segment = array_shift($url_segments); + + if (!isset($config[$segment])) + return null; + + $config = $config[$segment]; + $result['_path'][] = $segment; + + if (isset($config[self::GENERATOR_FUNC_KEY])) { + $value = array_shift($url_segments); + list($key, $value) = $this->mapUrlPartToVars($segment, $value); + $result[$key] = $value; + } + + $config = isset($config['children']) ? $config['children'] : null; + } + + return $result; + } + + protected function buildConfigItem($key, $config, $vars) + { + if (isset($config[self::GENERATOR_FUNC_KEY])) + return $this->buildItemWithMethod($key, $config, $vars); + else + return $this->buildGenericItem($key, $config, $vars); + } + + protected function buildItemWithMethod($key, $config, $vars) + { + $vars['_url'][] = $key; + + $method = $config[self::GENERATOR_FUNC_KEY]; + + return $this->$method($vars); + } + + protected function buildGenericItem($key, $config, $vars) + { + $url_segments = $vars['_url']; + $url_segments[] = $key; + + $result = array_merge($config, $vars); + + if (!isset($result['children'])) + $result['leaf'] = true; + else + unset($result['children']); + + $result['path'] = sprintf($this->getPathTemplate($result), $key); + $result['link'] = sprintf($this->getLinkTemplate($result), $key); + + unset($result['_url']); + unset($result['_path']); + + return [ $result ]; + } +} diff --git a/application/libraries/menu/StgBasedMenuLib.php b/application/libraries/menu/StgBasedMenuLib.php new file mode 100644 index 000000000..bcff3f7a3 --- /dev/null +++ b/application/libraries/menu/StgBasedMenuLib.php @@ -0,0 +1,340 @@ +. + */ + +if (! defined('BASEPATH')) exit('No direct script access allowed'); + +require_once(APPPATH . 'libraries/MenuBuilderLib.php'); + +/** + * StudVw Menu library + */ +class StgBasedMenuLib extends MenuBuilderLib +{ + /** + * Constructor + */ + public function __construct() + { + parent::__construct(); + $this->config = $this->_ci->config->item('config_stg_based'); + } + + protected function mapUrlPartToVars($key, $value) + { + if ($key == 'stg') + $key = 'studiengang_kz'; + if ($key == 'orgform') + $key = 'org_form'; + return [ $key, $value ]; + } + + protected function getStgs($vars) + { + $stgs = $this->_ci->permissionlib->getSTG_isEntitledFor('admin') ?: []; + $stgs = array_merge($stgs, $this->_ci->permissionlib->getSTG_isEntitledFor('assistenz') ?: []); + + if (!$stgs) + return []; + + $pathTemplate = $this->getPathTemplate($vars); + $linkTemplate = $this->getLinkTemplate($vars); + + $this->_ci->load->model('organisation/Studiengang_model', 'StudiengangModel'); + + $this->_ci->StudiengangModel->addJoin('public.tbl_lehrverband v', 'studiengang_kz'); + + $this->_ci->StudiengangModel->addDistinct(); + $this->_ci->StudiengangModel->addSelect("FORMAT(" . $this->_ci->StudiengangModel->escape($pathTemplate) . ", v.studiengang_kz) AS path"); + $this->_ci->StudiengangModel->addSelect("FORMAT(" . $this->_ci->StudiengangModel->escape($linkTemplate) . ", v.studiengang_kz) AS link"); + $this->_ci->StudiengangModel->addSelect( + "CONCAT(kurzbzlang, ' (', UPPER(CONCAT(typ, kurzbz)), ') - ', tbl_studiengang.bezeichnung) AS name", + false + ); + $this->_ci->StudiengangModel->addSelect("studiengang_kz AS title"); + $this->_ci->StudiengangModel->addSelect("studiengang_kz AS search"); + $this->_ci->StudiengangModel->addSelect('erhalter_kz'); + $this->_ci->StudiengangModel->addSelect('typ'); + $this->_ci->StudiengangModel->addSelect('kurzbz'); + $this->_ci->StudiengangModel->addSelect('studiengang_kz'); + $this->_ci->StudiengangModel->addSelect('studiengang_kz AS stg_kz'); + + $this->_ci->StudiengangModel->addOrder('erhalter_kz'); + $this->_ci->StudiengangModel->addOrder('typ'); + $this->_ci->StudiengangModel->addOrder('kurzbz'); + + $this->_ci->StudiengangModel->db->where_in('studiengang_kz', $stgs); + + $result = $this->_ci->StudiengangModel->loadWhere(['v.aktiv' => true]); + + return getData($result) ?: []; + } + + protected function getSemester($vars) + { + // TODO(chris): permission on stg + // TODO(chris): check vars + + $pathTemplate = $this->getPathTemplate($vars); + $linkTemplate = $this->getLinkTemplate($vars); + + $this->_ci->load->model('organisation/Studiengang_model', 'StudiengangModel'); + + $this->_ci->StudiengangModel->addJoin('public.tbl_lehrverband v', 'studiengang_kz'); + + $this->_ci->StudiengangModel->addDistinct(); + $this->_ci->StudiengangModel->addSelect("FORMAT(" . $this->_ci->StudiengangModel->escape($pathTemplate) . ", semester) AS path", false); + $this->_ci->StudiengangModel->addSelect("FORMAT(" . $this->_ci->StudiengangModel->escape($linkTemplate) . ", semester) AS link", false); + $this->_ci->StudiengangModel->addSelect("CONCAT( + UPPER(CONCAT(typ, kurzbz)), + '-', + semester, + ( + SELECT CASE WHEN bezeichnung IS NULL OR bezeichnung='' THEN ''::TEXT ELSE CONCAT(' (', bezeichnung, ')') END + FROM public.tbl_lehrverband + WHERE studiengang_kz=v.studiengang_kz AND semester=v.semester + ORDER BY verband, gruppe LIMIT 1 + ) + ) AS name", false); + + $this->_ci->StudiengangModel->addSelect('semester'); + $this->_ci->StudiengangModel->addSelect($this->_ci->StudiengangModel->escape($vars['studiengang_kz']) . '::integer AS stg_kz', false); + $this->_ci->StudiengangModel->addSelect("ARRAY['link-strict', 'student-collection'] AS droplink"); + + $this->_ci->StudiengangModel->addOrder('semester'); + + if (isset($vars['org_form'])) { + $this->_ci->StudiengangModel->addSelect("v.orgform_kurzbz"); + $this->_ci->StudiengangModel->db->group_start(); + $this->_ci->StudiengangModel->db->where('v.semester', 0); + $this->_ci->StudiengangModel->db->or_where('v.orgform_kurzbz', $vars['org_form']); + $this->_ci->StudiengangModel->db->group_end(); + } + + $result = $this->_ci->StudiengangModel->loadWhere([ + 'v.studiengang_kz' => $vars['studiengang_kz'], + 'v.aktiv' => true + ]); + + return getData($result) ?: []; + } + + protected function getOrgforms($vars) + { + // TODO(chris): permission on stg + // TODO(chris): check vars + + $pathTemplate = $this->getPathTemplate($vars); + $linkTemplate = $this->getLinkTemplate($vars); + + $this->_ci->load->model('organisation/Studiengang_model', 'StudiengangModel'); + + // NOTE(chris): if mischform show orgforms + $result = $this->_ci->StudiengangModel->load($vars['studiengang_kz']); + + if (!hasData($result)) + return []; + + $stg = current(getData($result)); + + if (!$stg->mischform) + return []; + + $this->_ci->load->model('organisation/Studienordnung_model', 'StudienordnungModel'); + + $this->_ci->StudienordnungModel->addDistinct(); + $this->_ci->StudienordnungModel->addSelect("FORMAT(" . $this->_ci->StudienordnungModel->escape($pathTemplate) . ", p.orgform_kurzbz) AS path"); + $this->_ci->StudienordnungModel->addSelect("FORMAT(" . $this->_ci->StudienordnungModel->escape($linkTemplate) . ", p.orgform_kurzbz) AS link"); + $this->_ci->StudienordnungModel->addSelect("p.orgform_kurzbz AS name"); + $this->_ci->StudienordnungModel->addSelect("studiengang_kz AS stg_kz"); + + $this->_ci->StudienordnungModel->addJoin('lehre.tbl_studienplan p', 'studienordnung_id'); + + $result = $this->_ci->StudienordnungModel->loadWhere([ + 'aktiv' => true, + 'studiengang_kz' => $vars['studiengang_kz'], + 'p.orgform_kurzbz !=' => 'DDP' + ]); + + return getData($result) ?: []; + } + + protected function getGroups($vars) + { + // TODO(chris): permission on stg + // TODO(chris): check vars + + $pathTemplate = $this->getPathTemplate($vars); + $linkTemplate = $this->getLinkTemplate($vars); + + $this->_ci->load->model('organisation/Gruppe_model', 'GruppeModel'); + + $this->_ci->GruppeModel->addDistinct(); + $this->_ci->GruppeModel->addSelect("FORMAT(" . $this->_ci->GruppeModel->escape($pathTemplate) . ", gruppe_kurzbz) AS path", false); + $this->_ci->GruppeModel->addSelect("FORMAT(" . $this->_ci->GruppeModel->escape($linkTemplate) . ", gruppe_kurzbz) AS link", false); + $this->_ci->GruppeModel->addSelect("CONCAT(gruppe_kurzbz, ' (', bezeichnung, ')') AS name", false); + $this->_ci->GruppeModel->addSelect("TRUE AS leaf", false); + + $this->_ci->GruppeModel->addSelect('sort'); + $this->_ci->GruppeModel->addSelect('gruppe_kurzbz'); + $this->_ci->GruppeModel->addSelect($this->_ci->GruppeModel->escape($vars['studiengang_kz']) . '::integer AS stg_kz', false); + $this->_ci->GruppeModel->addSelect("ARRAY['link-strict', 'student-collection'] AS droplink"); + + $this->_ci->GruppeModel->addOrder('sort'); + $this->_ci->GruppeModel->addOrder('gruppe_kurzbz'); + + $where = [ + 'studiengang_kz' => $vars['studiengang_kz'], + 'semester' => $vars['semester'], + 'lehre' => true, + 'sichtbar' => true, + 'aktiv' => true, + 'direktinskription' => false + ]; + + if (isset($vars['org_form'])) + $where['orgform_kurzbz'] = $vars['org_form']; + + $result = $this->_ci->GruppeModel->loadWhere($where); + + return getData($result) ?: []; + } + + protected function getVerbaende($vars) + { + // TODO(chris): permission on stg + // TODO(chris): check vars + + $pathTemplate = $this->getPathTemplate($vars); + $linkTemplate = $this->getLinkTemplate($vars); + + $this->_ci->load->model('organisation/Studiengang_model', 'StudiengangModel'); + + $this->_ci->StudiengangModel->addJoin('public.tbl_lehrverband v', 'studiengang_kz'); + + $this->_ci->StudiengangModel->addSelect("FORMAT(" . $this->_ci->StudiengangModel->escape($pathTemplate) . ", verband) AS path", false); + $this->_ci->StudiengangModel->addSelect("FORMAT(" . $this->_ci->StudiengangModel->escape($linkTemplate) . ", verband) AS link", false); + $this->_ci->StudiengangModel->addSelect("CONCAT(UPPER(CONCAT(typ, kurzbz)), '-', semester, verband, (SELECT CASE WHEN bezeichnung IS NULL OR bezeichnung='' THEN ''::TEXT ELSE CONCAT(' (', bezeichnung, ')') END FROM public.tbl_lehrverband WHERE studiengang_kz=v.studiengang_kz AND semester=v.semester AND verband=v.verband ORDER BY gruppe LIMIT 1)) AS name", false); + $this->_ci->StudiengangModel->addSelect("CASE WHEN MAX(gruppe)='' OR MAX(gruppe)=' ' THEN TRUE ELSE FALSE END AS leaf"); + + $this->_ci->StudiengangModel->addSelect($this->_ci->StudiengangModel->escape($vars['semester']) . ' AS semester'); + $this->_ci->StudiengangModel->addSelect('verband'); + $this->_ci->StudiengangModel->addSelect($this->_ci->StudiengangModel->escape($vars['studiengang_kz']) . '::integer AS stg_kz', false); + $this->_ci->StudiengangModel->addSelect("ARRAY['link-strict', 'student-collection'] AS droplink"); + + $this->_ci->StudiengangModel->addOrder('verband'); + + $this->_ci->StudiengangModel->addGroupBy('path, link, name, verband'); + + $where = [ + 'v.studiengang_kz' => $vars['studiengang_kz'], + 'v.semester' => $vars['semester'], + 'v.verband !=' => '', + 'v.aktiv' => true + ]; + + if (isset($vars['org_form']) && $vars['semester']) // NOTE(chris): on semester 0 show all? + $where['v.orgform_kurzbz'] = $vars['org_form']; + + $result = $this->_ci->StudiengangModel->loadWhere($where); + + return getData($result) ?: []; + } + + protected function getVerbandGroups($vars) + { + // TODO(chris): permission on stg + // TODO(chris): check vars + + $pathTemplate = $this->getPathTemplate($vars); + $linkTemplate = $this->getLinkTemplate($vars); + + $this->_ci->load->model('organisation/Studiengang_model', 'StudiengangModel'); + + $this->_ci->StudiengangModel->addJoin('public.tbl_lehrverband v', 'studiengang_kz'); + + $this->_ci->StudiengangModel->addDistinct(); + $this->_ci->StudiengangModel->addSelect("FORMAT(" . $this->_ci->StudiengangModel->escape($pathTemplate) . ", gruppe) AS path", false); + $this->_ci->StudiengangModel->addSelect("FORMAT(" . $this->_ci->StudiengangModel->escape($linkTemplate) . ", gruppe) AS link", false); + $this->_ci->StudiengangModel->addSelect("CONCAT(UPPER(CONCAT(typ, kurzbz)), '-', semester, verband, gruppe, (SELECT CASE WHEN bezeichnung IS NULL OR bezeichnung='' THEN ''::TEXT ELSE CONCAT(' (', bezeichnung, ')') END FROM public.tbl_lehrverband WHERE studiengang_kz=v.studiengang_kz AND semester=v.semester AND verband=v.verband AND gruppe=v.gruppe ORDER BY gruppe LIMIT 1)) AS name", false); + $this->_ci->StudiengangModel->addSelect("TRUE AS leaf", false); + + $this->_ci->StudiengangModel->addSelect('v.semester'); + $this->_ci->StudiengangModel->addSelect('v.verband'); + $this->_ci->StudiengangModel->addSelect('gruppe'); + $this->_ci->StudiengangModel->addSelect($this->_ci->StudiengangModel->escape($vars['studiengang_kz']) . '::integer AS stg_kz', false); + $this->_ci->StudiengangModel->addSelect("ARRAY['link-strict', 'student-collection'] AS droplink"); + + $this->_ci->StudiengangModel->addOrder('gruppe'); + + $where = [ + 'v.studiengang_kz' => $vars['studiengang_kz'], + 'v.semester' => $vars['semester'], + 'v.verband' => $vars['verband'], + 'v.gruppe !=' => '', + 'v.aktiv' => true + ]; + + if (isset($vars['org_form']) && $vars['semester']) // NOTE(chris): on semester 0 show all? + $where['v.orgform_kurzbz'] = $vars['org_form']; + + $result = $this->_ci->StudiengangModel->loadWhere($where); + + return getData($result) ?: []; + } + + protected function getStdSemester($vars) + { + // TODO(chris): permission on stg + // TODO(chris): check vars + + $pathTemplate = $this->getPathTemplate($vars); + $linkTemplate = $this->getLinkTemplate($vars); + + $number_displayed_past_studiensemester = null; + + $this->_ci->load->model('system/Variable_model', 'VariableModel'); + + $result = $this->_ci->VariableModel->getVariables(getAuthUID(), ['number_displayed_past_studiensemester']); + if (isError($result)) + return []; + + $data = getData($result); + if ($data && isset($data['number_displayed_past_studiensemester'])) { + $number_displayed_past_studiensemester = $data['number_displayed_past_studiensemester']; + } else { + $this->_ci->load->config('stv'); + $number_displayed_past_studiensemester = $this->_ci->config->item('number_displayed_past_studiensemester_default'); + } + + $this->_ci->load->model('organisation/Studiensemester_model', 'StudiensemesterModel'); + + $this->_ci->StudiensemesterModel->addPlusMinus(null, $number_displayed_past_studiensemester); + + $this->_ci->StudiensemesterModel->addSelect("studiensemester_kurzbz AS name"); + $this->_ci->StudiensemesterModel->addSelect("FORMAT(" . $this->_ci->StudiensemesterModel->escape($pathTemplate) . ", studiensemester_kurzbz) AS path", false); + $this->_ci->StudiensemesterModel->addSelect("FORMAT(" . $this->_ci->StudiensemesterModel->escape($linkTemplate) . ", studiensemester_kurzbz) AS link", false); + $this->_ci->StudiensemesterModel->addSelect($this->_ci->StudiensemesterModel->escape($vars['studiengang_kz']) . " AS stg_kz", false); + + $this->_ci->StudiensemesterModel->addOrder('ende'); + + $result = $this->_ci->StudiensemesterModel->load(); + + return getData($result) ?: []; + } +} diff --git a/application/libraries/menu/StvMenuLib.php b/application/libraries/menu/StvMenuLib.php new file mode 100644 index 000000000..0bd975937 --- /dev/null +++ b/application/libraries/menu/StvMenuLib.php @@ -0,0 +1,78 @@ +. + */ + +if (! defined('BASEPATH')) exit('No direct script access allowed'); + +require_once(APPPATH . 'libraries/menu/StgBasedMenuLib.php'); + +/** + * StudVw Menu library + */ +class StvMenuLib extends StgBasedMenuLib +{ + public function build($url_segments = []) + { + $result = $this->buildSubmenu($url_segments); + + if ($result === null) + show_404(); + + return $result; + } + public function buildAll() + { + return $this->buildMenu(); + } + + protected function getLinkTemplate($vars) + { + $result = $this->convertFullUrlToLinkUrl($vars['_url'], $this->config); + + $firstLevelPrestudent = (count($vars['_path']) > 1 && $vars['_path'][1] == 'prestudent'); + $secondLevelPrestudent = (count($vars['_path']) > 2 && $vars['_path'][2] == 'prestudent'); + if (!$firstLevelPrestudent && !$secondLevelPrestudent) + $result = 'CURRENT_SEMESTER/' . $result; + + if ($result) + return $result . '/%s'; + + return '%s'; + } + + private function convertFullUrlToLinkUrl($url_segments, $config) + { + if (!count($url_segments)) + return ''; + $result = $current_segment = array_shift($url_segments); + + if (isset($config[$current_segment][self::GENERATOR_FUNC_KEY])) { + $result = array_shift($url_segments); + } + + $children = ''; + + if (isset($config[$current_segment]['children'])) + $children = $this->convertFullUrlToLinkUrl($url_segments, $config[$current_segment]['children']); + + if ($children) + return $result . '/' . $children; + + return $result; + } +} From c468d350f345520693e47ce56cb354465ecac3d6 Mon Sep 17 00:00:00 2001 From: chfhtw Date: Wed, 13 May 2026 14:16:14 +0200 Subject: [PATCH 04/42] outsource config to better read it and make reuse easier --- application/config/menubuilder.php | 193 +----------------- .../config/menubuilder/config_stg_based.php | 141 +++++++++++++ 2 files changed, 144 insertions(+), 190 deletions(-) create mode 100644 application/config/menubuilder/config_stg_based.php diff --git a/application/config/menubuilder.php b/application/config/menubuilder.php index b63b4f079..07fff2f6b 100644 --- a/application/config/menubuilder.php +++ b/application/config/menubuilder.php @@ -21,195 +21,8 @@ if (!defined('BASEPATH')) exit('No direct script access allowed'); $config['stv'] = "menu/StvMenuLib"; -$config_part_stg_orgform = [ - 'prestudent' => [ - 'name' => ['stv', 'prestudent'], - 'no_sem_reload' => true, - 'children' => [ - 'stdsem' => [ - 'generationFunc' => 'getStdSemester', - 'children' => [ - 'interessenten' => [ - 'name' => ['stv', 'interessenten'], - 'children' => [ - 'bewerbungnichtabgeschickt' => [ - 'name' => 'Bewerbung nicht abgeschickt' - ], - 'bewerbungabgeschickt' => [ - 'name' => 'Bewerbung abgeschickt, Status unbestätigt' - ], - 'zgv' => [ - 'name' => 'ZGV erfüllt' - ], - 'statusbestaetigt' => [ - 'name' => 'Status bestätigt', - 'children' => [ - 'statusbestaetigtrtnichtangemeldet' => [ - 'name' => 'Nicht zum Reihungstest angemeldet' - ], - 'statusbestaetigtrtangemeldet' => [ - 'name' => 'Reihungstest angemeldet' - ] - ] - ], - 'reihungstestnichtangemeldet' => [ - 'name' => 'Nicht zum Reihungstest angemeldet' - ], - 'reihungstestangemeldet' => [ - 'name' => 'Reihungstest angemeldet' - ] +$CI =& get_instance(); - ] - ], - 'bewerber' => [ - 'name' => ['stv', 'bewerber'], - 'children' => [ - 'bewerberrtnichtangemeldet' => [ - 'name' => 'Nicht zum Reihungstest angemeldet' - ], - 'bewerberrtangemeldet' => [ - 'name' => 'Reihungstest angemeldet', - 'children' => [ - 'bewerberrtangemeldetteilgenommen' => [ - 'name' => 'Teilgenommen' - ], - 'bewerberrtangemeldetnichtteilgenommen' => [ - 'name' => 'Nicht teilgenommen' - ] - ] - ] - ] - ], - 'aufgenommen' => [ - 'name' => ['stv', 'aufgenommen'] - ], - 'warteliste' => [ - 'name' => ['stv', 'warteliste'] - ], - 'absage' => [ - 'name' => ['stv', 'absage'] - ], - 'incoming' => [ - 'name' => ['stv', 'incoming'] - ] - ] - ] - ] - ], - 'semester' => [ - 'generationFunc' => 'getSemester', - 'children' => [ - 'group' => [ - 'generationFunc' => 'getGroups' - ], - 'verband' => [ - 'generationFunc' => 'getVerbaende', - 'children' => [ - 'group' => [ - 'generationFunc' => 'getVerbandGroups' - ] - ] - ] - ] - ] -]; -$config['config_stg_based'] = [ - 'stg' => [ - 'generationFunc' => 'getStgs', - 'children' => array_merge( - $config_part_stg_orgform, - [ - 'orgform' => [ - 'generationFunc' => 'getOrgforms', - 'children' => $config_part_stg_orgform - ] - ] - ) - ], - 'inout' => [ - 'name' => ['stv', 'inout'], - 'children' => [ - 'incoming' => [ - 'name' => ['stv', 'incoming'] - ], - 'outgoing' => [ - 'name' => ['stv', 'outgoing'] - ], - 'shared_studies' => [ - 'name' => ['stv', 'shared_studies'] - ] - ] - ] -]; -/*$config['config_stg_based'] = [ - 'stgLib' => array_merge( - $config_part_stg_orgform, - [ - 'orgformLib' => [ - 'prestudentLib' => [ - 'stdsemLib' => [ - 'interessentenLib' - ], - 'bewerber' => [ - 'name' => ['stv', 'bewerber'], - 'children' => [ - 'bewerberrtnichtangemeldet' => [ - 'name' => 'Nicht zum Reihungstest angemeldet' - ], - 'bewerberrtangemeldet' => [ - 'name' => 'Reihungstest angemeldet', - 'children' => [ - 'bewerberrtangemeldetteilgenommen' => [ - 'name' => 'Teilgenommen' - ], - 'bewerberrtangemeldetnichtteilgenommen' => [ - 'name' => 'Nicht teilgenommen' - ] - ] - ] - ] - ], - 'aufgenommen' => [ - 'name' => ['stv', 'aufgenommen'] - ], - 'warteliste' => [ - 'name' => ['stv', 'warteliste'] - ], - 'absage' => [ - 'name' => ['stv', 'absage'] - ], - 'incoming' => [ - 'name' => ['stv', 'incoming'] - ] - ] - ] - ] - ], - 'semester' => [ - 'generationFunc' => 'getSemester', - 'children' => [ - 'group' => [ - 'generationFunc' => 'getGroups' - ], - 'verband' => [ - 'generationFunc' => 'getVerbaende', - 'children' => [ - 'group' => [ - 'generationFunc' => 'getVerbandGroups' - ] - ] - ] - ] - ] +$CI->load->config('menubuilder/config_stg_based'); - ] - ] - ) - ], - 'inoutLib' => [ - 'incomingLib', - 'outgoingLib', - 'shared_studiesLib' - ] -]; -*/ \ No newline at end of file +$config['config_stg_based'] = $CI->config->item('config_stg_based'); diff --git a/application/config/menubuilder/config_stg_based.php b/application/config/menubuilder/config_stg_based.php new file mode 100644 index 000000000..3fbb3b9a8 --- /dev/null +++ b/application/config/menubuilder/config_stg_based.php @@ -0,0 +1,141 @@ +. + */ + +if (!defined('BASEPATH')) exit('No direct script access allowed'); + +$config_part_stg_orgform = [ + 'prestudent' => [ + 'name' => ['stv', 'prestudent'], + 'no_sem_reload' => true, + 'children' => [ + 'stdsem' => [ + 'generationFunc' => 'getStdSemester', + 'children' => [ + 'interessenten' => [ + 'name' => ['stv', 'interessenten'], + 'children' => [ + 'bewerbungnichtabgeschickt' => [ + 'name' => 'Bewerbung nicht abgeschickt' + ], + 'bewerbungabgeschickt' => [ + 'name' => 'Bewerbung abgeschickt, Status unbestätigt' + ], + 'zgv' => [ + 'name' => 'ZGV erfüllt' + ], + 'statusbestaetigt' => [ + 'name' => 'Status bestätigt', + 'children' => [ + 'statusbestaetigtrtnichtangemeldet' => [ + 'name' => 'Nicht zum Reihungstest angemeldet' + ], + 'statusbestaetigtrtangemeldet' => [ + 'name' => 'Reihungstest angemeldet' + ] + ] + ], + 'reihungstestnichtangemeldet' => [ + 'name' => 'Nicht zum Reihungstest angemeldet' + ], + 'reihungstestangemeldet' => [ + 'name' => 'Reihungstest angemeldet' + ] + + ] + ], + 'bewerber' => [ + 'name' => ['stv', 'bewerber'], + 'children' => [ + 'bewerberrtnichtangemeldet' => [ + 'name' => 'Nicht zum Reihungstest angemeldet' + ], + 'bewerberrtangemeldet' => [ + 'name' => 'Reihungstest angemeldet', + 'children' => [ + 'bewerberrtangemeldetteilgenommen' => [ + 'name' => 'Teilgenommen' + ], + 'bewerberrtangemeldetnichtteilgenommen' => [ + 'name' => 'Nicht teilgenommen' + ] + ] + ] + ] + ], + 'aufgenommen' => [ + 'name' => ['stv', 'aufgenommen'] + ], + 'warteliste' => [ + 'name' => ['stv', 'warteliste'] + ], + 'absage' => [ + 'name' => ['stv', 'absage'] + ], + 'incoming' => [ + 'name' => ['stv', 'incoming'] + ] + ] + ] + ] + ], + 'semester' => [ + 'generationFunc' => 'getSemester', + 'children' => [ + 'group' => [ + 'generationFunc' => 'getGroups' + ], + 'verband' => [ + 'generationFunc' => 'getVerbaende', + 'children' => [ + 'group' => [ + 'generationFunc' => 'getVerbandGroups' + ] + ] + ] + ] + ] +]; +$config['config_stg_based'] = [ + 'stg' => [ + 'generationFunc' => 'getStgs', + 'children' => array_merge( + $config_part_stg_orgform, + [ + 'orgform' => [ + 'generationFunc' => 'getOrgforms', + 'children' => $config_part_stg_orgform + ] + ] + ) + ], + 'inout' => [ + 'name' => ['stv', 'inout'], + 'children' => [ + 'incoming' => [ + 'name' => ['stv', 'incoming'] + ], + 'outgoing' => [ + 'name' => ['stv', 'outgoing'] + ], + 'shared_studies' => [ + 'name' => ['stv', 'shared_studies'] + ] + ] + ] +]; From 4a283ebf5df5abaaa8cd20673b4e669b1e63b08e Mon Sep 17 00:00:00 2001 From: chfhtw Date: Thu, 25 Jun 2026 16:46:18 +0200 Subject: [PATCH 05/42] rename menu => treemenu --- .../api/frontend/v1/{Menu.php => Treemenu.php} | 2 +- public/js/api/factory/{menu.js => treemenu.js} | 2 +- .../components/Base/{Menu.js => Treemenu.js} | 18 +++++++++--------- .../Base/{Menu => Treemenu}/Entry.js | 2 +- .../Stv/Studentenverwaltung/Verband.js | 6 +++--- 5 files changed, 15 insertions(+), 15 deletions(-) rename application/controllers/api/frontend/v1/{Menu.php => Treemenu.php} (97%) rename public/js/api/factory/{menu.js => treemenu.js} (94%) rename public/js/components/Base/{Menu.js => Treemenu.js} (95%) rename public/js/components/Base/{Menu => Treemenu}/Entry.js (93%) diff --git a/application/controllers/api/frontend/v1/Menu.php b/application/controllers/api/frontend/v1/Treemenu.php similarity index 97% rename from application/controllers/api/frontend/v1/Menu.php rename to application/controllers/api/frontend/v1/Treemenu.php index 5ad97f403..fbaaa2c97 100644 --- a/application/controllers/api/frontend/v1/Menu.php +++ b/application/controllers/api/frontend/v1/Treemenu.php @@ -24,7 +24,7 @@ if (! defined('BASEPATH')) exit('No direct script access allowed'); * Provides data to the ajax get calls about menues * This controller works with JSON calls on the HTTP GET or POST and the output is always JSON */ -class Menu extends FHCAPI_Controller +class Treemenu extends FHCAPI_Controller { public function __construct() { diff --git a/public/js/api/factory/menu.js b/public/js/api/factory/treemenu.js similarity index 94% rename from public/js/api/factory/menu.js rename to public/js/api/factory/treemenu.js index d8b1b8635..e9c3764b0 100644 --- a/public/js/api/factory/menu.js +++ b/public/js/api/factory/treemenu.js @@ -19,7 +19,7 @@ export default { get(config, path = '') { return { method: 'get', - url: '/api/frontend/v1/menu/' + config + '/' + path + url: '/api/frontend/v1/treemenu/' + config + '/' + path }; }, // TODO(chris): handle favorites per config diff --git a/public/js/components/Base/Menu.js b/public/js/components/Base/Treemenu.js similarity index 95% rename from public/js/components/Base/Menu.js rename to public/js/components/Base/Treemenu.js index 64f1bc4c9..465b91ac2 100644 --- a/public/js/components/Base/Menu.js +++ b/public/js/components/Base/Treemenu.js @@ -1,14 +1,14 @@ -import MenuEntry from './Menu/Entry.js'; +import TreemenuEntry from './Treemenu/Entry.js'; import dragClick from '../../directives/dragClick.js'; -import ApiMenu from '../../api/factory/menu.js'; +import ApiTreemenu from '../../api/factory/treemenu.js'; export default { components: { PvTreetable: primevue.treetable, PvColumn: primevue.column, - MenuEntry + TreemenuEntry }, directives: { dragClick @@ -96,7 +96,7 @@ export default { this.loading = true; return this.$api - .call(ApiMenu.get(this.config, node.data.path)) + .call(ApiTreemenu.get(this.config, node.data.path)) .then(result => { const subNodes = result.data.map(this.mapResultToTreeData); const realNode = this.findNodeByKey(node.key); @@ -224,7 +224,7 @@ export default { filterFav() { this.favorites.on = !this.favorites.on; this.$api - .call(ApiMenu.favorites.set( + .call(ApiTreemenu.favorites.set( JSON.stringify(this.favorites) )); }, @@ -238,7 +238,7 @@ export default { } this.$api - .call(ApiMenu.favorites.set( + .call(ApiTreemenu.favorites.set( JSON.stringify(this.favorites) )); }, @@ -261,7 +261,7 @@ export default { }, mounted() { this.$api - .call(ApiMenu.get(this.config)) + .call(ApiTreemenu.get(this.config)) .then(result => { this.nodes = result.data.map(el => { el.root = true; @@ -273,7 +273,7 @@ export default { .catch(this.$fhcAlert.handleSystemError); this.$api - .call(ApiMenu.favorites.get()) + .call(ApiTreemenu.favorites.get()) .then(result => { if (result.data) { this.favorites = JSON.parse(result.data); @@ -316,7 +316,7 @@ export default {