From 4c6d645cc62bea6ae5cdbb8a2451940994a019e0 Mon Sep 17 00:00:00 2001 From: chfhtw Date: Tue, 18 Nov 2025 15:02:55 +0100 Subject: [PATCH 01/65] Studienverlauf verlinken im neuen FAS --- .../Stv/Studentenverwaltung/Details/Noten.js | 13 +++++++++++- system/phrasesupdate.php | 20 +++++++++++++++++++ 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/public/js/components/Stv/Studentenverwaltung/Details/Noten.js b/public/js/components/Stv/Studentenverwaltung/Details/Noten.js index 71b8096ec..817465879 100644 --- a/public/js/components/Stv/Studentenverwaltung/Details/Noten.js +++ b/public/js/components/Stv/Studentenverwaltung/Details/Noten.js @@ -25,6 +25,11 @@ export default { stdsem: '' }; }, + computed: { + gradeListLink() { + return this.$api.getUri('/person/gradelist/index/' + this.modelValue.uid); + } + }, methods: { reload() { this.$refs.zeugnis.$refs.table.reloadTable(); @@ -39,8 +44,14 @@ export default { const savedPath = window.localStorage.getItem(LOCAL_STORAGE_ID); this.stdsem = savedPath || ''; }, - template: ` + template: /* html */`
+
+
+ + + + +
+ + - + + + + diff --git a/system/phrasesupdate.php b/system/phrasesupdate.php index 5f6269365..d32821125 100644 --- a/system/phrasesupdate.php +++ b/system/phrasesupdate.php @@ -58177,6 +58177,26 @@ I have been informed that I am under no obligation to consent to the transmissio ) ), // ### Phrases Dashboard Admin END + array( + 'app' => 'core', + 'category' => 'studierendenantrag', + 'phrase' => 'link_Abmeldung', + 'insertvon' => 'system', + 'phrases' => array( + array( + 'sprache' => 'German', + 'text' => 'Weiter zur Abmeldung', + 'description' => '', + 'insertvon' => 'system' + ), + array( + 'sprache' => 'English', + 'text' => 'Continue to De-registration', + 'description' => '', + 'insertvon' => 'system' + ) + ) + ), ); From b9c045382db230272ace8cc609a67a9d264d9476 Mon Sep 17 00:00:00 2001 From: ma0068 Date: Wed, 20 May 2026 09:15:15 +0200 Subject: [PATCH 04/65] add Hinweistext for Modal Hinweis Beratungsgespraech --- .../views/lehre/Antrag/Student/List.php | 27 ++++------- system/phrasesupdate.php | 46 +++++++++++++++++++ 2 files changed, 54 insertions(+), 19 deletions(-) diff --git a/application/views/lehre/Antrag/Student/List.php b/application/views/lehre/Antrag/Student/List.php index 3c360cd0c..81b24996d 100644 --- a/application/views/lehre/Antrag/Student/List.php +++ b/application/views/lehre/Antrag/Student/List.php @@ -50,18 +50,6 @@ if(defined('CIS4')){ - - -

p->t('studierendenantrag', 'calltoaction_' . $type); ?>


@@ -97,18 +85,19 @@ if(defined('CIS4')){
+
@@ -457,6 +458,7 @@ export default { :tabulator-options="betriebsmittel_table_options" tableOnly :sideMenu="false" + :isUsingPresets="true" />
diff --git a/public/js/components/filter/Filter.js b/public/js/components/filter/Filter.js index 5f0447aea..3027ef377 100644 --- a/public/js/components/filter/Filter.js +++ b/public/js/components/filter/Filter.js @@ -18,6 +18,7 @@ import {CoreFetchCmpt} from '../../components/Fetch.js'; import FilterConfig from './Filter/Config.js'; import FilterColumns from './Filter/Columns.js'; +import TablePresets from './Filter/TablePresets.js'; import TableDownload from './Table/Download.js'; import collapseAutoClose from '../../directives/collapseAutoClose.js'; @@ -45,7 +46,8 @@ export const CoreFilterCmpt = { CoreFetchCmpt, FilterConfig, FilterColumns, - TableDownload + TablePresets, + TableDownload, }, directives: { collapseAutoClose @@ -91,6 +93,10 @@ export const CoreFilterCmpt = { type: Boolean, default: true }, + isUsingPresets: { + Boolean, + default: false, + }, }, inject: ["language"], data: function() { @@ -841,7 +847,7 @@ export const CoreFilterCmpt = { }); }, - template: ` + template: /*html*/ ` + + + + + . + */ + +/** + * + */ +export default { + name: "TablePresets", + props: ["identifier", "tabulator"], + emits: {}, + data: function () { + return { + // todo: remove once actual data is fetched + testDataForUIDev: [ + { + id: null, + name: "Test Preset 1", + displayedColumns: { + testCol1: true, + testCol2: false, + testCol3: true, + testCol4: false, + testCol5: true, + }, + filters: { + testCol3: "adis", + testCol5: "01.06.2026.-31.12.2026.", + }, + columnsOrder: ["testCol3", "testCol5", "testCol1"], + sort: { + column: "testCol1", + direction: "asc", + }, + isUserCustom: false, + }, + { + id: 1, + name: "Test Preset 2", + displayedColumns: { + testCol1: true, + testCol2: true, + testCol3: true, + testCol4: false, + testCol5: false, + }, + filters: { + testCol1: "posko", + }, + columnsOrder: ["testCol1", "testCol3", "testCol2"], + sort: { + column: "testCol3", + direction: "desc", + }, + }, + ], + presetInfo: null, + }; + }, + computed: { + presetInfoFormattedStrings() { + if (!this.presetInfo) return null; + + const displayedColumns = Object.keys(this.presetInfo.displayedColumns).filter((column) => this.presetInfo.displayedColumns[column]); + const filters = Object.keys(this.presetInfo.filters).map((column) => column + ": \"" + this.presetInfo.filters[column] + "\""); + return { + displayedColumns: "Displayed columns: " + displayedColumns.join(", "), + columnsOrder: "Columns ordering: " + this.presetInfo.columnsOrder.join(", "), + filters: "Column filters: " + filters.join(", "), + sort: "Sort: " + this.presetInfo.sort.column + " (" + (this.presetInfo.sort.direction === "asc" ? "ascending" : "descending") + ")", + }; + }, + }, + watch: { + // todo: watch presets, once they change nullify presetInfo and collapse preset info section + }, + methods: { + async showPresetInfo(preset) { + if (preset !== this.presetInfo) { + this.presetInfo = preset; + } + await this.$nextTick(); + if (!this.$refs.presetInfoCollapsible.getAttribute("class").split(" ").includes("show")) { + this.togglePresetInfoCollapsible(); + } + }, + hidePresetInfo() { + this.presetInfo = null; + this.togglePresetInfoCollapsible(); + }, + togglePresetInfoCollapsible() { + new bootstrap.Collapse("#presetInfoCollapsible" + this.$props.identifier, {toggle: true}); + }, + deletePreset(preset) { + // todo + }, + showNewPresetForm() { + // todo + if (!this.$refs.newPresetFormCollapsible.getAttribute("class").split(" ").includes("show")) { + this.toggleNewPresetFormCollapsible(); + } + + }, + toggleNewPresetFormCollapsible() { + new bootstrap.Collapse("#newPresetFormCollapsible" + this.$props.identifier, {toggle: true}); + }, + applyPreset() { + // todo + }, + }, + template: /*html*/ ` +
+
+
Table Presets
+
+
+
+
{{ preset.name }}
+
+
+ + +
+
+
+
+
+ + {{ "Save current configuration" }} +
+
+
+
+
+
+
+
+ {{ presetInfo?.name }} + +
+ {{ presetInfoFormattedStrings?.displayedColumns }} + {{ presetInfoFormattedStrings?.columnsOrder }} + {{ presetInfoFormattedStrings?.filters }} + {{ presetInfoFormattedStrings?.sort }} +
+
+
+
+ +
+
+
+
+ {{ "Save current configuration" }} + +
+ adis +
+
+
+
+
+
+ `, +}; From 0bb81b23e55d46b6623de880f53cacb935cedc97 Mon Sep 17 00:00:00 2001 From: adisposkofh Date: Tue, 9 Jun 2026 16:07:55 +0200 Subject: [PATCH 07/65] implemented table presets functionality --- .../api/frontend/v1/TabulatorPresets.php | 124 ++++++ .../tabulator/Tabulator_preset_model.php | 76 ++++ public/js/api/factory/tabulatorPresets.js | 48 +++ .../Cis/Profil/MitarbeiterProfil.js | 1 - public/js/components/filter/Filter.js | 17 +- .../components/filter/Filter/TablePresets.js | 373 ++++++++++++++---- system/dbupdate_3.4.php | 2 + .../77080_tabulator_presets_table.php | 55 +++ 8 files changed, 619 insertions(+), 77 deletions(-) create mode 100644 application/controllers/api/frontend/v1/TabulatorPresets.php create mode 100644 application/models/tabulator/Tabulator_preset_model.php create mode 100644 public/js/api/factory/tabulatorPresets.js create mode 100644 system/dbupdate_3.4/77080_tabulator_presets_table.php diff --git a/application/controllers/api/frontend/v1/TabulatorPresets.php b/application/controllers/api/frontend/v1/TabulatorPresets.php new file mode 100644 index 000000000..05de9bde7 --- /dev/null +++ b/application/controllers/api/frontend/v1/TabulatorPresets.php @@ -0,0 +1,124 @@ +. + */ + +if (! defined('BASEPATH')) exit('No direct script access allowed'); + +/** + * This controller operates between (interface) the JS (GUI) and the PhrasesLib (back-end) + * Provides data to the ajax get calls about the Phrasen plugin + * This controller works with JSON calls on the HTTP GET and the output is always JSON + */ +class TabulatorPresets extends FHCAPI_Controller +{ + public function __construct() + { + parent::__construct([ + 'getTabulatorPresets' => self::PERM_LOGGED, + 'createTabulatorPreset' => self::PERM_LOGGED, + 'deleteTabulatorPreset' => self::PERM_LOGGED, + ]); + + $this->load->model('tabulator/Tabulator_preset_model', 'TabulatorPresetModel'); + } + + //------------------------------------------------------------------------------------------------------------------ + // Public methods + + public function getTabulatorPresets() + { + $tableName = $this->input->get("tableName"); + if (!$tableName) { + $this->terminateWithError(error("Invalid parameters!")); + } + + $uid = getAuthUID(); + $presets = $this->TabulatorPresetModel->getTabulatorPresetsByUserAndTable($uid, $tableName); + $presetsData = $this->getDataOrTerminateWithError($presets) ?? []; + + $this->terminateWithSuccess($presetsData); + } + + public function createTabulatorPreset() + { + $tableName = $this->input->post("tableName"); + $presetName = $this->input->post("presetName"); + $preset = $this->input->post("preset"); + + if (!$tableName || !$presetName || !$preset) { + $this->terminateWithError("Invalid parameters!", "general", 400); + } + + $uid = getAuthUID(); + + $existingPresetsResult = $this->TabulatorPresetModel->getTabulatorPresetsByUserAndTable($uid, $tableName); + $existingPresets = $this->getDataOrTerminateWithError($existingPresetsResult) ?? []; + + if (count($existingPresets) > 19) { + $this->terminateWithError("Maximum number of presets reached!", "general", 409); + } + + $existingPresetNames = array_map(function($presetInfo) { + return $presetInfo->preset_name; + }, $existingPresets); + + if (in_array($presetName, $existingPresetNames)) { + $this->terminateWithError("Preset name duplicate not allowed!", "general", 409); + } + + $presetCreationResult = $this->TabulatorPresetModel->createTabulatorPreset($uid, $tableName, $presetName, $preset); + + if (isError($presetCreationResult)) { + $this->terminateWithError($presetCreationResult->retval); + } + + $newPresetData = $this->TabulatorPresetModel->getTabulatorPreset($presetCreationResult->retval); + $newPresetArray = $this->getDataOrTerminateWithError($newPresetData) ?? [null]; + $newPreset = $newPresetArray[0]; + + $this->terminateWithSuccess($newPreset); + } + + public function deleteTabulatorPreset() + { + $presetId = $this->input->post("presetId"); + if (!$presetId) { + $this->terminateWithError("Invalid parameters!", "general", 400); + } + + $presetData = $this->TabulatorPresetModel->getTabulatorPreset($presetId); + $presetArray = $this->getDataOrTerminateWithError($presetData) ?? [null]; + $preset = $presetArray[0]; + if (!$preset) { + $this->terminateWithError("Preset not found!", "general", 404); + } + + $uid = getAuthUID(); + $this->addMeta("preset", $preset); + if ($preset->benutzer_uid !== $uid) { + $this->terminateWithError("You are not allowed to delete this preset!", "general", 403); + } + + $presetDeletionResult = $this->TabulatorPresetModel->deleteTabulatorPreset($presetId); + if (isError($presetDeletionResult)) { + $this->terminateWithError($presetDeletionResult->retval); + } + + $this->terminateWithSuccess(); + } + +} \ No newline at end of file diff --git a/application/models/tabulator/Tabulator_preset_model.php b/application/models/tabulator/Tabulator_preset_model.php new file mode 100644 index 000000000..7a3de5491 --- /dev/null +++ b/application/models/tabulator/Tabulator_preset_model.php @@ -0,0 +1,76 @@ +dbTable = 'public.tbl_tabulator_presets'; + $this->pk = ['preset_id']; + $this->hasSequence = true; + + + $this->load->model('person/Benutzer_model', 'BenutzerModel'); + } + + public function getTabulatorPreset($presetId) + { + $query = " + SELECT * + FROM public.tbl_tabulator_presets + WHERE preset_id = ? + LIMIT 1 + "; + return $this->execQuery($query, array($presetId)); + } + + public function getTabulatorPresetsByUserAndTable($uid, $tableName) + { + $query = ' + SELECT * + FROM public.tbl_tabulator_presets + WHERE benutzer_uid = ? + AND table_name = ? + ORDER BY preset_id ASC + '; + return $this->execQuery($query, array($uid, $tableName)); + } + + public function createTabulatorPreset($uid, $tableName, $presetName, $preset) + { + $this->db->trans_start(false); + + $result = $this->insert([ + "benutzer_uid" => $uid, + "table_name" => $tableName, + "preset_name" => $presetName, + "preset_json" => json_encode($preset), + ]); + + $this->db->trans_complete(); + + if ($this->db->trans_status() === false) + { + $this->db->trans_rollback(); + return error('Something went wrong with tabulator preset creation.'); + } + + return success($result->retval); + } + + public function deleteTabulatorPreset($presetId) + { + $presetDeletionReseult = $this->delete(["preset_id" => $presetId]); + + if (isError($presetDeletionReseult)) + { + return error('Something went wrong during tabulator preset deletion.'); + } + + return success($presetDeletionReseult->retval); + } + +} diff --git a/public/js/api/factory/tabulatorPresets.js b/public/js/api/factory/tabulatorPresets.js new file mode 100644 index 000000000..1ee32550e --- /dev/null +++ b/public/js/api/factory/tabulatorPresets.js @@ -0,0 +1,48 @@ +/** + * Copyright (C) 2025 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 { + getTabulatorPresets(tableName) { + return { + method: "get", + url: + "/api/frontend/v1/TabulatorPresets/getTabulatorPresets?tableName=" + + tableName, + }; + }, + createTabulatorPreset(tableName, presetName, preset) { + return { + method: "post", + url: "/api/frontend/v1/TabulatorPresets/createTabulatorPreset", + params: { + tableName, + presetName, + preset, + }, + }; + }, + deleteTabulatorPreset(tabulatorPresetId) { + return { + method: "post", + url: + "/api/frontend/v1/TabulatorPresets/deleteTabulatorPreset", + params: { + presetId: tabulatorPresetId, + }, + }; + }, +}; diff --git a/public/js/components/Cis/Profil/MitarbeiterProfil.js b/public/js/components/Cis/Profil/MitarbeiterProfil.js index 4d719fcc1..7095380c3 100644 --- a/public/js/components/Cis/Profil/MitarbeiterProfil.js +++ b/public/js/components/Cis/Profil/MitarbeiterProfil.js @@ -446,7 +446,6 @@ export default { :tabulator-options="funktionen_table_options" tableOnly :sideMenu="false" - :isUsingPresets="true" />
diff --git a/public/js/components/filter/Filter.js b/public/js/components/filter/Filter.js index 3027ef377..9b21243dd 100644 --- a/public/js/components/filter/Filter.js +++ b/public/js/components/filter/Filter.js @@ -97,6 +97,14 @@ export const CoreFilterCmpt = { Boolean, default: false, }, + presetsId: { + String, + default: "", + }, + generalPresets: { + type: Array, + default: [] + }, }, inject: ["language"], data: function() { @@ -823,6 +831,9 @@ export const CoreFilterCmpt = { return res; }, {}); }, + afterTablePresetApplied(preset) { + this.selectedFields = preset.displayedColumns; + }, }, beforeCreate() { if (!this.tableOnly == !this.filterType) @@ -894,7 +905,7 @@ export const CoreFilterCmpt = { - + @@ -902,10 +913,12 @@ export const CoreFilterCmpt = {
diff --git a/public/js/components/filter/Filter/TablePresets.js b/public/js/components/filter/Filter/TablePresets.js index d5fcb2aa4..d47e223f0 100644 --- a/public/js/components/filter/Filter/TablePresets.js +++ b/public/js/components/filter/Filter/TablePresets.js @@ -15,111 +15,332 @@ * along with this program. If not, see . */ +import ApiTabulatorPresets from "../../../api/factory/tabulatorPresets.js"; + /** * */ export default { name: "TablePresets", - props: ["identifier", "tabulator"], - emits: {}, + props: { + presetsId: { type: String }, + tabulator: { type: Object }, + generalPresets: { type: Array, default: [] }, + }, + emits: ["applyTablePreset"], data: function () { return { - // todo: remove once actual data is fetched - testDataForUIDev: [ - { - id: null, - name: "Test Preset 1", - displayedColumns: { - testCol1: true, - testCol2: false, - testCol3: true, - testCol4: false, - testCol5: true, - }, - filters: { - testCol3: "adis", - testCol5: "01.06.2026.-31.12.2026.", - }, - columnsOrder: ["testCol3", "testCol5", "testCol1"], - sort: { - column: "testCol1", - direction: "asc", - }, - isUserCustom: false, - }, - { - id: 1, - name: "Test Preset 2", - displayedColumns: { - testCol1: true, - testCol2: true, - testCol3: true, - testCol4: false, - testCol5: false, - }, - filters: { - testCol1: "posko", - }, - columnsOrder: ["testCol1", "testCol3", "testCol2"], - sort: { - column: "testCol3", - direction: "desc", - }, - }, - ], + customUserPresets: [], presetInfo: null, + newPreset: null, + newPresetName: "", }; }, computed: { + allPresets() { + return this.$props.generalPresets.concat(this.customUserPresets); + }, presetInfoFormattedStrings() { if (!this.presetInfo) return null; - const displayedColumns = Object.keys(this.presetInfo.displayedColumns).filter((column) => this.presetInfo.displayedColumns[column]); - const filters = Object.keys(this.presetInfo.filters).map((column) => column + ": \"" + this.presetInfo.filters[column] + "\""); + const columnTitles = this.getColumnTitles(); + + const displayedColumns = this.presetInfo.displayedColumns.map( + (columnField) => columnTitles[columnField], + ); + + const headerFilters = Object.keys( + this.presetInfo.headerFilters, + ).map( + (column) => + columnTitles[column] + + '["' + + this.presetInfo.headerFilters[column] + + '"]', + ); return { - displayedColumns: "Displayed columns: " + displayedColumns.join(", "), - columnsOrder: "Columns ordering: " + this.presetInfo.columnsOrder.join(", "), - filters: "Column filters: " + filters.join(", "), - sort: "Sort: " + this.presetInfo.sort.column + " (" + (this.presetInfo.sort.direction === "asc" ? "ascending" : "descending") + ")", + displayedColumns: + "Displayed columns (in order): " + + displayedColumns.join(", "), + headerFilters: + "Column header filters: " + headerFilters.join(", "), + sort: + "Sort: " + + columnTitles[this.presetInfo.sort.column] + + " (" + + (this.presetInfo.sort.direction === "asc" + ? "ascending" + : "descending") + + ")", + }; + }, + newPresetFormattedStrings() { + if (!this.newPreset) return null; + + const columnTitles = this.getColumnTitles(); + + const displayedColumns = this.newPreset.displayedColumns.map( + (columnField) => columnTitles[columnField], + ); + + const headerFilters = Object.keys(this.newPreset.headerFilters).map( + (column) => + columnTitles[column] + + '["' + + this.newPreset.headerFilters[column] + + '"]', + ); + return { + displayedColumns: + "Displayed columns (in order): " + + displayedColumns.join(", "), + headerFilters: + "Column header filters: " + headerFilters.join(", "), + sort: + "Sort: " + + (this.newPreset.sort + ? columnTitles[this.newPreset.sort.column] + + " (" + + (this.newPreset.sort.direction === "asc" + ? "ascending" + : "descending") + + ")" + : ""), }; }, }, watch: { - // todo: watch presets, once they change nullify presetInfo and collapse preset info section + allPresets: { + handler() { + this.hidePresetInfo(); + this.hideNewPresetForm(); + }, + deep: true, + }, }, methods: { async showPresetInfo(preset) { if (preset !== this.presetInfo) { this.presetInfo = preset; } + + this.hideNewPresetForm(); + await this.$nextTick(); - if (!this.$refs.presetInfoCollapsible.getAttribute("class").split(" ").includes("show")) { + + if ( + !this.$refs.presetInfoCollapsible + .getAttribute("class") + .split(" ") + .includes("show") + ) { this.togglePresetInfoCollapsible(); } }, - hidePresetInfo() { + async hidePresetInfo() { + if ( + this.$refs.presetInfoCollapsible + .getAttribute("class") + .split(" ") + .includes("show") + ) { + this.togglePresetInfoCollapsible(); + } + await this.$nextTick(); this.presetInfo = null; - this.togglePresetInfoCollapsible(); }, togglePresetInfoCollapsible() { - new bootstrap.Collapse("#presetInfoCollapsible" + this.$props.identifier, {toggle: true}); + new bootstrap.Collapse( + "#presetInfoCollapsible_" + this.$props.presetsId, + { toggle: true }, + ); }, - deletePreset(preset) { - // todo - }, - showNewPresetForm() { - // todo - if (!this.$refs.newPresetFormCollapsible.getAttribute("class").split(" ").includes("show")) { + async showNewPresetForm() { + this.generateNewPreset(); + + this.hidePresetInfo(); + + await this.$nextTick(); + + if ( + !this.$refs.newPresetFormCollapsible + .getAttribute("class") + .split(" ") + .includes("show") + ) { this.toggleNewPresetFormCollapsible(); } + }, + generateNewPreset() { + const columns = this.tabulator + .getColumnDefinitions() + .filter((column) => column.field !== "collapse"); + const displayedColumns = columns + .filter((column) => column.visible) + .map((column) => column.field); + + const unparsedHeaderFilters = + this.tabulator.modules.filter.headerFilters; + const headerFilterFieldValuePairs = Object.entries( + unparsedHeaderFilters, + ).map(([columnField, filter]) => [columnField, filter.value]); + const headerFiltersFieldValuePairsExcludingHiddenColumns = + headerFilterFieldValuePairs.filter( + ([columnField, filterValue]) => { + return displayedColumns.includes(columnField); + }, + ); + const headerFilters = Object.fromEntries( + headerFiltersFieldValuePairsExcludingHiddenColumns, + ); + + const activeSort = this.tabulator.getSorters()[0]; + const sort = activeSort + ? { + column: activeSort.field, + direction: activeSort.dir, + } + : null; + + this.newPreset = { + displayedColumns, + headerFilters, + sort, + }; + }, + async hideNewPresetForm() { + if ( + this.$refs.newPresetFormCollapsible + .getAttribute("class") + .split(" ") + .includes("show") + ) { + this.toggleNewPresetFormCollapsible(); + } }, toggleNewPresetFormCollapsible() { - new bootstrap.Collapse("#newPresetFormCollapsible" + this.$props.identifier, {toggle: true}); + this.newPresetName = ""; + new bootstrap.Collapse( + "#newPresetFormCollapsible_" + this.$props.presetsId, + { toggle: true }, + ); }, - applyPreset() { + async createPreset() { + if (!this.newPresetName?.length) return; + + const presetCreationResponse = await this.$api.call( + ApiTabulatorPresets.createTabulatorPreset( + this.$props.presetsId, + this.newPresetName, + this.newPreset, + ), + ); + + if (presetCreationResponse.meta.status === "success") { + this.fetchCustomUserTabulatorPresets(); + this.hideNewPresetForm(); + } + }, + async deletePreset(preset) { + if (!preset.id) return; + + if ( + window.confirm( + "Are you sure you want to delete preset (((presetName)))?".replace( + "(((presetName)))", + preset.name, + ), + ) + ) { + const presetDeletionResponse = await this.$api.call( + ApiTabulatorPresets.deleteTabulatorPreset(preset.id), + ); + + if (presetDeletionResponse.meta.status === "success") { + this.fetchCustomUserTabulatorPresets(); + } + } + }, + async syncPresetWithConfig(preset) { // todo + return preset; }, + async applyPreset(preset) { + preset = await this.syncPresetWithConfig(preset); + + console.log(this.$props.tabulator); + let columns = this.$props.tabulator + .getColumnDefinitions() + .filter((column) => column.field !== "collapse"); + columns.forEach((column) => + this.$props.tabulator.hideColumn(column.field), + ); + preset.displayedColumns.forEach( + (columnField, index, displayedColumns) => { + this.$props.tabulator.showColumn(columnField); + if (index === 0) return; + this.$props.tabulator.moveColumn( + columnField, + displayedColumns[index - 1], + true, + ); + }, + ); + + this.$props.tabulator.clearHeaderFilter(); + Object.entries(preset.headerFilters).forEach( + ([columnField, filterValue]) => { + this.$props.tabulator.setHeaderFilterValue( + columnField, + filterValue, + ); + }, + ); + + this.$props.tabulator.clearSort(); + if (preset.sort?.column) { + this.$props.tabulator.setSort( + preset.sort.column, + preset.sort.direction ?? "asc", + ); + } + + this.$emit("tablePresetApplied", { preset }); + }, + async fetchCustomUserTabulatorPresets() { + let tabulatorPresetsResponse = await this.$api.call( + ApiTabulatorPresets.getTabulatorPresets(this.$props.presetsId), + ); + + if (tabulatorPresetsResponse.meta.status === "success") { + this.customUserPresets = tabulatorPresetsResponse.data.map( + (presetInfo) => { + return { + id: presetInfo.preset_id, + name: presetInfo.preset_name, + ...JSON.parse(presetInfo.preset_json), + }; + }, + ); + } + }, + getColumnTitles() { + const columnLocalizations = + this.$props.tabulator.getLocale() !== "default" + ? this.$props.tabulator.getLang().columns + : {}; + const columnFieldTitlePairs = this.$props.tabulator + .getColumnDefinitions() + .map((column) => [ + column.field, + columnLocalizations[column.field] ?? column.title, + ]); + return Object.fromEntries(columnFieldTitlePairs); + }, + }, + async created() { + await this.fetchCustomUserTabulatorPresets(); }, template: /*html*/ `
@@ -127,10 +348,10 @@ export default {
Table Presets
-
-
{{ preset.name }}
+
+
{{ preset.name }}
-
+
@@ -143,7 +364,7 @@ export default {
-
+

@@ -152,22 +373,26 @@ export default {
{{ presetInfoFormattedStrings?.displayedColumns }} - {{ presetInfoFormattedStrings?.columnsOrder }} - {{ presetInfoFormattedStrings?.filters }} + {{ presetInfoFormattedStrings?.headerFilters }} {{ presetInfoFormattedStrings?.sort }}
-
- +

{{ "Save current configuration" }} - + +
+ {{ newPresetFormattedStrings?.displayedColumns }} + {{ newPresetFormattedStrings?.headerFilters }} + {{ newPresetFormattedStrings?.sort }} +
+ +
{{ "Save" }}
- adis
diff --git a/system/dbupdate_3.4.php b/system/dbupdate_3.4.php index 8b6af9f2d..95714b303 100644 --- a/system/dbupdate_3.4.php +++ b/system/dbupdate_3.4.php @@ -94,6 +94,7 @@ require_once('dbupdate_3.4/71399_dashboard_update_widget_paths.php'); require_once('dbupdate_3.4/71645_studvw_messagetab_ladezeit.php'); require_once('dbupdate_3.4/71566_studienordnungsdokument_neuer_organisationseinheitstyp_programm.php'); require_once('dbupdate_3.4/70376_lohnguide.php'); +require_once('dbupdate_3.4/77080_tabulator_presets_table.php'); // *** Pruefung und hinzufuegen der neuen Attribute und Tabellen echo '

Pruefe Tabellen und Attribute!

'; @@ -391,6 +392,7 @@ $tabellen=array( "public.tbl_studiengangstyp" => array("typ","bezeichnung","beschreibung","bezeichnung_mehrsprachig"), "public.tbl_studienjahr" => array("studienjahr_kurzbz","bezeichnung"), "public.tbl_studiensemester" => array("studiensemester_kurzbz","bezeichnung","start","ende","studienjahr_kurzbz","ext_id","beschreibung","onlinebewerbung"), + "public.tbl_tabulator_presets" => array("preset_id", "benutzer_uid", "table_name", "preset_json"), "public.tbl_tag" => array("tag"), "public.tbl_variable" => array("name","uid","wert"), "public.tbl_variablenname" => array("name","defaultwert"), diff --git a/system/dbupdate_3.4/77080_tabulator_presets_table.php b/system/dbupdate_3.4/77080_tabulator_presets_table.php new file mode 100644 index 000000000..bef240b17 --- /dev/null +++ b/system/dbupdate_3.4/77080_tabulator_presets_table.php @@ -0,0 +1,55 @@ +db_query("SELECT 1 FROM public.tbl_tabulator_presets LIMIT 1")) { + $qry = "CREATE TABLE public.tbl_tabulator_presets ( + preset_id int NOT NULL, + benutzer_uid varchar(33) NOT NULL, + table_name varchar(255) NOT NULL, + preset_name varchar(255) NOT NULL, + preset_json varchar(10000) NOT NULL, + CONSTRAINT tbl_tabulator_presets_preset_id_pk PRIMARY KEY(preset_id), + CONSTRAINT tbl_tabulator_presets_benutzer_uid_fk FOREIGN KEY (benutzer_uid) REFERENCES public.tbl_benutzer(uid) + );"; + + if (!$db->db_query($qry)) + echo 'public.tbl_tabulator_presets: ' . $db->db_last_error() . '
'; + else + echo '
public.tbl_tabulator_presets: table created'; + + + $db->db_query('CREATE SEQUENCE IF NOT EXISTS public.seq_tbl_tabulator_presets_preset_id + INCREMENT BY 1 + NO MAXVALUE + NO MINVALUE + CACHE 1;'); + + $db->db_query("ALTER TABLE public.tbl_tabulator_presets ALTER COLUMN preset_id SET DEFAULT nextval('public.seq_tbl_tabulator_presets_preset_id');"); + + + $qry = 'GRANT SELECT, UPDATE, INSERT, DELETE ON TABLE public.tbl_tabulator_presets TO web;'; + if (!$db->db_query($qry)) + echo 'public.tbl_tabulator_presets: ' . $db->db_last_error() . '
'; + else + echo '
Granted privileges to web on public.tbl_tabulator_presets'; + + $qry = 'GRANT SELECT, UPDATE, INSERT, DELETE ON TABLE public.tbl_tabulator_presets TO vilesci;'; + if (!$db->db_query($qry)) + echo 'public.tbl_tabulator_presets: ' . $db->db_last_error() . '
'; + else + echo '
Granted privileges to vilesci on public.tbl_tabulator_presets'; + + $qry = 'GRANT USAGE ON public.seq_tbl_tabulator_presets_preset_id TO web;'; + if (!$db->db_query($qry)) + echo 'public.tbl_tabulator_presets: ' . $db->db_last_error() . '
'; + else + echo '
Granted privileges to web on public.tbl_tabulator_presets'; + + $qry = 'GRANT USAGE ON public.seq_tbl_tabulator_presets_preset_id TO vilesci;'; + if (!$db->db_query($qry)) + echo 'public.tbl_tabulator_presets: ' . $db->db_last_error() . '
'; + else + echo '
Granted privileges to vilesci on public.tbl_tabulator_presets'; +} From 1d0ca15bcb53905d54a775b27085e4baa41339d1 Mon Sep 17 00:00:00 2001 From: adisposkofh Date: Tue, 9 Jun 2026 17:02:02 +0200 Subject: [PATCH 08/65] passing generalPresets prop from filter to table presets --- public/js/components/filter/Filter.js | 1 + 1 file changed, 1 insertion(+) diff --git a/public/js/components/filter/Filter.js b/public/js/components/filter/Filter.js index 9b21243dd..1eeea5e20 100644 --- a/public/js/components/filter/Filter.js +++ b/public/js/components/filter/Filter.js @@ -920,6 +920,7 @@ export const CoreFilterCmpt = { :data-bs-parent="'#filterCollapsables' + idExtra" :presetsId="$props.presetsId" :tabulator="tabulator" + :generalPresets="$props.generalPresets" class="card-body collapse" > From d2c97d0238a3b1cbcbe3311fb4c2856d44f25aaf Mon Sep 17 00:00:00 2001 From: adisposkofh Date: Wed, 10 Jun 2026 14:43:20 +0200 Subject: [PATCH 09/65] check to sync table preset with modified config --- .../api/frontend/v1/TabulatorPresets.php | 35 +++- .../tabulator/Tabulator_preset_model.php | 23 ++- public/js/api/factory/tabulatorPresets.js | 10 ++ .../components/filter/Filter/TablePresets.js | 155 ++++++++++++++---- 4 files changed, 180 insertions(+), 43 deletions(-) diff --git a/application/controllers/api/frontend/v1/TabulatorPresets.php b/application/controllers/api/frontend/v1/TabulatorPresets.php index 05de9bde7..e8c27aed6 100644 --- a/application/controllers/api/frontend/v1/TabulatorPresets.php +++ b/application/controllers/api/frontend/v1/TabulatorPresets.php @@ -30,6 +30,7 @@ class TabulatorPresets extends FHCAPI_Controller parent::__construct([ 'getTabulatorPresets' => self::PERM_LOGGED, 'createTabulatorPreset' => self::PERM_LOGGED, + 'updateTabulatorPreset' => self::PERM_LOGGED, 'deleteTabulatorPreset' => self::PERM_LOGGED, ]); @@ -93,6 +94,39 @@ class TabulatorPresets extends FHCAPI_Controller $this->terminateWithSuccess($newPreset); } + public function updateTabulatorPreset() + { + $presetId = $this->input->post("presetId"); + $preset = $this->input->post("preset"); + + if (!$presetId || !$preset) { + $this->terminateWithError("Invalid parameters!", "general", 400); + } + + $existingPresetData = $this->TabulatorPresetModel->getTabulatorPreset($presetId); + $existingPresetArray = $this->getDataOrTerminateWithError($existingPresetData) ?? [null]; + $existingPreset = $existingPresetArray[0]; + if (!$existingPreset) { + $this->terminateWithError("Preset not found!", "general", 404); + } + + $uid = getAuthUID(); + if ($existingPreset->benutzer_uid !== $uid) { + $this->terminateWithError("You are not allowed to delete this preset!", "general", 403); + } + + $presetUpdateResult = $this->TabulatorPresetModel->updateTabulatorPreset($presetId, $preset); + if (isError($presetUpdateResult)) { + $this->terminateWithError($presetUpdateResult->retval); + } + + $updatedPresetData = $this->TabulatorPresetModel->getTabulatorPreset($presetId); + $updatedPresetArray = $this->getDataOrTerminateWithError($updatedPresetData) ?? [null]; + $updatedPreset = $updatedPresetArray[0]; + + $this->terminateWithSuccess($updatedPreset); + } + public function deleteTabulatorPreset() { $presetId = $this->input->post("presetId"); @@ -108,7 +142,6 @@ class TabulatorPresets extends FHCAPI_Controller } $uid = getAuthUID(); - $this->addMeta("preset", $preset); if ($preset->benutzer_uid !== $uid) { $this->terminateWithError("You are not allowed to delete this preset!", "general", 403); } diff --git a/application/models/tabulator/Tabulator_preset_model.php b/application/models/tabulator/Tabulator_preset_model.php index 7a3de5491..209420069 100644 --- a/application/models/tabulator/Tabulator_preset_model.php +++ b/application/models/tabulator/Tabulator_preset_model.php @@ -41,24 +41,31 @@ class Tabulator_preset_model extends DB_Model public function createTabulatorPreset($uid, $tableName, $presetName, $preset) { - $this->db->trans_start(false); - - $result = $this->insert([ + $presetCreationResult = $this->insert([ "benutzer_uid" => $uid, "table_name" => $tableName, "preset_name" => $presetName, "preset_json" => json_encode($preset), ]); - - $this->db->trans_complete(); - if ($this->db->trans_status() === false) + if (isError($presetCreationResult)) { - $this->db->trans_rollback(); return error('Something went wrong with tabulator preset creation.'); } - return success($result->retval); + return success($presetCreationResult->retval); + } + + public function updateTabulatorPreset($presetId, $preset) + { + $presetUpdateResult = $this->update(["preset_id" => $presetId], ["preset_json" => json_encode($preset)]); + + if (isError($presetUpdateResult)) + { + return error('Something went wrong during tabulator preset update.'); + } + + return success($presetUpdateResult->retval); } public function deleteTabulatorPreset($presetId) diff --git a/public/js/api/factory/tabulatorPresets.js b/public/js/api/factory/tabulatorPresets.js index 1ee32550e..1653ccb40 100644 --- a/public/js/api/factory/tabulatorPresets.js +++ b/public/js/api/factory/tabulatorPresets.js @@ -35,6 +35,16 @@ export default { }, }; }, + updateTabulatorPreset(presetId, preset) { + return { + method: "post", + url: "/api/frontend/v1/TabulatorPresets/updateTabulatorPreset", + params: { + presetId, + preset, + }, + }; + }, deleteTabulatorPreset(tabulatorPresetId) { return { method: "post", diff --git a/public/js/components/filter/Filter/TablePresets.js b/public/js/components/filter/Filter/TablePresets.js index d47e223f0..132f7954f 100644 --- a/public/js/components/filter/Filter/TablePresets.js +++ b/public/js/components/filter/Filter/TablePresets.js @@ -45,19 +45,26 @@ export default { const columnTitles = this.getColumnTitles(); - const displayedColumns = this.presetInfo.displayedColumns.map( - (columnField) => columnTitles[columnField], + let displayedColumns = + this.presetInfo.displayedColumns?.map( + (columnField) => columnTitles[columnField] ?? "", + ) ?? []; + displayedColumns = displayedColumns.filter( + (string) => string.length, ); - const headerFilters = Object.keys( - this.presetInfo.headerFilters, - ).map( - (column) => - columnTitles[column] + - '["' + - this.presetInfo.headerFilters[column] + - '"]', - ); + let headerFilters = this.presetInfo.headerFilters + ? Object.keys(this.presetInfo.headerFilters).map((column) => + columnTitles[column] + ? columnTitles[column] + + '["' + + this.presetInfo.headerFilters[column] + + '"]' + : "", + ) + : []; + headerFilters = headerFilters.filter((string) => string.length); + return { displayedColumns: "Displayed columns (in order): " + @@ -65,13 +72,14 @@ export default { headerFilters: "Column header filters: " + headerFilters.join(", "), sort: - "Sort: " + - columnTitles[this.presetInfo.sort.column] + - " (" + - (this.presetInfo.sort.direction === "asc" - ? "ascending" - : "descending") + - ")", + "Sort: " + this.presetInfo.sort + ? columnTitles[this.presetInfo.sort.column] + + " (" + + (this.presetInfo.sort.direction === "asc" + ? "ascending" + : "descending") + + ")" + : "", }; }, newPresetFormattedStrings() { @@ -172,9 +180,9 @@ export default { } }, generateNewPreset() { - const columns = this.tabulator - .getColumnDefinitions() - .filter((column) => column.field !== "collapse"); + const columns = this.tabulator.columnManager.columns.filter( + (column) => column.field !== "collapse", + ); const displayedColumns = columns .filter((column) => column.visible) @@ -262,21 +270,98 @@ export default { } } }, - async syncPresetWithConfig(preset) { - // todo + syncPresetWithConfig(preset) { + const validColumns = this.$props.tabulator.getColumnDefinitions(); + let isUpdateNecessary = false; + + if (preset.displayedColumns) { + const initialNumberOfDisplayedColumns = + preset.displayedColumns.length; + preset.displayedColumns = preset.displayedColumns.filter( + (columnField) => { + return validColumns.some( + (column) => column.field === columnField, + ); + }, + ); + const updatedNumberOfDisplayedColumns = + preset.displayedColumns.length; + + isUpdateNecessary = + updatedNumberOfDisplayedColumns < + initialNumberOfDisplayedColumns; + } + + if (preset.headerFilters) { + const initialNumberOfHeaderFilters = Object.keys( + preset.headerFilters, + ).length; + Object.keys(preset.headerFilters).forEach((columnField) => { + if ( + !validColumns.some( + (column) => + column.field === columnField && + column.headerFilter, + ) || + !preset.displayedColumns.includes(columnField) + ) { + delete preset.headerFilters[columnField]; + } + }); + const updatedNumberOfHeaderFilters = Object.keys( + preset.headerFilters, + ).length; + + isUpdateNecessary = + isUpdateNecessary || + updatedNumberOfHeaderFilters < initialNumberOfHeaderFilters; + } + + if (preset.sort) { + if ( + !validColumns.some( + (column) => + column.field === preset.sort.column && + column.headerSort, + ) + ) { + preset.sort = null; + isUpdateNecessary = true; + } + } + + if (isUpdateNecessary) { + this.updateTabulatorPreset(preset); + } + return preset; }, + async updateTabulatorPreset(preset) { + if (!preset.id) return; + + const tabulatorPresetUpdateResponse = await this.$api.call( + ApiTabulatorPresets.updateTabulatorPreset(preset.id, { + displayedColumns: preset.displayedColumns, + headerFilters: preset.headerFilters, + sort: preset.sort, + }), + ); + + if (tabulatorPresetUpdateResponse.meta.status === "success") { + this.fetchCustomUserTabulatorPresets(); + } + }, async applyPreset(preset) { - preset = await this.syncPresetWithConfig(preset); + preset = this.syncPresetWithConfig(preset); - console.log(this.$props.tabulator); let columns = this.$props.tabulator .getColumnDefinitions() .filter((column) => column.field !== "collapse"); columns.forEach((column) => this.$props.tabulator.hideColumn(column.field), ); - preset.displayedColumns.forEach( + + preset.displayedColumns?.forEach( (columnField, index, displayedColumns) => { this.$props.tabulator.showColumn(columnField); if (index === 0) return; @@ -289,14 +374,16 @@ export default { ); this.$props.tabulator.clearHeaderFilter(); - Object.entries(preset.headerFilters).forEach( - ([columnField, filterValue]) => { - this.$props.tabulator.setHeaderFilterValue( - columnField, - filterValue, - ); - }, - ); + if (preset.headerFilters) { + Object.entries(preset.headerFilters).forEach( + ([columnField, filterValue]) => { + this.$props.tabulator.setHeaderFilterValue( + columnField, + filterValue, + ); + }, + ); + } this.$props.tabulator.clearSort(); if (preset.sort?.column) { From 7abd35d11b3b3d3f48e66af49a48f19f529d1a44 Mon Sep 17 00:00:00 2001 From: adisposkofh Date: Thu, 11 Jun 2026 11:20:34 +0200 Subject: [PATCH 10/65] added tabulator preset phrases --- .../components/filter/Filter/TablePresets.js | 57 +++-- system/phrasesupdate.php | 202 ++++++++++++++++++ 2 files changed, 240 insertions(+), 19 deletions(-) diff --git a/public/js/components/filter/Filter/TablePresets.js b/public/js/components/filter/Filter/TablePresets.js index 132f7954f..f8cbb6c2d 100644 --- a/public/js/components/filter/Filter/TablePresets.js +++ b/public/js/components/filter/Filter/TablePresets.js @@ -67,17 +67,24 @@ export default { return { displayedColumns: - "Displayed columns (in order): " + + this.$p.t("tabulator_presets/displayed_columns") + + " (" + + this.$p.t("tabulator_presets/in_order") + + "): " + displayedColumns.join(", "), headerFilters: - "Column header filters: " + headerFilters.join(", "), + this.$p.t("tabulator_presets/column_header_filters") + + ": " + + headerFilters.join(", "), sort: - "Sort: " + this.presetInfo.sort + this.$p.t("tabulator_presets/sort") + + ": " + + this.presetInfo.sort ? columnTitles[this.presetInfo.sort.column] + " (" + (this.presetInfo.sort.direction === "asc" - ? "ascending" - : "descending") + + ? this.$p.t("tabulator_presets/asc") + : this.$p.t("tabulator_presets/desc")) + ")" : "", }; @@ -100,18 +107,24 @@ export default { ); return { displayedColumns: - "Displayed columns (in order): " + + this.$p.t("tabulator_presets/displayed_columns") + + " (" + + this.$p.t("tabulator_presets/in_order") + + "): " + displayedColumns.join(", "), headerFilters: - "Column header filters: " + headerFilters.join(", "), + this.$p.t("tabulator_presets/column_header_filters") + + ": " + + headerFilters.join(", "), sort: - "Sort: " + + this.$p.t("tabulator_presets/sort") + + ": " + (this.newPreset.sort ? columnTitles[this.newPreset.sort.column] + " (" + (this.newPreset.sort.direction === "asc" - ? "ascending" - : "descending") + + ? this.$p.t("tabulator_presets/asc") + : this.$p.t("tabulator_presets/desc")) + ")" : ""), }; @@ -255,10 +268,9 @@ export default { if ( window.confirm( - "Are you sure you want to delete preset (((presetName)))?".replace( - "(((presetName)))", - preset.name, - ), + this.$p + .t("tabulator_presets/preset_deletion_confirmation") + .replace("(((presetName)))", preset.name), ) ) { const presetDeletionResponse = await this.$api.call( @@ -427,12 +439,13 @@ export default { }, }, async created() { + await this.$p.loadCategory(["tabulator_presets", "global"]); await this.fetchCustomUserTabulatorPresets(); }, template: /*html*/ `
-
Table Presets
+
{{ $p.t("tabulator_presets/table_presets") }}
@@ -447,7 +460,7 @@ export default {
- {{ "Save current configuration" }} + {{ $p.t("tabulator_presets/save_current_config") }}
@@ -470,15 +483,21 @@ export default {
- {{ "Save current configuration" }} + {{ $p.t("tabulator_presets/save_current_config") }}
{{ newPresetFormattedStrings?.displayedColumns }} {{ newPresetFormattedStrings?.headerFilters }} {{ newPresetFormattedStrings?.sort }}
- -
{{ "Save" }}
+ +
+ {{ $p.t('global/speichern') }} +
diff --git a/system/phrasesupdate.php b/system/phrasesupdate.php index 8a68a43ff..737239056 100644 --- a/system/phrasesupdate.php +++ b/system/phrasesupdate.php @@ -58559,6 +58559,208 @@ I have been informed that I am under no obligation to consent to the transmissio ) ), // generic tabulator phrases END + // tabulator presets section phrases START + array( + 'app' => 'core', + 'category' => 'tabulator_presets', + 'phrase' => 'displayed_columns', + 'insertvon' => 'system', + 'phrases' => array( + array( + 'sprache' => 'German', + 'text' => 'Angezeigte Spalten', + 'description' => '', + 'insertvon' => 'system' + ), + array( + 'sprache' => 'English', + 'text' => 'Displayed columns', + 'description' => '', + 'insertvon' => 'system' + ) + ) + ), + array( + 'app' => 'core', + 'category' => 'tabulator_presets', + 'phrase' => 'in_order', + 'insertvon' => 'system', + 'phrases' => array( + array( + 'sprache' => 'German', + 'text' => 'in Reihenfolge', + 'description' => '', + 'insertvon' => 'system' + ), + array( + 'sprache' => 'English', + 'text' => 'in order', + 'description' => '', + 'insertvon' => 'system' + ) + ) + ), + array( + 'app' => 'core', + 'category' => 'tabulator_presets', + 'phrase' => 'column_header_filters', + 'insertvon' => 'system', + 'phrases' => array( + array( + 'sprache' => 'German', + 'text' => 'Spalten Header Filter', + 'description' => '', + 'insertvon' => 'system' + ), + array( + 'sprache' => 'English', + 'text' => 'Column header filters', + 'description' => '', + 'insertvon' => 'system' + ) + ) + ), + array( + 'app' => 'core', + 'category' => 'tabulator_presets', + 'phrase' => 'asc', + 'insertvon' => 'system', + 'phrases' => array( + array( + 'sprache' => 'German', + 'text' => 'aufsteigend', + 'description' => '', + 'insertvon' => 'system' + ), + array( + 'sprache' => 'English', + 'text' => 'ascending', + 'description' => '', + 'insertvon' => 'system' + ) + ) + ), + array( + 'app' => 'core', + 'category' => 'tabulator_presets', + 'phrase' => 'desc', + 'insertvon' => 'system', + 'phrases' => array( + array( + 'sprache' => 'German', + 'text' => 'absteigend', + 'description' => '', + 'insertvon' => 'system' + ), + array( + 'sprache' => 'English', + 'text' => 'descending', + 'description' => '', + 'insertvon' => 'system' + ) + ) + ), + array( + 'app' => 'core', + 'category' => 'tabulator_presets', + 'phrase' => 'sort', + 'insertvon' => 'system', + 'phrases' => array( + array( + 'sprache' => 'German', + 'text' => 'Sortierung', + 'description' => '', + 'insertvon' => 'system' + ), + array( + 'sprache' => 'English', + 'text' => 'Sort', + 'description' => '', + 'insertvon' => 'system' + ) + ) + ), + array( + 'app' => 'core', + 'category' => 'tabulator_presets', + 'phrase' => 'preset_deletion_confirmation', + 'insertvon' => 'system', + 'phrases' => array( + array( + 'sprache' => 'German', + 'text' => 'Sind Sie sicher, dass Sie die Voreinstellung (((presetName))) löschen möchten?', + 'description' => '', + 'insertvon' => 'system' + ), + array( + 'sprache' => 'English', + 'text' => 'Are you sure you want to delete preset (((presetName)))?', + 'description' => '', + 'insertvon' => 'system' + ) + ) + ), + array( + 'app' => 'core', + 'category' => 'tabulator_presets', + 'phrase' => 'table_presets', + 'insertvon' => 'system', + 'phrases' => array( + array( + 'sprache' => 'German', + 'text' => 'Tabelle Voreinstellungen', + 'description' => '', + 'insertvon' => 'system' + ), + array( + 'sprache' => 'English', + 'text' => 'Table presets', + 'description' => '', + 'insertvon' => 'system' + ) + ) + ), + array( + 'app' => 'core', + 'category' => 'tabulator_presets', + 'phrase' => 'save_current_config', + 'insertvon' => 'system', + 'phrases' => array( + array( + 'sprache' => 'German', + 'text' => 'Akutelle Einstellung speichern', + 'description' => '', + 'insertvon' => 'system' + ), + array( + 'sprache' => 'English', + 'text' => 'Save current configuration', + 'description' => '', + 'insertvon' => 'system' + ) + ) + ), + array( + 'app' => 'core', + 'category' => 'tabulator_presets', + 'phrase' => 'preset_name', + 'insertvon' => 'system', + 'phrases' => array( + array( + 'sprache' => 'German', + 'text' => 'Voreinstellung Name', + 'description' => '', + 'insertvon' => 'system' + ), + array( + 'sprache' => 'English', + 'text' => 'Preset name', + 'description' => '', + 'insertvon' => 'system' + ) + ) + ), + // tabulator presets section phrases END ); From 34c210a2a77171a038402d6a41ec034e86d1dd0a Mon Sep 17 00:00:00 2001 From: adisposkofh Date: Thu, 11 Jun 2026 11:27:43 +0200 Subject: [PATCH 11/65] fixed table preset info string formatting --- .../js/components/filter/Filter/TablePresets.js | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/public/js/components/filter/Filter/TablePresets.js b/public/js/components/filter/Filter/TablePresets.js index f8cbb6c2d..f0e0e3328 100644 --- a/public/js/components/filter/Filter/TablePresets.js +++ b/public/js/components/filter/Filter/TablePresets.js @@ -65,6 +65,14 @@ export default { : []; headerFilters = headerFilters.filter((string) => string.length); + const sortColumn = this.presetInfo.sort?.column + ? columnTitles[this.presetInfo.sort.column] + : null; + const sortDirection = + this.presetInfo.sort?.direction === "desc" + ? this.$p.t("tabulator_presets/desc") + : this.$p.t("tabulator_presets/asc"); + return { displayedColumns: this.$p.t("tabulator_presets/displayed_columns") + @@ -79,14 +87,7 @@ export default { sort: this.$p.t("tabulator_presets/sort") + ": " + - this.presetInfo.sort - ? columnTitles[this.presetInfo.sort.column] + - " (" + - (this.presetInfo.sort.direction === "asc" - ? this.$p.t("tabulator_presets/asc") - : this.$p.t("tabulator_presets/desc")) + - ")" - : "", + (sortColumn ? sortColumn + " (" + sortDirection + ")" : ""), }; }, newPresetFormattedStrings() { From f0f9becc1f7e2dfd16a5c342215d100dc14c6a4b Mon Sep 17 00:00:00 2001 From: adisposkofh Date: Thu, 11 Jun 2026 11:29:29 +0200 Subject: [PATCH 12/65] fixed new table preset info string formatting --- .../components/filter/Filter/TablePresets.js | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/public/js/components/filter/Filter/TablePresets.js b/public/js/components/filter/Filter/TablePresets.js index f0e0e3328..512d2b47e 100644 --- a/public/js/components/filter/Filter/TablePresets.js +++ b/public/js/components/filter/Filter/TablePresets.js @@ -106,6 +106,15 @@ export default { this.newPreset.headerFilters[column] + '"]', ); + + const sortColumn = this.newPreset.sort?.column + ? columnTitles[this.newPreset.sort.column] + : null; + const sortDirection = + this.newPreset.sort?.direction === "desc" + ? this.$p.t("tabulator_presets/desc") + : this.$p.t("tabulator_presets/asc"); + return { displayedColumns: this.$p.t("tabulator_presets/displayed_columns") + @@ -120,14 +129,7 @@ export default { sort: this.$p.t("tabulator_presets/sort") + ": " + - (this.newPreset.sort - ? columnTitles[this.newPreset.sort.column] + - " (" + - (this.newPreset.sort.direction === "asc" - ? this.$p.t("tabulator_presets/asc") - : this.$p.t("tabulator_presets/desc")) + - ")" - : ""), + (sortColumn ? sortColumn + " (" + sortDirection + ")" : ""), }; }, }, From c9011fd6ede90e8dbae31ab2e5cce57cffbd8905 Mon Sep 17 00:00:00 2001 From: adisposkofh Date: Thu, 11 Jun 2026 11:30:42 +0200 Subject: [PATCH 13/65] minor naming change in table presets --- public/js/components/filter/Filter/TablePresets.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/public/js/components/filter/Filter/TablePresets.js b/public/js/components/filter/Filter/TablePresets.js index 512d2b47e..c764b9284 100644 --- a/public/js/components/filter/Filter/TablePresets.js +++ b/public/js/components/filter/Filter/TablePresets.js @@ -346,12 +346,12 @@ export default { } if (isUpdateNecessary) { - this.updateTabulatorPreset(preset); + this.updatePreset(preset); } return preset; }, - async updateTabulatorPreset(preset) { + async updatePreset(preset) { if (!preset.id) return; const tabulatorPresetUpdateResponse = await this.$api.call( From 538223752b5d9827be63c81bc25cf2e29a7cd151 Mon Sep 17 00:00:00 2001 From: adisposkofh Date: Thu, 11 Jun 2026 12:38:57 +0200 Subject: [PATCH 14/65] hiding preset info/new preset form when table presets section is collapsed --- public/js/components/filter/Filter.js | 4 ++++ public/js/components/filter/Filter/TablePresets.js | 11 +++++++++-- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/public/js/components/filter/Filter.js b/public/js/components/filter/Filter.js index 1eeea5e20..9b5e14f43 100644 --- a/public/js/components/filter/Filter.js +++ b/public/js/components/filter/Filter.js @@ -143,6 +143,7 @@ export const CoreFilterCmpt = { page: false, }, collapsedHeadingLocalizationTimer: null, + areTablePresetsShown: false, }; }, computed: { @@ -916,11 +917,14 @@ export const CoreFilterCmpt = { v-if="$props.isUsingPresets && $props.presetsId?.length" v-collapse-auto-close @tablePresetApplied="afterTablePresetApplied($event.preset)" + @[\`shown.bs.collapse\`]="areTablePresetsShown = true" + @[\`hide.bs.collapse\`]="areTablePresetsShown = false" :id="'tablePresets' + idExtra" :data-bs-parent="'#filterCollapsables' + idExtra" :presetsId="$props.presetsId" :tabulator="tabulator" :generalPresets="$props.generalPresets" + :isShown="areTablePresetsShown" class="card-body collapse" > diff --git a/public/js/components/filter/Filter/TablePresets.js b/public/js/components/filter/Filter/TablePresets.js index c764b9284..f8432d2db 100644 --- a/public/js/components/filter/Filter/TablePresets.js +++ b/public/js/components/filter/Filter/TablePresets.js @@ -26,6 +26,7 @@ export default { presetsId: { type: String }, tabulator: { type: Object }, generalPresets: { type: Array, default: [] }, + isShown: { type: Boolean, default: true } }, emits: ["applyTablePreset"], data: function () { @@ -141,6 +142,12 @@ export default { }, deep: true, }, + isShown() { + if (!this.$props.isShown) { + this.hidePresetInfo(); + this.hideNewPresetForm(); + } + }, }, methods: { async showPresetInfo(preset) { @@ -467,7 +474,7 @@ export default {
-
+

@@ -481,7 +488,7 @@ export default {
-
+

From 3ced1966f4202200f7b10c4b36736cd592118c07 Mon Sep 17 00:00:00 2001 From: adisposkofh Date: Thu, 11 Jun 2026 12:53:34 +0200 Subject: [PATCH 15/65] minor fix --- application/controllers/api/frontend/v1/TabulatorPresets.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/application/controllers/api/frontend/v1/TabulatorPresets.php b/application/controllers/api/frontend/v1/TabulatorPresets.php index e8c27aed6..b5aec2362 100644 --- a/application/controllers/api/frontend/v1/TabulatorPresets.php +++ b/application/controllers/api/frontend/v1/TabulatorPresets.php @@ -112,7 +112,7 @@ class TabulatorPresets extends FHCAPI_Controller $uid = getAuthUID(); if ($existingPreset->benutzer_uid !== $uid) { - $this->terminateWithError("You are not allowed to delete this preset!", "general", 403); + $this->terminateWithError("You are not allowed to update this preset!", "general", 403); } $presetUpdateResult = $this->TabulatorPresetModel->updateTabulatorPreset($presetId, $preset); From c9fe633706482dbb31c95344284f202e3bc25093 Mon Sep 17 00:00:00 2001 From: adisposkofh Date: Thu, 11 Jun 2026 13:21:56 +0200 Subject: [PATCH 16/65] localized tabulator preset backend error messages --- .../api/frontend/v1/TabulatorPresets.php | 25 ++- .../tabulator/Tabulator_preset_model.php | 6 +- system/phrasesupdate.php | 180 ++++++++++++++++++ 3 files changed, 198 insertions(+), 13 deletions(-) diff --git a/application/controllers/api/frontend/v1/TabulatorPresets.php b/application/controllers/api/frontend/v1/TabulatorPresets.php index b5aec2362..c38cce033 100644 --- a/application/controllers/api/frontend/v1/TabulatorPresets.php +++ b/application/controllers/api/frontend/v1/TabulatorPresets.php @@ -35,6 +35,11 @@ class TabulatorPresets extends FHCAPI_Controller ]); $this->load->model('tabulator/Tabulator_preset_model', 'TabulatorPresetModel'); + + $this->loadPhrases([ + 'global', + 'tabulator_presets' + ]); } //------------------------------------------------------------------------------------------------------------------ @@ -44,7 +49,7 @@ class TabulatorPresets extends FHCAPI_Controller { $tableName = $this->input->get("tableName"); if (!$tableName) { - $this->terminateWithError(error("Invalid parameters!")); + $this->terminateWithError(error($this->p->t("global", "invalid_params_err_msg"))); } $uid = getAuthUID(); @@ -61,7 +66,7 @@ class TabulatorPresets extends FHCAPI_Controller $preset = $this->input->post("preset"); if (!$tableName || !$presetName || !$preset) { - $this->terminateWithError("Invalid parameters!", "general", 400); + $this->terminateWithError($this->p->t("global", "invalid_params_err_msg"), "general", 400); } $uid = getAuthUID(); @@ -70,7 +75,7 @@ class TabulatorPresets extends FHCAPI_Controller $existingPresets = $this->getDataOrTerminateWithError($existingPresetsResult) ?? []; if (count($existingPresets) > 19) { - $this->terminateWithError("Maximum number of presets reached!", "general", 409); + $this->terminateWithError($this->p->t("tabulator_presets", "max_presets_count_err_msg"), "general", 409); } $existingPresetNames = array_map(function($presetInfo) { @@ -78,7 +83,7 @@ class TabulatorPresets extends FHCAPI_Controller }, $existingPresets); if (in_array($presetName, $existingPresetNames)) { - $this->terminateWithError("Preset name duplicate not allowed!", "general", 409); + $this->terminateWithError($this->p->t("tabulator_presets", "preset_name_duplicate_err_msg"), "general", 409); } $presetCreationResult = $this->TabulatorPresetModel->createTabulatorPreset($uid, $tableName, $presetName, $preset); @@ -100,19 +105,19 @@ class TabulatorPresets extends FHCAPI_Controller $preset = $this->input->post("preset"); if (!$presetId || !$preset) { - $this->terminateWithError("Invalid parameters!", "general", 400); + $this->terminateWithError($this->p->t("global", "invalid_params_err_msg"), "general", 400); } $existingPresetData = $this->TabulatorPresetModel->getTabulatorPreset($presetId); $existingPresetArray = $this->getDataOrTerminateWithError($existingPresetData) ?? [null]; $existingPreset = $existingPresetArray[0]; if (!$existingPreset) { - $this->terminateWithError("Preset not found!", "general", 404); + $this->terminateWithError($this->p->t("tabulator_presets", "preset_not_found_err_msg"), "general", 404); } $uid = getAuthUID(); if ($existingPreset->benutzer_uid !== $uid) { - $this->terminateWithError("You are not allowed to update this preset!", "general", 403); + $this->terminateWithError($this->p->t("tabulator_presets", "preset_not_own_update_err_msg"), "general", 403); } $presetUpdateResult = $this->TabulatorPresetModel->updateTabulatorPreset($presetId, $preset); @@ -131,19 +136,19 @@ class TabulatorPresets extends FHCAPI_Controller { $presetId = $this->input->post("presetId"); if (!$presetId) { - $this->terminateWithError("Invalid parameters!", "general", 400); + $this->terminateWithError($this->p->t("global", "invalid_params_err_msg"), "general", 400); } $presetData = $this->TabulatorPresetModel->getTabulatorPreset($presetId); $presetArray = $this->getDataOrTerminateWithError($presetData) ?? [null]; $preset = $presetArray[0]; if (!$preset) { - $this->terminateWithError("Preset not found!", "general", 404); + $this->terminateWithError($this->p->t("tabulator_presets", "preset_not_found_err_msg"), "general", 404); } $uid = getAuthUID(); if ($preset->benutzer_uid !== $uid) { - $this->terminateWithError("You are not allowed to delete this preset!", "general", 403); + $this->terminateWithError($this->p->t("tabulator_presets", "preset_not_own_delete_err_msg"), "general", 403); } $presetDeletionResult = $this->TabulatorPresetModel->deleteTabulatorPreset($presetId); diff --git a/application/models/tabulator/Tabulator_preset_model.php b/application/models/tabulator/Tabulator_preset_model.php index 209420069..a3c45739d 100644 --- a/application/models/tabulator/Tabulator_preset_model.php +++ b/application/models/tabulator/Tabulator_preset_model.php @@ -50,7 +50,7 @@ class Tabulator_preset_model extends DB_Model if (isError($presetCreationResult)) { - return error('Something went wrong with tabulator preset creation.'); + return error('Something went wrong during preset creation!'); } return success($presetCreationResult->retval); @@ -62,7 +62,7 @@ class Tabulator_preset_model extends DB_Model if (isError($presetUpdateResult)) { - return error('Something went wrong during tabulator preset update.'); + return error('Something went wrong during preset update!'); } return success($presetUpdateResult->retval); @@ -74,7 +74,7 @@ class Tabulator_preset_model extends DB_Model if (isError($presetDeletionReseult)) { - return error('Something went wrong during tabulator preset deletion.'); + return error('Something went wrong during preset deletion!'); } return success($presetDeletionReseult->retval); diff --git a/system/phrasesupdate.php b/system/phrasesupdate.php index 737239056..17b852eab 100644 --- a/system/phrasesupdate.php +++ b/system/phrasesupdate.php @@ -33112,6 +33112,26 @@ array( ) ) ), + array( + 'app' => 'anwesenheiten', + 'category' => 'global', + 'phrase' => 'invalid_params_err_msg', + 'insertvon' => 'system', + 'phrases' => array( + array( + 'sprache' => 'German', + 'text' => 'Ungültige Parameterübergabe!', + 'description' => '', + 'insertvon' => 'system' + ), + array( + 'sprache' => 'English', + 'text' => 'Invalid parameters!', + 'description' => '', + 'insertvon' => 'system' + ) + ) + ), array( 'app' => 'anwesenheiten', 'category' => 'global', @@ -58760,6 +58780,166 @@ I have been informed that I am under no obligation to consent to the transmissio ) ) ), + array( + 'app' => 'core', + 'category' => 'tabulator_presets', + 'phrase' => 'max_presets_count_err_msg', + 'insertvon' => 'system', + 'phrases' => array( + array( + 'sprache' => 'German', + 'text' => 'Maximale Anzahl an Voreinstellungen erreicht!', + 'description' => '', + 'insertvon' => 'system' + ), + array( + 'sprache' => 'English', + 'text' => 'Maximum number of presets reached!', + 'description' => '', + 'insertvon' => 'system' + ) + ) + ), + array( + 'app' => 'core', + 'category' => 'tabulator_presets', + 'phrase' => 'preset_name_duplicate_err_msg', + 'insertvon' => 'system', + 'phrases' => array( + array( + 'sprache' => 'German', + 'text' => 'Doppelte Voreinstellunge Namen sind nicht zulässig!', + 'description' => '', + 'insertvon' => 'system' + ), + array( + 'sprache' => 'English', + 'text' => 'Preset name duplicate not allowed!', + 'description' => '', + 'insertvon' => 'system' + ) + ) + ), + array( + 'app' => 'core', + 'category' => 'tabulator_presets', + 'phrase' => 'preset_not_found_err_msg', + 'insertvon' => 'system', + 'phrases' => array( + array( + 'sprache' => 'German', + 'text' => 'Voreinstellung nicht gefunden!', + 'description' => '', + 'insertvon' => 'system' + ), + array( + 'sprache' => 'English', + 'text' => 'Preset not found!', + 'description' => '', + 'insertvon' => 'system' + ) + ) + ), + array( + 'app' => 'core', + 'category' => 'tabulator_presets', + 'phrase' => 'preset_not_own_update_err_msg', + 'insertvon' => 'system', + 'phrases' => array( + array( + 'sprache' => 'German', + 'text' => 'Sie dürfen diese Voreinstellung nicht ändern!', + 'description' => '', + 'insertvon' => 'system' + ), + array( + 'sprache' => 'English', + 'text' => 'You are not allowed to update this preset!', + 'description' => '', + 'insertvon' => 'system' + ) + ) + ), + array( + 'app' => 'core', + 'category' => 'tabulator_presets', + 'phrase' => 'preset_not_own_delete_err_msg', + 'insertvon' => 'system', + 'phrases' => array( + array( + 'sprache' => 'German', + 'text' => 'Sie dürfen diese Voreinstellung nicht löschen!', + 'description' => '', + 'insertvon' => 'system' + ), + array( + 'sprache' => 'English', + 'text' => 'You are not allowed to delete this preset!', + 'description' => '', + 'insertvon' => 'system' + ) + ) + ), + array( + 'app' => 'core', + 'category' => 'tabulator_presets', + 'phrase' => 'preset_creation_generic_err_msg', + 'insertvon' => 'system', + 'phrases' => array( + array( + 'sprache' => 'German', + 'text' => 'Bei der Erstellung der Voreinstellung ist ein Fehler aufgetreten!', + 'description' => '', + 'insertvon' => 'system' + ), + array( + 'sprache' => 'English', + 'text' => 'Something went wrong during preset creation!', + 'description' => '', + 'insertvon' => 'system' + ) + ) + ), + array( + 'app' => 'core', + 'category' => 'tabulator_presets', + 'phrase' => 'preset_update_generic_err_msg', + 'insertvon' => 'system', + 'phrases' => array( + array( + 'sprache' => 'German', + 'text' => 'Bei der Voreinstellungsänderung ist ein Fehler aufgetreten!', + 'description' => '', + 'insertvon' => 'system' + ), + array( + 'sprache' => 'English', + 'text' => 'Something went wrong during preset update!', + 'description' => '', + 'insertvon' => 'system' + ) + ) + ), + array( + 'app' => 'core', + 'category' => 'tabulator_presets', + 'phrase' => 'preset_deletion_generic_err_msg', + 'insertvon' => 'system', + 'phrases' => array( + array( + 'sprache' => 'German', + 'text' => 'Beim Löschen der Voreinstellung ist ein Fehler aufgetreten!', + 'description' => '', + 'insertvon' => 'system' + ), + array( + 'sprache' => 'English', + 'text' => 'Something went wrong during preset deletion!', + 'description' => '', + 'insertvon' => 'system' + ) + ) + ), // tabulator presets section phrases END ); From f59e5a20ea6eeafa10dd6193ba119c7f9c68b4e8 Mon Sep 17 00:00:00 2001 From: adisposkofh Date: Thu, 11 Jun 2026 13:36:26 +0200 Subject: [PATCH 17/65] formatting preset info/new preset overviews --- .../components/filter/Filter/TablePresets.js | 76 ++++++++++++------- 1 file changed, 47 insertions(+), 29 deletions(-) diff --git a/public/js/components/filter/Filter/TablePresets.js b/public/js/components/filter/Filter/TablePresets.js index f8432d2db..f7e4a6934 100644 --- a/public/js/components/filter/Filter/TablePresets.js +++ b/public/js/components/filter/Filter/TablePresets.js @@ -26,7 +26,7 @@ export default { presetsId: { type: String }, tabulator: { type: Object }, generalPresets: { type: Array, default: [] }, - isShown: { type: Boolean, default: true } + isShown: { type: Boolean, default: true }, }, emits: ["applyTablePreset"], data: function () { @@ -53,6 +53,13 @@ export default { displayedColumns = displayedColumns.filter( (string) => string.length, ); + const displayedColumnsString = displayedColumns.length + ? this.$p.t("tabulator_presets/displayed_columns") + + " (" + + this.$p.t("tabulator_presets/in_order") + + "): " + + displayedColumns.join(", ") + : ""; let headerFilters = this.presetInfo.headerFilters ? Object.keys(this.presetInfo.headerFilters).map((column) => @@ -65,6 +72,11 @@ export default { ) : []; headerFilters = headerFilters.filter((string) => string.length); + const headerFiltersString = headerFilters.length + ? this.$p.t("tabulator_presets/column_header_filters") + + ": " + + headerFilters.join(", ") + : ""; const sortColumn = this.presetInfo.sort?.column ? columnTitles[this.presetInfo.sort.column] @@ -73,22 +85,19 @@ export default { this.presetInfo.sort?.direction === "desc" ? this.$p.t("tabulator_presets/desc") : this.$p.t("tabulator_presets/asc"); + const sortString = sortColumn + ? this.$p.t("tabulator_presets/sort") + + ": " + + sortColumn + + " (" + + sortDirection + + ")" + : ""; return { - displayedColumns: - this.$p.t("tabulator_presets/displayed_columns") + - " (" + - this.$p.t("tabulator_presets/in_order") + - "): " + - displayedColumns.join(", "), - headerFilters: - this.$p.t("tabulator_presets/column_header_filters") + - ": " + - headerFilters.join(", "), - sort: - this.$p.t("tabulator_presets/sort") + - ": " + - (sortColumn ? sortColumn + " (" + sortDirection + ")" : ""), + displayedColumns: displayedColumnsString, + headerFilters: headerFiltersString, + sort: sortString, }; }, newPresetFormattedStrings() { @@ -99,6 +108,13 @@ export default { const displayedColumns = this.newPreset.displayedColumns.map( (columnField) => columnTitles[columnField], ); + const displayedColumnsString = displayedColumns.length + ? this.$p.t("tabulator_presets/displayed_columns") + + " (" + + this.$p.t("tabulator_presets/in_order") + + "): " + + displayedColumns.join(", ") + : ""; const headerFilters = Object.keys(this.newPreset.headerFilters).map( (column) => @@ -107,6 +123,11 @@ export default { this.newPreset.headerFilters[column] + '"]', ); + const headerFiltersString = headerFilters.length + ? this.$p.t("tabulator_presets/column_header_filters") + + ": " + + headerFilters.join(", ") + : ""; const sortColumn = this.newPreset.sort?.column ? columnTitles[this.newPreset.sort.column] @@ -115,22 +136,19 @@ export default { this.newPreset.sort?.direction === "desc" ? this.$p.t("tabulator_presets/desc") : this.$p.t("tabulator_presets/asc"); + const sortString = sortColumn + ? this.$p.t("tabulator_presets/sort") + + ": " + + sortColumn + + " (" + + sortDirection + + ")" + : ""; return { - displayedColumns: - this.$p.t("tabulator_presets/displayed_columns") + - " (" + - this.$p.t("tabulator_presets/in_order") + - "): " + - displayedColumns.join(", "), - headerFilters: - this.$p.t("tabulator_presets/column_header_filters") + - ": " + - headerFilters.join(", "), - sort: - this.$p.t("tabulator_presets/sort") + - ": " + - (sortColumn ? sortColumn + " (" + sortDirection + ")" : ""), + displayedColumns: displayedColumnsString, + headerFilters: headerFiltersString, + sort: sortString, }; }, }, From 4474a24ee2d98d3142dba512c6803210cb8742a0 Mon Sep 17 00:00:00 2001 From: adisposkofh Date: Thu, 11 Jun 2026 14:09:45 +0200 Subject: [PATCH 18/65] streamlined collapsing table presets sub-collapsibles when tablePresets is collapsed --- public/js/components/filter/Filter.js | 18 ++++++++++++---- .../components/filter/Filter/TablePresets.js | 21 +++++++------------ 2 files changed, 21 insertions(+), 18 deletions(-) diff --git a/public/js/components/filter/Filter.js b/public/js/components/filter/Filter.js index 9b5e14f43..b52a09f65 100644 --- a/public/js/components/filter/Filter.js +++ b/public/js/components/filter/Filter.js @@ -143,7 +143,6 @@ export const CoreFilterCmpt = { page: false, }, collapsedHeadingLocalizationTimer: null, - areTablePresetsShown: false, }; }, computed: { @@ -835,6 +834,17 @@ export const CoreFilterCmpt = { afterTablePresetApplied(preset) { this.selectedFields = preset.displayedColumns; }, + updateTablePresetsSubCollapsibles() { + if ( + !this.$refs.tablePresets.$el + .getAttribute("class") + .split(" ") + .includes("show") + ) { + this.$refs.tablePresets?.hidePresetInfo(); + this.$refs.tablePresets?.hideNewPresetForm(); + } + }, }, beforeCreate() { if (!this.tableOnly == !this.filterType) @@ -917,14 +927,14 @@ export const CoreFilterCmpt = { v-if="$props.isUsingPresets && $props.presetsId?.length" v-collapse-auto-close @tablePresetApplied="afterTablePresetApplied($event.preset)" - @[\`shown.bs.collapse\`]="areTablePresetsShown = true" - @[\`hide.bs.collapse\`]="areTablePresetsShown = false" + @[\`shown.bs.collapse\`]="updateTablePresetsSubCollapsibles()" + @[\`hidden.bs.collapse\`]="updateTablePresetsSubCollapsibles()" :id="'tablePresets' + idExtra" :data-bs-parent="'#filterCollapsables' + idExtra" :presetsId="$props.presetsId" :tabulator="tabulator" :generalPresets="$props.generalPresets" - :isShown="areTablePresetsShown" + ref="tablePresets" class="card-body collapse" > diff --git a/public/js/components/filter/Filter/TablePresets.js b/public/js/components/filter/Filter/TablePresets.js index f7e4a6934..0805be874 100644 --- a/public/js/components/filter/Filter/TablePresets.js +++ b/public/js/components/filter/Filter/TablePresets.js @@ -26,7 +26,6 @@ export default { presetsId: { type: String }, tabulator: { type: Object }, generalPresets: { type: Array, default: [] }, - isShown: { type: Boolean, default: true }, }, emits: ["applyTablePreset"], data: function () { @@ -160,12 +159,6 @@ export default { }, deep: true, }, - isShown() { - if (!this.$props.isShown) { - this.hidePresetInfo(); - this.hideNewPresetForm(); - } - }, }, methods: { async showPresetInfo(preset) { @@ -258,7 +251,7 @@ export default { sort, }; }, - async hideNewPresetForm() { + hideNewPresetForm() { if ( this.$refs.newPresetFormCollapsible .getAttribute("class") @@ -500,9 +493,9 @@ export default { {{ presetInfo?.name }}
- {{ presetInfoFormattedStrings?.displayedColumns }} - {{ presetInfoFormattedStrings?.headerFilters }} - {{ presetInfoFormattedStrings?.sort }} + {{ presetInfoFormattedStrings.displayedColumns }} + {{ presetInfoFormattedStrings.headerFilters }} + {{ presetInfoFormattedStrings.sort }}
@@ -514,9 +507,9 @@ export default { {{ $p.t("tabulator_presets/save_current_config") }}
- {{ newPresetFormattedStrings?.displayedColumns }} - {{ newPresetFormattedStrings?.headerFilters }} - {{ newPresetFormattedStrings?.sort }} + {{ newPresetFormattedStrings.displayedColumns }} + {{ newPresetFormattedStrings.headerFilters }} + {{ newPresetFormattedStrings.sort }}
Date: Tue, 16 Jun 2026 16:59:36 +0200 Subject: [PATCH 19/65] phrase typo fix --- system/phrasesupdate.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/system/phrasesupdate.php b/system/phrasesupdate.php index 17b852eab..98d2833f6 100644 --- a/system/phrasesupdate.php +++ b/system/phrasesupdate.php @@ -58748,7 +58748,7 @@ I have been informed that I am under no obligation to consent to the transmissio 'phrases' => array( array( 'sprache' => 'German', - 'text' => 'Akutelle Einstellung speichern', + 'text' => 'Aktuelle Einstellung speichern', 'description' => '', 'insertvon' => 'system' ), From 2692806d4f272e7ea5cf73fe319df06fed5e58b2 Mon Sep 17 00:00:00 2001 From: adisposkofh Date: Tue, 16 Jun 2026 17:19:27 +0200 Subject: [PATCH 20/65] not syncing dev-provided general table presets --- public/js/components/filter/Filter/TablePresets.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/public/js/components/filter/Filter/TablePresets.js b/public/js/components/filter/Filter/TablePresets.js index 0805be874..dd7c73557 100644 --- a/public/js/components/filter/Filter/TablePresets.js +++ b/public/js/components/filter/Filter/TablePresets.js @@ -385,7 +385,9 @@ export default { } }, async applyPreset(preset) { - preset = this.syncPresetWithConfig(preset); + if (preset.id) { + preset = this.syncPresetWithConfig(preset); + } let columns = this.$props.tabulator .getColumnDefinitions() From c0ed296ba744f9fb7d809b56f0b1a17f73df9122 Mon Sep 17 00:00:00 2001 From: adisposkofh Date: Tue, 16 Jun 2026 17:21:15 +0200 Subject: [PATCH 21/65] Revert "phrase typo fix" This reverts commit a59f6085fe6323182613ba34a5dc7fc984d3f7c9. --- system/phrasesupdate.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/system/phrasesupdate.php b/system/phrasesupdate.php index 98d2833f6..17b852eab 100644 --- a/system/phrasesupdate.php +++ b/system/phrasesupdate.php @@ -58748,7 +58748,7 @@ I have been informed that I am under no obligation to consent to the transmissio 'phrases' => array( array( 'sprache' => 'German', - 'text' => 'Aktuelle Einstellung speichern', + 'text' => 'Akutelle Einstellung speichern', 'description' => '', 'insertvon' => 'system' ), From ff58f7a102802998b4b6ed6c1e20f6707f8cba3d Mon Sep 17 00:00:00 2001 From: adisposkofh Date: Tue, 16 Jun 2026 17:23:01 +0200 Subject: [PATCH 22/65] minor fix --- public/js/components/filter/Filter/TablePresets.js | 4 +--- system/phrasesupdate.php | 2 +- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/public/js/components/filter/Filter/TablePresets.js b/public/js/components/filter/Filter/TablePresets.js index dd7c73557..0805be874 100644 --- a/public/js/components/filter/Filter/TablePresets.js +++ b/public/js/components/filter/Filter/TablePresets.js @@ -385,9 +385,7 @@ export default { } }, async applyPreset(preset) { - if (preset.id) { - preset = this.syncPresetWithConfig(preset); - } + preset = this.syncPresetWithConfig(preset); let columns = this.$props.tabulator .getColumnDefinitions() diff --git a/system/phrasesupdate.php b/system/phrasesupdate.php index 17b852eab..98d2833f6 100644 --- a/system/phrasesupdate.php +++ b/system/phrasesupdate.php @@ -58748,7 +58748,7 @@ I have been informed that I am under no obligation to consent to the transmissio 'phrases' => array( array( 'sprache' => 'German', - 'text' => 'Akutelle Einstellung speichern', + 'text' => 'Aktuelle Einstellung speichern', 'description' => '', 'insertvon' => 'system' ), From c86d6f4bf3ce2c53f88347391d35f44c52586946 Mon Sep 17 00:00:00 2001 From: ma0068 Date: Tue, 7 Jul 2026 14:31:23 +0200 Subject: [PATCH 23/65] - enable opening in new tab with prevent.default - rendering of maillinks - useBCC without CC --- application/views/Studentenverwaltung.php | 1 + .../js/components/Stv/Studentenverwaltung.js | 4 +- .../Details/Kontaktieren.js | 91 +++++++++++++++++-- public/js/helpers/EmailHelpers.js | 78 +++++++++++++++- system/phrasesupdate.php | 20 ++++ 5 files changed, 181 insertions(+), 13 deletions(-) diff --git a/application/views/Studentenverwaltung.php b/application/views/Studentenverwaltung.php index 8dd2dd93d..c1a55c6bf 100644 --- a/application/views/Studentenverwaltung.php +++ b/application/views/Studentenverwaltung.php @@ -62,6 +62,7 @@ $configArray = [ logout-url="" :permissions="" :config="" + auth-uid="" >
diff --git a/public/js/components/Stv/Studentenverwaltung.js b/public/js/components/Stv/Studentenverwaltung.js index 66ff7f891..acb1b84f2 100644 --- a/public/js/components/Stv/Studentenverwaltung.js +++ b/public/js/components/Stv/Studentenverwaltung.js @@ -57,7 +57,8 @@ export default { url_mode: String, url_prestudent_id: String, url_tab: String, - url_studiengang: String + url_studiengang: String, + authUid: String }, provide() { return { @@ -91,6 +92,7 @@ export default { hasZGVMasterPermission: this.permissions['student/editMakkZgv'], hasZGVDoctorPermission: this.permissions['student/editDokZgv'], hasBismeldenPermission: this.permissions['student/editBismelden'], + authUid: this.authUid } }, diff --git a/public/js/components/Stv/Studentenverwaltung/Details/Kontaktieren.js b/public/js/components/Stv/Studentenverwaltung/Details/Kontaktieren.js index d075b5a82..3d2b8ed80 100644 --- a/public/js/components/Stv/Studentenverwaltung/Details/Kontaktieren.js +++ b/public/js/components/Stv/Studentenverwaltung/Details/Kontaktieren.js @@ -1,4 +1,5 @@ -import { splitMailsHelper } from "../../../../helpers/EmailHelpers.js" +import { splitMailLinks } from "../../../../helpers/EmailHelpers.js"; + export default { name: "Kontaktieren", computed: { @@ -21,7 +22,18 @@ export default { props: { modelValue: Object }, - + inject: { + authUid: { + from: 'authUid', + required: true + }, + }, + data(){ + return { + mailLinks: [], + showMailDialog: false + } + }, methods: { internMail(event) { if (this.internMails.length) @@ -34,25 +46,84 @@ export default { { splitMailsHelper(this.privateMails, event, null, this.$fhcAlert, this.$p) } + }, + createMailLinks(event, mailGroup){ + let recipients = mailGroup == 'private' ? this.privateMails : this.internMails; + this.mailLinks = splitMailLinks( + recipients, + "", + event.ctrlKey || event.metaKey, + this.authUid + ); + + this.showMailDialog = true; + }, + handleMailLink(event){ + window.open(event.target.href, '_blank', 'noopener'); + }, + reset(){ + this.showMailDialog = false; } }, template: ` -
-
-
+
+ +
-
- -
+ +
-
-
` + +
+
+

{{$p.t('stv', 'zuvieleEMails')}}

+ + + + + +
+ +
+ ` }; \ No newline at end of file diff --git a/public/js/helpers/EmailHelpers.js b/public/js/helpers/EmailHelpers.js index 87daa828a..47aff573d 100644 --- a/public/js/helpers/EmailHelpers.js +++ b/public/js/helpers/EmailHelpers.js @@ -40,6 +40,80 @@ export async function splitMailsHelper(mails, event, subject, alertPluginRef, ph window.location.href = mailLink; } } - } -} \ No newline at end of file +} + + +/** + * just splits the list of mails + * + * @param mails emailadresses + * @param subject subject + * @param useBcc useBcc + * @returns array of links for splitted mails + */ +export function splitMailLinks(mails, subject = "", useBcc = false, uidCC= "", maxLength = 2024) { + if (!Array.isArray(mails) || mails.length === 0) { + return []; + } + + const separator = ","; + const encodedSubject = subject ? encodeURIComponent(subject) : ""; + + // reserve space for the subject parameter + const subjectLength = encodedSubject + ? `?subject=${encodedSubject}`.length + : 0; + + const limit = maxLength - subjectLength; + + const links = []; + let currentRecipients = []; + + for (const mail of mails) { + if (!mail) continue; + + const testRecipients = [...currentRecipients, mail]; + const recipientString = testRecipients.join(separator); + + if (recipientString.length > limit && currentRecipients.length > 0) { + + links.push(createMailto( + currentRecipients.join(separator), + encodedSubject, + useBcc, + uidCC + )); + + currentRecipients = [mail]; + } + else { + currentRecipients.push(mail); + } + } + + if (currentRecipients.length > 0) { + links.push(createMailto( + currentRecipients.join(separator), + encodedSubject, + useBcc, + uidCC + )); + } + + return links; +} + +function createMailto(recipients, encodedSubject, useBcc, uidCC) { + let mailadressUid = uidCC + '@technikum-wien.at'; + + let link = useBcc + ? `mailto:${encodeURIComponent(mailadressUid)}?cc=${encodeURIComponent(mailadressUid)}&bcc=${encodeURIComponent(recipients)}` + : `mailto:${encodeURIComponent(recipients)}`; + + if (encodedSubject) { + link += `${useBcc ? "&" : "?"}subject=${encodedSubject}`; + } + + return link; +} diff --git a/system/phrasesupdate.php b/system/phrasesupdate.php index 92a05156b..be53ff438 100644 --- a/system/phrasesupdate.php +++ b/system/phrasesupdate.php @@ -58197,6 +58197,26 @@ I have been informed that I am under no obligation to consent to the transmissio ) ), // ### Phrases Dashboard Admin END + array( + 'app' => 'core', + 'category' => 'ui', + 'phrase' => 'openInMailClient', + 'insertvon' => 'system', + 'phrases' => array( + array( + 'sprache' => 'German', + 'text' => 'In Mailanwendung öffnen', + 'description' => '', + 'insertvon' => 'system' + ), + array( + 'sprache' => 'English', + 'text' => 'Open in Mailclient', + 'description' => '', + 'insertvon' => 'system' + ) + ) + ), ); From 68292b60afe4b56ce211da9bae55157b0d3baa5b Mon Sep 17 00:00:00 2001 From: ma0068 Date: Mon, 20 Jul 2026 17:36:54 +0200 Subject: [PATCH 24/65] add Tags Incoming and Preabbrecher --- application/config/stv.php | 2 + .../libraries/tags/CoreIncomingTagLib.php | 63 +++++++++++++++ .../libraries/tags/CorePreabbrecherTagLib.php | 80 +++++++++++++++++++ application/models/codex/Bisio_model.php | 53 ++++++++++++ public/css/tags.css | 9 +++ .../75959_StudVw_Automatische_Tags.php | 22 ++++- 6 files changed, 225 insertions(+), 4 deletions(-) create mode 100644 application/libraries/tags/CoreIncomingTagLib.php create mode 100644 application/libraries/tags/CorePreabbrecherTagLib.php diff --git a/application/config/stv.php b/application/config/stv.php index 3090e3f2e..4893560e0 100644 --- a/application/config/stv.php +++ b/application/config/stv.php @@ -150,4 +150,6 @@ $config['stv_prestudent_tags'] = [ 'unterbrecher_auto' => ['readonly' => true], 'stbtr_erh_auto' => ['readonly' => true], 'jgv_auto' => ['readonly' => true], + 'in_auto' => ['readonly' => true], + 'preabbr_auto' => ['readonly' => true], ]; diff --git a/application/libraries/tags/CoreIncomingTagLib.php b/application/libraries/tags/CoreIncomingTagLib.php new file mode 100644 index 000000000..26d7dad0b --- /dev/null +++ b/application/libraries/tags/CoreIncomingTagLib.php @@ -0,0 +1,63 @@ +ci = get_instance(); + $this->ci->load->model('codex/Bisio_model', 'BisioModel'); + } + + public function getZuordnungIds(array $params) + { + if(!isset($params['studiensemester_kurzbz'])) + { + return (object) array( + 'idArray' => [] + ); + } + + $semester = $params['studiensemester_kurzbz']; + + $result = $this->ci->BisioModel->getIncomingsOfSemester($semester); + + $data = $result->retval; + + $outgoing_data = array_map(function($item) { + return [ + 'id' => $item->prestudent_id, + 'von' => $item->von, + 'bis' => $item->bis + ]; + }, $data); + + return (object) array( + 'data' => $outgoing_data, + 'typeId' => 'prestudent_id', + ); + } + + public function isCriteriaSetFor(array $params) + { + if ( !isset($params['id'], $params['studiensemester_kurzbz'], $params['typeId']) || $params['typeId'] !== 'prestudent_id') + return false; + + $semester = $params['studiensemester_kurzbz']; + $prestudent_id = $params['id']; + + $result = $this->ci->BisioModel->isPrestudentIncoming($semester, $prestudent_id); + + if (hasData($result)) { + return $result; + } + else + return null; + } + +} diff --git a/application/libraries/tags/CorePreabbrecherTagLib.php b/application/libraries/tags/CorePreabbrecherTagLib.php new file mode 100644 index 000000000..41712ac3b --- /dev/null +++ b/application/libraries/tags/CorePreabbrecherTagLib.php @@ -0,0 +1,80 @@ +ci = get_instance(); + $this->ci->load->model('education/Studierendenantrag_model', 'StudierendenantragModel'); + } + + public function getZuordnungIds(array $params) + { + if(!isset($params['studiensemester_kurzbz'])) + { + return (object) array( + 'idArray' => [] + ); + } + + $semester = $params['studiensemester_kurzbz']; + + $this->ci->StudierendenantragModel->addJoin('public.tbl_studiensemester', 'studiensemester_kurzbz'); + $this->ci->StudierendenantragModel->db->where_in('typ', self::TYP_ABMELDUNG); + $this->ci->StudierendenantragModel->db->where('studiensemester_kurzbz', $semester); + $result = $this->ci->StudierendenantragModel->getWithLastStatusWhere([ + 'studierendenantrag_statustyp_kurzbz' => Studierendenantragstatus_model::STATUS_CREATED + ]); + + $data = $result->retval; + + $prewiederholer_data = array_map(function($item) { + return [ + 'id' => $item->prestudent_id, + 'von' => $item->start, + 'bis' => $item->ende + ]; + }, $data); + + return (object) array( + 'data' => $prewiederholer_data, + 'typeId' => 'prestudent_id', + ); + } + + public function isCriteriaSetFor(array $params) + { + if ( !isset($params['id'], $params['studiensemester_kurzbz'], $params['typeId']) || $params['typeId'] !== 'prestudent_id') + return false; + + $semester = $params['studiensemester_kurzbz']; + $prestudent_id = $params['id']; + + $this->ci->StudierendenantragModel->addSelect('prestudent_id'); + $this->ci->StudierendenantragModel->addSelect('start as von'); + $this->ci->StudierendenantragModel->addSelect('ende as bis'); + + $this->ci->StudierendenantragModel->addJoin('public.tbl_studiensemester', 'studiensemester_kurzbz'); + $this->ci->StudierendenantragModel->db->where_in('typ', self::TYP_ABMELDUNG); + $this->ci->StudierendenantragModel->db->where('studiensemester_kurzbz', $semester); + $this->ci->StudierendenantragModel->db->where('prestudent_id', $prestudent_id); + $result = $this->ci->StudierendenantragModel->getWithLastStatusWhere([ + 'studierendenantrag_statustyp_kurzbz' => Studierendenantragstatus_model::STATUS_CREATED + ]); + + if(hasData($result)) + { + return $result; + } + else + return null; + } + +} diff --git a/application/models/codex/Bisio_model.php b/application/models/codex/Bisio_model.php index 9a9fd37fb..972661c8b 100644 --- a/application/models/codex/Bisio_model.php +++ b/application/models/codex/Bisio_model.php @@ -73,6 +73,34 @@ class Bisio_model extends DB_Model return $this->execQuery($query, array($studiensemester_kurzbz)); } + /** + * Gets incoming students of certain Semester + * @param String studiensemester_kurzbz + * @return array of prestudent_ids + */ + public function getIncomingsOfSemester($studiensemester_kurzbz) + { + $query = " + SELECT DISTINCT ps.prestudent_id, tbl_bisio.von, tbl_bisio.bis + FROM bis.tbl_bisio + JOIN public.tbl_student USING (student_uid) + JOIN public.tbl_prestudent ps USING (prestudent_id) + JOIN public.tbl_prestudentstatus pss ON (ps.prestudent_id = pss.prestudent_id) + JOIN public.tbl_studiensemester ss ON (pss.studiensemester_kurzbz = ss.studiensemester_kurzbz) + WHERE ss.studiensemester_kurzbz = ? + AND nation_code = 'A' + AND ( + tbl_bisio.von <= ss.ende + AND ( + tbl_bisio.bis >= ss.start + OR tbl_bisio.bis IS NULL + ) + ) + "; + + return $this->execQuery($query, array($studiensemester_kurzbz)); + } + public function isPrestudentOutgoing($studiensemester_kurzbz, $prestudent_id) { $query = " @@ -97,4 +125,29 @@ class Bisio_model extends DB_Model return $this->execQuery($query, array($studiensemester_kurzbz, $prestudent_id)); } + + public function isPrestudentIncoming($studiensemester_kurzbz, $prestudent_id) + { + $query = " + SELECT + ps.prestudent_id, tbl_bisio.von, tbl_bisio.bis + FROM bis.tbl_bisio + JOIN public.tbl_student USING (student_uid) + JOIN public.tbl_prestudent ps USING (prestudent_id) + JOIN public.tbl_prestudentstatus pss ON (ps.prestudent_id = pss.prestudent_id) + JOIN public.tbl_studiensemester ss ON (pss.studiensemester_kurzbz = ss.studiensemester_kurzbz) + WHERE ss.studiensemester_kurzbz = ? + AND nation_code = 'A' + AND ( + tbl_bisio.von <= ss.ende + AND ( + tbl_bisio.bis >= ss.start + OR tbl_bisio.bis IS NULL + ) + ) + AND ps.prestudent_id = ? + "; + + return $this->execQuery($query, array($studiensemester_kurzbz, $prestudent_id)); + } } diff --git a/public/css/tags.css b/public/css/tags.css index 3b3377d5d..0634c5ce3 100644 --- a/public/css/tags.css +++ b/public/css/tags.css @@ -91,6 +91,15 @@ color: black; } +.tag_skyblue { + background-color: #90D5FF; + color: black; +} +.tag_blush { + background-color: #A83E3E; + color: white; +} + .tag_done { text-decoration: line-through; } diff --git a/system/dbupdate_3.4/75959_StudVw_Automatische_Tags.php b/system/dbupdate_3.4/75959_StudVw_Automatische_Tags.php index 1b6d6b1dd..1954ae48d 100644 --- a/system/dbupdate_3.4/75959_StudVw_Automatische_Tags.php +++ b/system/dbupdate_3.4/75959_StudVw_Automatische_Tags.php @@ -16,7 +16,7 @@ if(!@$db->db_query("SELECT taglib FROM public.tbl_notiz_typ LIMIT 1")) $tags = [ [ 'typ_kurzbz' => 'wh_auto', - 'bezeichnung' => '{WH, R}', + 'bezeichnung' => '{WH, WH}', 'style' => 'tag_dark_grey', 'prioritaet' => 11, 'taglib' => 'tags/CoreWiederholerTagLib' @@ -30,14 +30,14 @@ $tags = [ ], [ 'typ_kurzbz' => 'out_auto', - 'bezeichnung' => '{Out, Out}', + 'bezeichnung' => '{Outgoing, Outgoing}', 'style' => 'tag_limette', 'prioritaet' => 12, 'taglib' => 'tags/CoreOutgoingTagLib' ], [ 'typ_kurzbz' => 'prewh_auto', - 'bezeichnung' => '{Pre-WH, pre-R}', + 'bezeichnung' => '{Pre-WH, pre-WH}', 'style' => 'tag_light_grey', 'prioritaet' => 13, 'taglib' => 'tags/CorePrewiederholerTagLib' @@ -51,7 +51,7 @@ $tags = [ ], [ 'typ_kurzbz' => 'unterbrecher_auto', - 'bezeichnung' => '{UB, I}', + 'bezeichnung' => '{UB, UB}', 'style' => 'tag_turquoise', 'prioritaet' => 15, 'taglib' => 'tags/CoreUnterbrecherTagLib' @@ -70,6 +70,20 @@ $tags = [ 'prioritaet' => 17, 'taglib' => 'tags/CoreJgvTagLib' ], + [ + 'typ_kurzbz' => 'preabbr_auto', + 'bezeichnung' => '{Pre-Abbr, pre-Abbr}', + 'style' => 'tag_blush', + 'prioritaet' => 18, + 'taglib' => 'tags/CorePreabbrecherTagLib' + ], + [ + 'typ_kurzbz' => 'in_auto', + 'bezeichnung' => '{Incoming, Incoming}', + 'style' => 'tag_skyblue', + 'prioritaet' => 19, + 'taglib' => 'tags/CoreIncomingTagLib' + ], ]; From 7cda22f8079f4a9ba425b13d9e5fc583a72377fa Mon Sep 17 00:00:00 2001 From: ma0068 Date: Tue, 21 Jul 2026 10:41:27 +0200 Subject: [PATCH 25/65] use language index for tags --- .../api/frontend/v1/stv/Students.php | 48 +++++++++++++------ application/core/Tag_Controller.php | 12 +++-- application/libraries/stv/StudentListLib.php | 8 ++-- 3 files changed, 46 insertions(+), 22 deletions(-) diff --git a/application/controllers/api/frontend/v1/stv/Students.php b/application/controllers/api/frontend/v1/stv/Students.php index e77e20889..f822c7205 100644 --- a/application/controllers/api/frontend/v1/stv/Students.php +++ b/application/controllers/api/frontend/v1/stv/Students.php @@ -103,6 +103,8 @@ class Students extends FHCAPI_Controller */ public function getIncoming($studiensemester_kurzbz) { + $index_bezeichnung_mehrsprachig = $this->_getLanguageIndex(); + $this->addMeta('ci_method', __FUNCTION__); $this->addMeta('ci_params', [ 'studiensemester_kurzbz' => $studiensemester_kurzbz @@ -135,7 +137,7 @@ class Students extends FHCAPI_Controller $this->addFilter($studiensemester_kurzbz); - $result = $this->studentlistlib->execute($studiensemester_kurzbz); + $result = $this->studentlistlib->execute($studiensemester_kurzbz, $index_bezeichnung_mehrsprachig); $data = $this->getDataOrTerminateWithError($result); $this->decodeTagsJsonInResult($data); @@ -149,6 +151,8 @@ class Students extends FHCAPI_Controller */ public function getOutgoing($studiensemester_kurzbz) { + $index_bezeichnung_mehrsprachig = $this->_getLanguageIndex(); + $this->addMeta('ci_method', __FUNCTION__); $this->addMeta('ci_params', [ 'studiensemester_kurzbz' => $studiensemester_kurzbz @@ -194,7 +198,7 @@ class Students extends FHCAPI_Controller $this->addFilter($studiensemester_kurzbz); - $result = $this->studentlistlib->execute($studiensemester_kurzbz); + $result = $this->studentlistlib->execute($studiensemester_kurzbz, $index_bezeichnung_mehrsprachig); $data = $this->getDataOrTerminateWithError($result); $this->decodeTagsJsonInResult($data); @@ -208,6 +212,7 @@ class Students extends FHCAPI_Controller */ public function getGemeinsamestudien($studiensemester_kurzbz) { + $index_bezeichnung_mehrsprachig = $this->_getLanguageIndex(); $this->addMeta('ci_method', __FUNCTION__); $this->addMeta('ci_params', [ 'studiensemester_kurzbz' => $studiensemester_kurzbz @@ -239,7 +244,7 @@ class Students extends FHCAPI_Controller $this->addFilter($studiensemester_kurzbz); - $result = $this->studentlistlib->execute($studiensemester_kurzbz); + $result = $this->studentlistlib->execute($studiensemester_kurzbz, $index_bezeichnung_mehrsprachig); $data = $this->getDataOrTerminateWithError($result); $this->decodeTagsJsonInResult($data); @@ -290,6 +295,8 @@ class Students extends FHCAPI_Controller { $stdsemEsc = $studiensemester_kurzbz ? $this->PrestudentModel->escape($studiensemester_kurzbz) : 'NULL'; + $index_bezeichnung_mehrsprachig = $this->_getLanguageIndex(); + $selectRT = " SELECT 1 FROM public.tbl_rt_person @@ -444,7 +451,7 @@ class Students extends FHCAPI_Controller $this->addFilter($studiensemester_kurzbz); - $result = $this->studentlistlib->execute($studiensemester_kurzbz); + $result = $this->studentlistlib->execute($studiensemester_kurzbz, $index_bezeichnung_mehrsprachig); $data = $this->getDataOrTerminateWithError($result); $this->decodeTagsJsonInResult($data); @@ -547,6 +554,7 @@ class Students extends FHCAPI_Controller $gruppe_kurzbz = null, $orgform_kurzbz = null ) { + $index_bezeichnung_mehrsprachig = $this->_getLanguageIndex(); $this->load->model('organisation/Studiensemester_model', 'StudiensemesterModel'); if (!$this->StudiensemesterModel->isValidStudiensemester($studiensemester_kurzbz)) @@ -591,10 +599,8 @@ class Students extends FHCAPI_Controller } } - $this->addFilter($studiensemester_kurzbz); - - $result = $this->studentlistlib->execute($studiensemester_kurzbz); + $result = $this->studentlistlib->execute($studiensemester_kurzbz, $index_bezeichnung_mehrsprachig); $data = $this->getDataOrTerminateWithError($result); $this->decodeTagsJsonInResult($data); @@ -620,6 +626,7 @@ class Students extends FHCAPI_Controller */ public function getPrestudent($studiensemester_kurzbz, $prestudent_id) { + $index_bezeichnung_mehrsprachig = $this->_getLanguageIndex(); $this->addMeta('ci_method', __FUNCTION__); $this->addMeta('ci_params', array( 'studiensemester_kurzbz' => $studiensemester_kurzbz, @@ -647,10 +654,8 @@ class Students extends FHCAPI_Controller $this->studentlistlib->addWhere('tbl_prestudent.prestudent_id', $prestudent_id); - $this->addFilter($studiensemester_kurzbz); - - $result = $this->studentlistlib->execute($studiensemester_kurzbz); + $result = $this->studentlistlib->execute($studiensemester_kurzbz, $index_bezeichnung_mehrsprachig); $data = $this->getDataOrTerminateWithError($result); $this->decodeTagsJsonInResult($data); @@ -664,6 +669,7 @@ class Students extends FHCAPI_Controller */ public function getStudent($studiensemester_kurzbz, $student_uid) { + $index_bezeichnung_mehrsprachig = $this->_getLanguageIndex(); $this->addMeta('ci_method', __FUNCTION__); $this->addMeta('ci_params', array( 'studiensemester_kurzbz' => $studiensemester_kurzbz, @@ -679,11 +685,9 @@ class Students extends FHCAPI_Controller $this->studentlistlib->addWhere('s.student_uid', $student_uid); - $this->addFilter($studiensemester_kurzbz); - - $result = $this->studentlistlib->execute($studiensemester_kurzbz); + $result = $this->studentlistlib->execute($studiensemester_kurzbz, $index_bezeichnung_mehrsprachig); $data = $this->getDataOrTerminateWithError($result); $this->decodeTagsJsonInResult($data); @@ -698,6 +702,8 @@ class Students extends FHCAPI_Controller */ public function getPerson($studiensemester_kurzbz, $person_id) { + $index_bezeichnung_mehrsprachig = $this->_getLanguageIndex(); + $this->addMeta('ci_method', __FUNCTION__); $this->addMeta('ci_params', array( 'studiensemester_kurzbz' => $studiensemester_kurzbz, @@ -717,7 +723,7 @@ class Students extends FHCAPI_Controller $this->addFilter($studiensemester_kurzbz); - $result = $this->studentlistlib->execute($studiensemester_kurzbz); + $result = $this->studentlistlib->execute($studiensemester_kurzbz, $index_bezeichnung_mehrsprachig); $data = $this->getDataOrTerminateWithError($result); $this->decodeTagsJsonInResult($data); @@ -731,6 +737,7 @@ class Students extends FHCAPI_Controller */ public function search($studiensemester_kurzbz) { + $index_bezeichnung_mehrsprachig = $this->_getLanguageIndex(); $this->addMeta('ci_method', __FUNCTION__); $this->addMeta('ci_params', array( 'studiensemester_kurzbz' => $studiensemester_kurzbz @@ -786,7 +793,7 @@ class Students extends FHCAPI_Controller $this->addFilter($studiensemester_kurzbz); - $result = $this->studentlistlib->execute($studiensemester_kurzbz); + $result = $this->studentlistlib->execute($studiensemester_kurzbz, $index_bezeichnung_mehrsprachig); $data = $this->getDataOrTerminateWithError($result); $this->decodeTagsJsonInResult($data); @@ -818,4 +825,15 @@ class Students extends FHCAPI_Controller } } } + + private function _getLanguageIndex() + { + $this->load->model('system/Sprache_model', 'SpracheModel'); + $this->SpracheModel->addSelect('index'); + $result = $this->SpracheModel->loadWhere(array('sprache' => getUserLanguage())); + $language = hasData($result) ? getData($result)[0]->index : 1; + $index_bezeichnung_mehrsprachig = $language - 1; + + return $index_bezeichnung_mehrsprachig; + } } diff --git a/application/core/Tag_Controller.php b/application/core/Tag_Controller.php index c4c5afa88..0600200c8 100644 --- a/application/core/Tag_Controller.php +++ b/application/core/Tag_Controller.php @@ -42,6 +42,9 @@ class Tag_Controller extends FHCAPI_Controller public function getTag($readonly_tags = null) { + $language = $this->_getLanguageIndex(); + $index_bezeichnung_mehrsprachig = $language - 1; + $id = $this->input->get('id'); if (is_array($readonly_tags) && !isEmptyArray($readonly_tags)) @@ -66,7 +69,7 @@ class Tag_Controller extends FHCAPI_Controller $this->NotizModel->addSelect( "tbl_notiz.titel, tbl_notiz.text, - array_to_json(bezeichnung_mehrsprachig::varchar[])->>0 as bezeichnung, + array_to_json(bezeichnung_mehrsprachig::varchar[])->>". $index_bezeichnung_mehrsprachig . " as bezeichnung, tbl_notiz.notiz_id, tbl_notiz_typ.style, tbl_notiz_typ.automatisiert, @@ -96,10 +99,11 @@ class Tag_Controller extends FHCAPI_Controller public function getTags($tags = null) { $language = $this->_getLanguageIndex(); + $index_bezeichnung_mehrsprachig = $language - 1; $this->NotiztypModel->addSelect( "typ_kurzbz as tag_typ_kurzbz, - array_to_json(bezeichnung_mehrsprachig::varchar[])->>0 as bezeichnung, + array_to_json(bezeichnung_mehrsprachig::varchar[])->>". $index_bezeichnung_mehrsprachig . " as bezeichnung, style, beschreibung, tag, @@ -283,6 +287,8 @@ class Tag_Controller extends FHCAPI_Controller } public function getAllTags($readonly_tags = false){ + $language = $this->_getLanguageIndex(); + $index_bezeichnung_mehrsprachig = $language - 1; $prestudent_id = $this->input->get('prestudent_id'); //TODO check for readonly: necessary? @@ -307,7 +313,7 @@ class Tag_Controller extends FHCAPI_Controller $this->NotizModel->addSelect( "tbl_notiz.titel, tbl_notiz.text, - array_to_json(bezeichnung_mehrsprachig::varchar[])->>0 as bezeichnung, + array_to_json(bezeichnung_mehrsprachig::varchar[])->>". $index_bezeichnung_mehrsprachig . " as bezeichnung, tbl_notiz.notiz_id, tbl_notiz_typ.style, tbl_notiz_typ.automatisiert, diff --git a/application/libraries/stv/StudentListLib.php b/application/libraries/stv/StudentListLib.php index 432b75bd4..de84cda0b 100644 --- a/application/libraries/stv/StudentListLib.php +++ b/application/libraries/stv/StudentListLib.php @@ -251,9 +251,9 @@ class StudentListLib * * @return stdClass result of the query */ - public function execute($studiensemester_kurzbz) + public function execute($studiensemester_kurzbz, $index_bezeichnung_mehrsprachig=0) { - $this->addSelectAndJoinForTagsIfConfigured($studiensemester_kurzbz); + $this->addSelectAndJoinForTagsIfConfigured($studiensemester_kurzbz, $index_bezeichnung_mehrsprachig); $stdsemEsc = $studiensemester_kurzbz ? $this->_ci->PrestudentModel->escape($studiensemester_kurzbz) : 'NULL'; @@ -302,7 +302,7 @@ class StudentListLib * @param string studiensemester_kurzbz * */ - protected function addSelectAndJoinForTagsIfConfigured($studiensemester_kurzbz) + protected function addSelectAndJoinForTagsIfConfigured($studiensemester_kurzbz, $index_bezeichnung_mehrsprachig) { if (defined('STV_TAGS_ENABLED') && STV_TAGS_ENABLED) { $this->_ci->load->config('stv'); @@ -326,7 +326,7 @@ class StudentListLib SELECT DISTINCT ON (n.notiz_id) n.notiz_id AS id, nt.typ_kurzbz, - array_to_json(nt.bezeichnung_mehrsprachig)->>0 AS beschreibung, + array_to_json(nt.bezeichnung_mehrsprachig)->>". $index_bezeichnung_mehrsprachig . " AS beschreibung, n.text AS notiz, nt.style, n.erledigt AS done, From 1ae02d8ed8bcc6f9afe71713f468e21b31f0014f Mon Sep 17 00:00:00 2001 From: ma0068 Date: Tue, 21 Jul 2026 13:35:31 +0200 Subject: [PATCH 26/65] refactor addTaginTable and deleteTagInTable, add missing logic for automated tags in case of manual adding --- public/js/helpers/TagHelper.js | 67 +++++++++++++++++++++++---- public/js/tabulator/formatter/tags.js | 4 ++ 2 files changed, 61 insertions(+), 10 deletions(-) diff --git a/public/js/helpers/TagHelper.js b/public/js/helpers/TagHelper.js index 9282aa167..0573858d9 100644 --- a/public/js/helpers/TagHelper.js +++ b/public/js/helpers/TagHelper.js @@ -6,9 +6,52 @@ export function addTagInTable(addedTag, rows, matchKey, tagsKey = "tags") rows.forEach(row => { const rowData = row.getData(); - let updated = false; - addedTag.response.forEach(tag => + //add save check if String or Array and avoid same reference later for mutations + let raw = rowData[tagsKey]; + let tags = typeof raw === "string" + ? JSON.parse(raw || "[]") + : Array.isArray(raw) + ? [...raw] + : []; + + let updated = false; + for (const tag of addedTag.response) + { + if (rowData[matchKey] !== tag[matchKey]) + continue; + + //avoid double inserts + if (tags.some(x => x.id === tag.id)) + continue; + + const newTag = { + id: tag.id, + prestudent_id: tag.prestudent_id, + //add also information of addedTag + beschreibung: addedTag.beschreibung, + notiz: addedTag.notiz ?? "", + style: addedTag.style, + done: addedTag.done ?? false, + typ_kurzbz: addedTag.tag_typ_kurzbz ?? addedTag.typ_kurzbz, //here seem to be 2 variations + automatisiert: addedTag.automatisiert + }; + + tags.unshift(newTag); + updated = true; + } + + if (updated) + { + row.update({ + [tagsKey]: JSON.stringify(tags) + }); + + row.reformat(); + } + }); + +/* addedTag.response.forEach(tag => { if (rowData[matchKey] !== tag[matchKey]) return; @@ -36,7 +79,7 @@ export function addTagInTable(addedTag, rows, matchKey, tagsKey = "tags") if (updated) row.update(rowData); - }); + });*/ } export function deleteTagInTable(deletedTag, rows, tagsKeys = ['tags']) @@ -49,26 +92,30 @@ export function deleteTagInTable(deletedTag, rows, tagsKeys = ['tags']) let updates = {}; let changed = false; - tagsKeys.forEach(key => { - let tags; + for (const key of tagsKeys) { + let raw = rowData[key]; + let tags = []; try { - tags = JSON.parse(rowData[key] || "[]"); + if (typeof raw === "string") { + tags = JSON.parse(raw || "[]"); + } else if (Array.isArray(raw)) { + tags = [...raw]; + } } catch (e) { tags = []; } if (!Array.isArray(tags)) - return; + continue; let filtered = tags.filter(tag => tag?.id !== deletedTag); - if (filtered.length !== tags.length) - { + if (filtered.length !== tags.length) { updates[key] = JSON.stringify(filtered); changed = true; } - }); + } if (changed) { row.update(updates); diff --git a/public/js/tabulator/formatter/tags.js b/public/js/tabulator/formatter/tags.js index 367a8a5fb..f334bb8df 100644 --- a/public/js/tabulator/formatter/tags.js +++ b/public/js/tabulator/formatter/tags.js @@ -49,6 +49,10 @@ export function tagFormatter(cell, tagComponent) tagElement.title = tag.notiz; tagElement.className = "tag " + tag.style; if (tag.done) tagElement.className += " tag_done"; + if (tag.automatisiert){ + tagElement.className += " tag_auto"; + tagElement.innerHTML = " " + tag.beschreibung; + } const tagDef = mappedData.find(t => t.typ_kurzbz === tag.typ_kurzbz); From a8dd99519515f91a1f7d769872d0e31cae0182ce Mon Sep 17 00:00:00 2001 From: ma0068 Date: Wed, 22 Jul 2026 14:11:07 +0200 Subject: [PATCH 27/65] rebuild tags for whole studiengang / selection refactor backend for multiaction rebuildTags add phrases refactor haederFilter --- application/core/Tag_Controller.php | 21 ++- .../components/DetailHeader/DetailHeader.js | 4 +- .../Stv/Studentenverwaltung/List.js | 69 +++++++++- .../Studentenverwaltung/List/ModalLoading.js | 69 ++++++++++ system/phrasesupdate.php | 123 ++++++++++++++++++ 5 files changed, 278 insertions(+), 8 deletions(-) create mode 100644 public/js/components/Stv/Studentenverwaltung/List/ModalLoading.js diff --git a/application/core/Tag_Controller.php b/application/core/Tag_Controller.php index 0600200c8..cb7deda82 100644 --- a/application/core/Tag_Controller.php +++ b/application/core/Tag_Controller.php @@ -344,16 +344,27 @@ class Tag_Controller extends FHCAPI_Controller public function rebuildTagsForTypeId() { - $id = $this->input->post('id'); + $ids = $this->input->post('ids'); $typeId = $this->input->post('typeId'); $semester = $this->input->post('sem'); - $result = $this->taglib->rebuildTagsForTypeId($typeId, $id, $semester); + $idsSuccess = []; + $idsError = []; + $outputResult = []; - if (isError($result)) - return error ('Error occurred during updateAutomatedTags'); + foreach ($ids as $id) + { + $result = $this->taglib->rebuildTagsForTypeId($typeId, $id, $semester); - $this->terminateWithSuccess($result); + if (isError($result)) + $idsError[] = $id; + else + $idsSuccess[] = $id; + + $outputResult[] = $result; + } + + $this->terminateWithSuccess([$outputResult, $idsSuccess, $idsError]); } private function _setAuthUID() diff --git a/public/js/components/DetailHeader/DetailHeader.js b/public/js/components/DetailHeader/DetailHeader.js index 84e462df3..8bf1fe2be 100644 --- a/public/js/components/DetailHeader/DetailHeader.js +++ b/public/js/components/DetailHeader/DetailHeader.js @@ -315,8 +315,10 @@ export default { this.reload(); }, rebuildPrestudentTags(){ + const id = [this.headerData[0].prestudent_id]; + const params = { - id : this.headerData[0].prestudent_id, + ids: id, typeId: 'prestudent_id', sem: this.currentSemester }; diff --git a/public/js/components/Stv/Studentenverwaltung/List.js b/public/js/components/Stv/Studentenverwaltung/List.js index 56a6a5334..f9abb3558 100644 --- a/public/js/components/Stv/Studentenverwaltung/List.js +++ b/public/js/components/Stv/Studentenverwaltung/List.js @@ -4,6 +4,7 @@ import CoreTag from '../../Tag/Tag.js'; import { tagHeaderFilter } from "../../../tabulator/filters/extendedHeaderFilter.js"; import { addTagInTable, deleteTagInTable, updateTagInTable } from "../../../../js/helpers/TagHelper.js"; import { tagFormatter } from "../../../../js/tabulator/formatter/tags.js"; +import ModalLoading from "./List/ModalLoading.js"; import ApiTag from "../../../api/factory/stv/tag.js"; import ListFilter from './List/Filter.js'; @@ -18,7 +19,8 @@ export default { CoreFilterCmpt, ListNew, CoreTag, - ListFilter + ListFilter, + ModalLoading }, directives: { draggable @@ -246,7 +248,9 @@ export default { oldScrollUrl: '', oldScrollLeft: 0, oldScrollTop: 0, - allPrestudents: [] + allPrestudents: [], + rebuildData: [], + isLoading: false } }, computed: { @@ -646,11 +650,55 @@ export default { this.oldScrollUrl = this.$refs.table.tabulator.getAjaxUrl(); } }, + rebuild(selected){ + this.isLoading = true; + let prestudentIds = []; + this.showModal(); + if(!selected.length) + prestudentIds = this.$refs.table.tabulator + .getData() + .map(row => row.prestudent_id); + else + prestudentIds = selected.map(item => item.prestudent_id); + + const params = { + ids: prestudentIds, + typeId: 'prestudent_id', + sem: this.studiensemesterKurzbz + }; + + return this.$api + .call(ApiTag.rebuildTagsforTypeId(params)) + .then(result => { + this.rebuildData = result.data; + this.isLoading = false; + console.log("Rebuild manually triggered"); + if(this.rebuildData[1].length > 0) + { + this.$fhcAlert.alertSuccess(this.$p.t('tag', 'alertSuccessRebuild', { count: this.rebuildData[1].length })); + } + if(this.rebuildData[2].length > 0) + { + this.$fhcAlert.alertError(this.$p.t('tag', 'alertErrorRebuild') + this.rebuildData[2].toString()); + } + }) + .catch(this.$fhcAlert.handleSystemError); + }, + showModal(){ + this.$refs.modalLocked.open(); + }, }, // TODO(chris): focusin, focusout, keydown and tabindex should be in the filter component // TODO(chris): filter component column chooser has no accessibilty features template: `
+ + +
+ + +