diff --git a/application/controllers/api/frontend/v1/stv/Config.php b/application/controllers/api/frontend/v1/stv/Config.php index 922c6a8e7..5d076ffc4 100644 --- a/application/controllers/api/frontend/v1/stv/Config.php +++ b/application/controllers/api/frontend/v1/stv/Config.php @@ -33,6 +33,8 @@ class Config extends FHCAPI_Controller { // TODO(chris): permissions parent::__construct([ + 'get' => ['admin:r', 'assistenz:r'], + 'set' => ['admin:r', 'assistenz:r'], 'filter' => ['admin:r', 'assistenz:r'], 'student' => ['admin:r', 'assistenz:r'], 'students' => ['admin:r', 'assistenz:r'] @@ -55,6 +57,95 @@ class Config extends FHCAPI_Controller } /** + * get App config + */ + public function get() + { + $this->load->model('system/Variable_model', 'VariableModel'); + $this->load->config('stv'); + + $config = []; + + #number_displayed_past_studiensemester + $result = $this->VariableModel->getVariables(getAuthUID(), ['number_displayed_past_studiensemester']); + $data = $this->getDataOrTerminateWithError($result); + + $number_displayed_past_studiensemester_default = $this->config->item('number_displayed_past_studiensemester_default'); + + $config['number_displayed_past_studiensemester'] = [ + "type" => "number", + "label" => $this->p->t('stv', 'settings_no_displayed_past_sem'), + "value" => $data['number_displayed_past_studiensemester'] + ?? $number_displayed_past_studiensemester_default + ]; + + #font_size + $result = $this->VariableModel->getVariables(getAuthUID(), ['stv_font_size']); + $data = $this->getDataOrTerminateWithError($result); + $config['font_size'] = [ + "type" => "select", + "label" => $this->p->t('stv', 'settings_fontsize'), + "value" => $data['stv_font_size'] ?? "fs_normal", + "options" => [ + "fs_xx-small" => $this->p->t('stv', 'settings_fontsize_xx-small'), + "fs_x-small" => $this->p->t('stv', 'settings_fontsize_x-small'), + "fs_small" => $this->p->t('stv', 'settings_fontsize_small'), + "fs_normal" => $this->p->t('stv', 'settings_fontsize_normal'), + "fs_big" => $this->p->t('stv', 'settings_fontsize_big'), + "fs_huge" => $this->p->t('stv', 'settings_fontsize_huge') + ] + ]; + + #others + Events::trigger('stv_config_get', function & () use (&$config) { + return $config; + }); + + $this->terminateWithSuccess($config); + } + + /** + * set App config + */ + public function set() + { + $this->load->model('system/Variable_model', 'VariableModel'); + $this->load->library('form_validation'); + + $this->form_validation->set_rules( + 'number_displayed_past_studiensemester', + $this->p->t('stv', 'settings_no_displayed_past_sem'), + 'required|integer' + ); + $this->form_validation->set_rules( + 'font_size', + $this->p->t('stv', 'settings_fontsize'), + 'required|in_list[fs_xx-small,fs_x-small,fs_small,fs_normal,fs_big,fs_huge]' + ); + + Events::trigger('stv_config_validation', $this->form_validation); + + if (!$this->form_validation->run()) + $this->terminateWithValidationErrors($this->form_validation->error_array()); + + + $this->VariableModel->setVariable( + getAuthUID(), + 'number_displayed_past_studiensemester', + $this->input->post('number_displayed_past_studiensemester') + ); + $this->VariableModel->setVariable( + getAuthUID(), + 'stv_font_size', + $this->input->post('font_size') + ); + + Events::trigger('stv_config_set', $this->input); + + $this->terminateWithSuccess(); + } + + /* * Get the config for the student filters * * @return void diff --git a/application/controllers/api/frontend/v1/stv/Verband.php b/application/controllers/api/frontend/v1/stv/Verband.php index 9fcd97c91..db832b30b 100644 --- a/application/controllers/api/frontend/v1/stv/Verband.php +++ b/application/controllers/api/frontend/v1/stv/Verband.php @@ -165,7 +165,17 @@ class Verband extends FHCAPI_Controller $this->StudiengangModel->addDistinct(); $this->StudiengangModel->addSelect("CONCAT(" . $this->StudiengangModel->escape($link) . ", semester) AS link", false); - $this->StudiengangModel->addSelect("CONCAT(UPPER(CONCAT(typ, kurzbz)), '-', semester, (SELECT CASE WHEN bezeichnung IS NULL OR bezeichnung='' THEN ''::TEXT ELSE CONCAT(' (', bezeichnung, ')') END FROM public.tbl_lehrverband WHERE studiengang_kz=v.studiengang_kz AND semester=v.semester ORDER BY verband, gruppe LIMIT 1)) AS name", false); + $this->StudiengangModel->addSelect("CONCAT( + UPPER(CONCAT(typ, kurzbz)), + '-', + semester, + ( + SELECT CASE WHEN bezeichnung IS NULL OR bezeichnung='' THEN ''::TEXT ELSE CONCAT(' (', bezeichnung, ')') END + FROM public.tbl_lehrverband + WHERE studiengang_kz=v.studiengang_kz AND semester=v.semester + ORDER BY verband, gruppe LIMIT 1 + ) + ) AS name", false); $this->StudiengangModel->addSelect('semester'); $this->StudiengangModel->addSelect($this->StudiengangModel->escape($studiengang_kz) . '::integer AS stg_kz', false); @@ -188,6 +198,7 @@ class Verband extends FHCAPI_Controller array_unshift($list, [ 'name' => 'PreStudent', 'link' => $link . 'prestudent', + 'no_sem_reload' => true, 'stg_kz' => (int)$studiengang_kz, 'children' => $this->getStdSem($link . 'prestudent/', $studiengang_kz) ]); @@ -216,7 +227,6 @@ class Verband extends FHCAPI_Controller $list = array_merge($list, $result); } } - } $this->terminateWithSuccess($list); } diff --git a/application/views/Studentenverwaltung.php b/application/views/Studentenverwaltung.php index 01e611657..16b8c0045 100644 --- a/application/views/Studentenverwaltung.php +++ b/application/views/Studentenverwaltung.php @@ -53,6 +53,8 @@ $configArray = [ active-addons="" stv-root="" cis-root="" + avatar-url="" + logout-url="" :permissions="" :config="" > diff --git a/public/css/Studentenverwaltung.css b/public/css/Studentenverwaltung.css index 56e99b937..eb6becc15 100644 --- a/public/css/Studentenverwaltung.css +++ b/public/css/Studentenverwaltung.css @@ -11,6 +11,24 @@ html { font-size: .875em; } +html.fs_xx-small { + font-size: .5em; +} +html.fs_x-small { + font-size: .625em; +} +html.fs_small { + font-size: .75em; +} +html.fs_normal { + font-size: .875em; +} +html.fs_big { + font-size: 1em; +} +html.fs_huge { + font-size: 1.125em; +} #appMenu { width: 300px; @@ -43,6 +61,12 @@ html { flex: 1 1 auto; } +#nav-user-btn img { + object-fit: contain; + height: 2.5rem; + width: 2.5rem; +} + .tabulator-row.disabled.tabulator-row-odd .tabulator-cell { color: var(--gray-400); } @@ -160,4 +184,4 @@ html { .tiny-90 div.tox.tox-tinymce { height: 90% !important; -} \ No newline at end of file +} diff --git a/public/js/api/factory/stv/config.js b/public/js/api/factory/stv/config.js new file mode 100644 index 000000000..c54b2f8b2 --- /dev/null +++ b/public/js/api/factory/stv/config.js @@ -0,0 +1,32 @@ +/** + * 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 { + get() { + return { + method: 'get', + url: 'api/frontend/v1/stv/config/get' + }; + }, + set(params) { + return { + method: 'post', + url: 'api/frontend/v1/stv/config/set', + params + }; + } +}; \ No newline at end of file diff --git a/public/js/components/AppConfig.js b/public/js/components/AppConfig.js new file mode 100644 index 000000000..b6b6aaeac --- /dev/null +++ b/public/js/components/AppConfig.js @@ -0,0 +1,135 @@ +/** + * 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 . + */ + +import BsModal from "./Bootstrap/Modal.js"; +import FhcForm from "./Form/Form.js"; +import FormInput from "./Form/Input.js"; + + +export default { + name: 'AppConfig', + components: { + BsModal, + FhcForm, + FormInput + }, + emits: [ + 'update:modelValue' + ], + props: { + modelValue: { + type: Object, + required: true + }, + endpoints: { + type: Object, + required: true + } + }, + data() { + return { + setup: {}, + tempValues: {} + }; + }, + watch: { + '$p.user_language.value'(n, o) { + if (n !== o && o !== undefined && Object.keys(this.setup).length) { + this.$api + .call(this.endpoints.get()) + .then(res => { + this.setup = {}; + Object.keys(res.data).forEach(key => { + const binding = { ...res.data[key] }; + delete binding.value; + delete binding.options; + const options = res.data[key].options; + this.setup[key] = { + binding, + options + }; + }); + }) + .catch(this.$fhcAlert.handleSystemErrors); + } + } + + }, + methods: { + update() { + this.$refs.form + .call(this.endpoints.set(this.tempValues)) + .then(() => { + this.$emit('update:modelValue', { ...this.tempValues }); + this.$refs.modal.hide(); + this.$fhcAlert.alertSuccess(this.$p.t('ui/settings_saved')); + }) + .catch(this.$fhcAlert.handleSystemErrors); + } + }, + created() { + this.$api + .call(this.endpoints.get()) + .then(res => { + Object.keys(res.data).forEach(key => { + const binding = { ...res.data[key] }; + delete binding.value; + delete binding.options; + const options = res.data[key].options; + this.tempValues[key] = res.data[key].value; + this.setup[key] = { + binding, + options + }; + }); + this.$emit('update:modelValue', { ...this.tempValues }); + }) + .catch(this.$fhcAlert.handleSystemErrors); + }, + template: /* html */` + + + + + + + ` +}; diff --git a/public/js/components/Stv/Studentenverwaltung.js b/public/js/components/Stv/Studentenverwaltung.js index fc0430454..6bf8d70ab 100644 --- a/public/js/components/Stv/Studentenverwaltung.js +++ b/public/js/components/Stv/Studentenverwaltung.js @@ -16,8 +16,10 @@ */ import CoreSearchbar from "../searchbar/searchbar.js"; +import NavLanguage from "../navigation/Language.js"; import VerticalSplit from "../verticalsplit/verticalsplit.js"; import AppMenu from "../AppMenu.js"; +import AppConfig from "../AppConfig.js"; import StvVerband from "./Studentenverwaltung/Verband.js"; import StvList from "./Studentenverwaltung/List.js"; import StvDetails from "./Studentenverwaltung/Details.js"; @@ -26,14 +28,17 @@ import StvStudiensemester from "./Studentenverwaltung/Studiensemester.js"; import ApiSearchbar from "../../api/factory/searchbar.js"; import ApiStv from "../../api/factory/stv.js"; import ApiStvVerband from '../../api/factory/stv/verband.js'; +import ApiStvConfig from '../../api/factory/stv/config.js'; export default { name: 'Studentenverwaltung', components: { CoreSearchbar, + NavLanguage, VerticalSplit, AppMenu, + AppConfig, StvVerband, StvList, StvDetails, @@ -45,6 +50,8 @@ export default { permissions: Object, stvRoot: String, cisRoot: String, + avatarUrl: String, + logoutUrl: String, activeAddons: String, // semicolon separated list of active addons url_studiensemester_kurzbz: String, url_mode: String, @@ -76,11 +83,14 @@ export default { }, configShowAufnahmegruppen: this.config.showAufnahmegruppen, configAllowUebernahmePunkte: this.config.allowUebernahmePunkte, - configUseReihungstestPunkte: this.config.useReihungstestPunkte + configUseReihungstestPunkte: this.config.useReihungstestPunkte, + appConfig: Vue.computed(() => this.appconfig) } }, data() { return { + appconfig: {}, + configEndpoints: ApiStvConfig, selected: [], searchbaroptions: { origin: 'stv', @@ -149,6 +159,22 @@ export default { }, url_prestudent_id() { this.handlePersonUrl(); + }, + 'appconfig.font_size'() { + // add to html class + const classList = Object.keys(this.$refs.config.setup.font_size.options); + classList.forEach(cn => document.documentElement.classList.remove(cn)); + document.documentElement.classList.add(this.appconfig.font_size); + // recalc Tabulator heights + if (this.$el) { + const tabulatorEls = this.$el.querySelectorAll('.tabulator'); + for (const el of tabulatorEls) { + const tabulators = Tabulator.findTable(el); + if (tabulators) { + tabulators[0].searchRows().forEach(row => row.normalizeHeight()); + } + } + } } }, methods: { @@ -431,6 +457,51 @@ export default { show-btn-submit @submit.prevent="onSearch" > +
@@ -462,5 +533,6 @@ export default {
+ ` }; diff --git a/public/js/components/Stv/Studentenverwaltung/Details.js b/public/js/components/Stv/Studentenverwaltung/Details.js index 7bd028f3c..dca08c07f 100644 --- a/public/js/components/Stv/Studentenverwaltung/Details.js +++ b/public/js/components/Stv/Studentenverwaltung/Details.js @@ -41,25 +41,34 @@ export default { return Object.fromEntries(Object.entries(this.configStudents).filter(([ , value ]) => !value.showOnlyWithUid && !value.showOnlyWithUid)); } }, + watch: { + '$p.user_language.value'(n, o) { + if (n !== o && o !== undefined) + this.loadConfig(); + } + }, methods: { + loadConfig() { + this.$api + .call(ApiStvApp.configStudent()) + .then(result => { + this.configStudent = result.data; + }) + .catch(this.$fhcAlert.handleSystemError); + this.$api + .call(ApiStvApp.configStudents()) + .then(result => { + this.configStudents = result.data; + }) + .catch(this.$fhcAlert.handleSystemError); + }, reload() { if (this.$refs.tabs?.$refs?.current?.reload) this.$refs.tabs.$refs.current.reload(); } }, created() { - this.$api - .call(ApiStvApp.configStudent()) - .then(result => { - this.configStudent = result.data; - }) - .catch(this.$fhcAlert.handleSystemError); - this.$api - .call(ApiStvApp.configStudents()) - .then(result => { - this.configStudents = result.data; - }) - .catch(this.$fhcAlert.handleSystemError); + this.loadConfig(); }, template: `
diff --git a/public/js/components/Stv/Studentenverwaltung/List.js b/public/js/components/Stv/Studentenverwaltung/List.js index 10c012fb0..689515bbe 100644 --- a/public/js/components/Stv/Studentenverwaltung/List.js +++ b/public/js/components/Stv/Studentenverwaltung/List.js @@ -2,6 +2,8 @@ import {CoreFilterCmpt} from "../../filter/Filter.js"; import ListNew from './List/New.js'; import ListFilter from './List/Filter.js'; +import { capitalize } from '../../../helpers/StringHelpers.js'; + import draggable from '../../../directives/draggable.js'; export default { @@ -233,7 +235,84 @@ export default { return "StudentList_" + today + ".csv"; } }, + watch: { + '$p.user_language.value'(n, o) { + if (n !== o && o !== undefined && this.$refs.table.tableBuilt) { + this.translateTabulator(); + } + } + }, methods: { + translateTabulator() { + this.$p + .loadCategory(['global', 'person', 'lehre', 'ui', 'profilUpdate', 'admission', 'stv']) + .then(() => { + const translations = { + uid: capitalize(this.$p.t('person/uid')), + titelpre: capitalize(this.$p.t('person/titelpre')), + nachname: capitalize(this.$p.t('person/nachname')), + vorname: capitalize(this.$p.t('person/vorname')), + wahlname: capitalize(this.$p.t('person/wahlname')), + vornamen: capitalize(this.$p.t('person/vornamen')), + titelpost: capitalize(this.$p.t('person/titelpost')), + ersatzkennzeichen: capitalize(this.$p.t('person/ersatzkennzeichen')), + gebdatum: capitalize(this.$p.t('person/geburtsdatum')), + geschlecht: capitalize(this.$p.t('person/geschlecht')), + semester: capitalize(this.$p.t('lehre/sem')), + verband: capitalize(this.$p.t('lehre/verb')), + gruppe: capitalize(this.$p.t('lehre/grp')), + studiengang: capitalize(this.$p.t('lehre/studiengang')), + studiengang_kz: capitalize(this.$p.t('lehre/studiengang_kz')), + matrikelnr: capitalize(this.$p.t('person/personenkennzeichen')), + person_id: capitalize(this.$p.t('person/person_id')), + status: capitalize(this.$p.t('global/status')), + status_datum: capitalize(this.$p.t('profilUpdate/statusDate')), + status_bestaetigung: capitalize(this.$p.t('global/status_bestaetigung')), + mail_privat: capitalize(this.$p.t('person/email_private')), + mail_intern: capitalize(this.$p.t('person/email_intern')), + anmerkungen: capitalize(this.$p.t('stv/notes_person')), + anmerkung: capitalize(this.$p.t('stv/notes_prestudent')), + orgform_kurzbz: capitalize(this.$p.t('lehre/orgform')), + aufmerksamdurch_kurzbz: capitalize(this.$p.t('person/aufmerksamDurch')), + punkte: capitalize(this.$p.t('admission/gesamtpunkte')), + aufnahmegruppe_kurzbz: capitalize(this.$p.t('stv/aufnahmegruppe_kurzbz')), + dual: capitalize(this.$p.t('lehre/dual_short')), + matr_nr: capitalize(this.$p.t('person/matrikelnummer')), + studienplan_bezeichnung: capitalize(this.$p.t('lehre/studienplan')), + prestudent_id: capitalize(this.$p.t('ui/prestudent_id')), + priorisierung_relativ: capitalize(this.$p.t('lehre/prioritaet')), + mentor: capitalize(this.$p.t('stv/mentor')), + bnaktiv: capitalize(this.$p.t('person/aktiv')) + }; + + /** NOTE(chris): + * use this approach because updateDefinition + * on the Tabulator columns is way slower and + * freezes up the GUI. + */ + // Overwrite definition for column show/hide + this.$refs.table.tabulator.getColumns().forEach(col => { + const trans = translations[col.getField()]; + if (!trans) + return; + col.getDefinition().title = trans; + }); + // Overwrite node in dom + this.$refs.table.tabulator.element + .querySelectorAll('.tabulator-col[tabulator-field]') + .forEach(el => { + const field = el.getAttribute('tabulator-field'); + if (!translations[field]) + return; + + const title = el.querySelector('.tabulator-col-title'); + if (!title) + return; + + title.innerText = translations[field]; + }); + }); + }, reload() { this.$refs.table.reloadTable(); }, @@ -408,6 +487,7 @@ export default { */` :new-btn-label="$p.t('stv/action_new')" @click:new="actionNewPrestudent" + @table-built="translateTabulator" >