diff --git a/application/config/stv.php b/application/config/stv.php
index e03c00084..84b148362 100644
--- a/application/config/stv.php
+++ b/application/config/stv.php
@@ -61,7 +61,11 @@ $config['tabs'] =
'notes' => [
//if true, the count of Messages will be shown in the header of the Tab Messages
'showCountNotes' => true
- ]
+ ],
+ 'combinePeople' => [
+ //multitab should only be shown with this length of selection
+ 'validCountMulti' => 2,
+ ],
];
// List of fields to show when ZGV_DOKTOR_ANZEIGEN is defined
@@ -117,5 +121,6 @@ $config['students_tab_order'] = [
'status',
'groups',
'finalexam',
+ 'combinePeople',
'archive',
];
diff --git a/application/controllers/api/frontend/v1/stv/Config.php b/application/controllers/api/frontend/v1/stv/Config.php
index 97d626246..3bf48bf5b 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
@@ -407,6 +498,15 @@ class Config extends FHCAPI_Controller
]
];
+ if($this->permissionlib->isBerechtigt('basis/person'))
+ {
+ $result['combinePeople'] = [
+ 'title' => $this->p->t('stv', 'tab_combine_people'),
+ 'component' => './Stv/Studentenverwaltung/Details/CombinePeople.js',
+ 'config' => $config['combinePeople']
+ ];
+ }
+
$result['kontaktieren'] = [
'title' => $this->p->t('stv', 'tab_kontaktieren'),
'component' => absoluteJsImportUrl('public/js/components/Stv/Studentenverwaltung/Details/Kontaktieren.js'),
diff --git a/application/controllers/api/frontend/v1/stv/Students.php b/application/controllers/api/frontend/v1/stv/Students.php
index 12440f036..9dbea65f2 100644
--- a/application/controllers/api/frontend/v1/stv/Students.php
+++ b/application/controllers/api/frontend/v1/stv/Students.php
@@ -765,6 +765,86 @@ class Students extends FHCAPI_Controller
$this->terminateWithSuccess($data);
}
+ /**
+ * @param string $studiensemester_kurzbz
+ *
+ * @return void
+ */
+ public function search($studiensemester_kurzbz)
+ {
+ $this->addMeta('ci_method', __FUNCTION__);
+ $this->addMeta('ci_params', array(
+ 'studiensemester_kurzbz' => $studiensemester_kurzbz
+ ));
+
+ $this->load->library('SearchLib', [ 'config' => 'searchstv' ]);
+ $this->load->library('form_validation');
+
+ $this->form_validation->set_rules('searchstr', 'searchstr', 'required');
+ $this->form_validation->set_rules('types[]', 'types', 'required');
+
+ if (!$this->form_validation->run())
+ $this->terminateWithValidationErrors($this->form_validation->error_array());
+
+ $result = $this->searchlib->search($this->input->post('searchstr'), $this->input->post('types'));
+
+ $data = $this->getDataOrTerminateWithError($result);
+
+
+ $this->load->model('crm/Prestudent_model', 'PrestudentModel');
+
+ $this->prepareQuery($studiensemester_kurzbz);
+
+ $this->PrestudentModel->addSelect("COALESCE(v.semester::text, CASE WHEN public.get_rolle_prestudent(tbl_prestudent.prestudent_id, NULL) IN ('Aufgenommener', 'Bewerber', 'Wartender', 'interessent') THEN public.get_absem_prestudent(tbl_prestudent.prestudent_id, NULL)::text ELSE ''::text END) AS semester", false);
+ $this->PrestudentModel->addSelect('v.verband');
+ $this->PrestudentModel->addSelect('v.gruppe');
+
+ //add status per semester
+ $this->PrestudentModel->addSelect(
+ "(
+ SELECT status_kurzbz
+ FROM public.tbl_prestudentstatus pss
+ WHERE pss.prestudent_id = public.tbl_prestudent.prestudent_id
+ AND pss.studiensemester_kurzbz = " . $this->PrestudentModel->escape($studiensemester_kurzbz) . "
+ ORDER BY GREATEST(pss.datum, '0001-01-01') DESC
+ LIMIT 1
+ ) AS statusofsemester"
+ );
+
+ $this->addSelectPrioRel();
+
+ $this->addFilter($studiensemester_kurzbz);
+
+ $prestudent_ids = [];
+ $student_uids = [];
+ $this->addMeta('data', $data);
+ foreach ($data as $row) {
+ $dataset = json_decode($row->data);
+ if ($row->type == 'prestudent') {
+ $prestudent_ids[] = $dataset->prestudent_id;
+ } elseif ($row->type == 'student') {
+ $student_uids[] = $dataset->uid;
+ }
+ }
+
+ if ($prestudent_ids && $student_uids) {
+ $this->PrestudentModel->db->where_in('tbl_prestudent.prestudent_id', $prestudent_ids);
+ $this->PrestudentModel->db->or_where_in('s.student_uid', $student_uids);
+ } elseif ($prestudent_ids) {
+ $this->PrestudentModel->db->where_in('tbl_prestudent.prestudent_id', $prestudent_ids);
+ } elseif ($student_uids) {
+ $this->PrestudentModel->db->where_in('s.student_uid', $student_uids);
+ } else {
+ $this->terminateWithSuccess([]);
+ }
+
+ $result = $this->PrestudentModel->load();
+
+ $data = $this->getDataOrTerminateWithError($result);
+
+ $this->terminateWithSuccess($data);
+ }
+
/**
* @param string|null $studiensemester_kurzbz
* @param string $type
diff --git a/application/controllers/api/frontend/v1/stv/Verband.php b/application/controllers/api/frontend/v1/stv/Verband.php
index 9fcd97c91..eb25a548b 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);
@@ -173,6 +183,7 @@ class Verband extends FHCAPI_Controller
$this->StudiengangModel->addOrder('semester');
if ($org_form !== null) {
+ $this->StudiengangModel->addSelect("v.orgform_kurzbz");
$this->StudiengangModel->db->group_start();
$this->StudiengangModel->db->where('v.semester', 0);
$this->StudiengangModel->db->or_where('v.orgform_kurzbz', $org_form);
@@ -188,6 +199,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 +228,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="= defined('ACTIVE_ADDONS') ? ACTIVE_ADDONS : ''; ?>"
stv-root="= site_url('Studentenverwaltung'); ?>"
cis-root="= CIS_ROOT; ?>"
+ avatar-url="= site_url('Cis/Pub/bild/person/' . getAuthPersonId()); ?>"
+ logout-url="= site_url('Cis/Auth/logout'); ?>"
:permissions="= htmlspecialchars(json_encode($permissions)); ?>"
:config="= htmlspecialchars(json_encode($configArray)); ?>"
>
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/css/components/AppMenu.css b/public/css/components/AppMenu.css
index b980c1efc..e142858f8 100644
--- a/public/css/components/AppMenu.css
+++ b/public/css/components/AppMenu.css
@@ -16,11 +16,15 @@
padding: .5rem 1rem;
text-decoration: none;
}
+.fhc-app-menu li a.disabled {
+ --bs-link-opacity: .5;
+}
.fhc-app-menu li a.active,
.fhc-app-menu li a:hover {
--bs-link-color-rgb: var(--bs-link-hover-color-rgb);
background: var(--surface-hover);
}
+.fhc-app-menu li a.disabled,
.fhc-app-menu li a.active {
pointer-events: none;
}
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/api/factory/stv/students.js b/public/js/api/factory/stv/students.js
index 07d4453d8..cae2f31b2 100644
--- a/public/js/api/factory/stv/students.js
+++ b/public/js/api/factory/stv/students.js
@@ -46,6 +46,14 @@ export default {
url: url
};
},
+ search(params, studiensemester_kurzbz) {
+ return {
+ method: 'post',
+ url: 'api/frontend/v1/stv/students/search/'
+ + encodeURIComponent(studiensemester_kurzbz),
+ params
+ };
+ },
verband(relative_path) {
return {
method: 'get',
diff --git a/public/js/apps/Studentenverwaltung.js b/public/js/apps/Studentenverwaltung.js
index 5eda16dd6..e6f77d5f5 100644
--- a/public/js/apps/Studentenverwaltung.js
+++ b/public/js/apps/Studentenverwaltung.js
@@ -148,6 +148,44 @@ const router = VueRouter.createRouter({
next();
}
},
+ {
+ name: 'search',
+ path: `/${ciPath}/studentenverwaltung/:studiensemester_kurzbz/search/:searchstr`,
+ component: FhcStudentenverwaltung,
+ props(route) {
+ return {
+ url_studiensemester_kurzbz: route.params.studiensemester_kurzbz,
+ url_mode: 'search',
+ url_prestudent_id: route.params.searchstr
+ };
+ },
+ beforeEnter(to, from, next) {
+ const isSemester = /^[WS]S\d{4}$/.test(to.params.studiensemester_kurzbz);
+ if (!isSemester) {
+ return next({name: 'index'});
+ }
+ next();
+ }
+ },
+ {
+ name: 'search_w_types',
+ path: `/${ciPath}/studentenverwaltung/:studiensemester_kurzbz/search/:types/:searchstr`,
+ component: FhcStudentenverwaltung,
+ props(route) {
+ return {
+ url_studiensemester_kurzbz: route.params.studiensemester_kurzbz,
+ url_mode: 'search',
+ url_prestudent_id: route.params.type + '/' + route.params.searchstr
+ };
+ },
+ beforeEnter(to, from, next) {
+ const isSemester = /^[WS]S\d{4}$/.test(to.params.studiensemester_kurzbz);
+ if (!isSemester) {
+ return next({name: 'index'});
+ }
+ next();
+ }
+ },
{
path: '/:pathMatch(.*)*',
redirect: {
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 */`
+
+
+ {{ $p.t('ui/settings') }}
+
+
+
+
+
+
+
+
+
+
+
+ `
+};
diff --git a/public/js/components/AppMenu.js b/public/js/components/AppMenu.js
index 33d35f8df..9e85debcd 100644
--- a/public/js/components/AppMenu.js
+++ b/public/js/components/AppMenu.js
@@ -64,5 +64,6 @@ export default {
{{ menu.description }}
+
`
};
diff --git a/public/js/components/Calendar/Base/Grid.js b/public/js/components/Calendar/Base/Grid.js
index c232dd955..3418a9151 100644
--- a/public/js/components/Calendar/Base/Grid.js
+++ b/public/js/components/Calendar/Base/Grid.js
@@ -316,7 +316,7 @@ export default {
template: /* html */`
this.appconfig)
}
},
data() {
return {
+ appconfig: {},
+ configEndpoints: ApiStvConfig,
selected: [],
searchbaroptions: {
origin: 'stv',
calcheightonly: true,
+ nolivesearch: true,
types: {
student: Vue.computed(() => this.$p.t('search/type_student')),
prestudent: Vue.computed(() => this.$p.t('search/type_prestudent'))
@@ -123,6 +134,8 @@ export default {
studiengangKz: undefined,
studiengangKuerzel: '',
studiensemesterKurzbz: this.defaultSemester,
+ selected_semester: undefined,
+ selected_orgform: undefined,
lists: {
nations: [],
sprachen: [],
@@ -131,6 +144,44 @@ export default {
verbandEndpoint: ApiStvVerband
}
},
+ computed: {
+ appMenuExtraItems() {
+ const extraItems = [];
+
+ if (this.studiengangKz !== undefined && this.selected_semester !== undefined) {
+ const studiengang_kz = String(this.studiengangKz);
+ const semester = String(this.selected_semester);
+ const orgform = this.selected_orgform || '';
+
+ extraItems.push({
+ link: FHC_JS_DATA_STORAGE_OBJECT.app_root
+ + 'content/statistik/notenspiegel.php?type=xls'
+ + '&studiengang_kz=' + studiengang_kz
+ + '&semester=' + semester
+ + '&orgform=' + orgform,
+ description: 'stv/grade_report_xls'
+ });
+ extraItems.push({
+ link: FHC_JS_DATA_STORAGE_OBJECT.app_root
+ + 'content/statistik/notenspiegel_erweitert.php?typ=xls'
+ + '&studiengang_kz=' + studiengang_kz
+ + '&semester=' + semester
+ + '&orgform=' + orgform,
+ description: 'stv/grade_report_xls_extended'
+ });
+ extraItems.push({
+ link: FHC_JS_DATA_STORAGE_OBJECT.app_root
+ + 'content/statistik/notenspiegel.php?type=html'
+ + '&studiengang_kz=' + studiengang_kz
+ + '&semester=' + semester
+ + '&orgform=' + orgform,
+ description: 'stv/grade_report_html'
+ });
+ }
+
+ return extraItems;
+ }
+ },
watch: {
'url_studiensemester_kurzbz': function (newVal, oldVal) {
if (newVal !== oldVal) {
@@ -146,6 +197,25 @@ export default {
},
'url_mode': function () {
this.handlePersonUrl();
+ },
+ 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: {
@@ -159,7 +229,7 @@ export default {
}
},
buildPrestudentSearchResultLink(data) {
- return this.$fhcApi.getUri(
+ return this.$api.getUri(
'/studentenverwaltung'
+ '/' + this.studiensemesterKurzbz
+ '/prestudent/'
@@ -167,7 +237,7 @@ export default {
);
},
buildStudentSearchResultLink(data) {
- return this.$fhcApi.getUri(
+ return this.$api.getUri(
'/studentenverwaltung'
+ '/' + this.studiensemesterKurzbz
+ '/student/'
@@ -175,14 +245,14 @@ export default {
);
},
buildPersonSearchResultLink(data) {
- return this.$fhcApi.getUri(
+ return this.$api.getUri(
'/studentenverwaltung'
+ '/' + this.studiensemesterKurzbz
+ '/person/'
+ data.person_id
);
},
- onSelectVerband( {link, studiengang_kz}) {
+ onSelectVerband({ link, studiengang_kz, semester, orgform_kurzbz }) {
let urlpath = String(link);
if (!urlpath.match(/\/prestudent/))
{
@@ -191,6 +261,8 @@ export default {
this.$refs.stvList.updateUrl(ApiStv.students.verband(urlpath));
this.studiengangKz = studiengang_kz;
+ this.selected_semester = semester;
+ this.selected_orgform = orgform_kurzbz;
const stg = this.lists.stgs.find((element) => {
return (element.studiengang_kz === this.studiengangKz);
});
@@ -249,6 +321,34 @@ export default {
ApiStv.students.person(this.$route.params.person_id, 'CURRENT_SEMESTER'),
true
);
+ } else if (this.$route.params.searchstr) {
+ const searchsettings = {
+ searchstr: this.$route.params.searchstr,
+ types: this.$route.params.types?.split('+') || []
+ };
+
+ // init into student list
+ this.$refs.stvList.updateUrl(
+ ApiStv.students.search(searchsettings, this.studiensemesterKurzbz)
+ );
+
+ // init into searchbar
+ this.$refs.searchbar.searchsettings.searchstr = searchsettings.searchstr;
+ this.$refs.searchbar.searchsettings.types = searchsettings.types;
+ this.$nextTick(this.blurSearchbar);
+ }
+ else
+ {
+ this.clearTabulator();
+ }
+ },
+ clearTabulator() {
+ if(['index', 'studiensemester'].includes(this.$route.name))
+ {
+ if(this.$refs?.stvList?.$refs?.table?.tabulator)
+ {
+ this.$refs.stvList.$refs.table.tabulator.setData([]);
+ }
}
},
checkUrlStudiengang() {
@@ -269,6 +369,42 @@ export default {
});
}
}
+ else
+ {
+ this.studiengangKz = undefined;
+ this.studiengangKuerzel = '';
+ this.clearTabulator();
+ }
+ },
+ onSearch(e) {
+ const searchsettings = { ...this.$refs.searchbar.searchsettings };
+ if (searchsettings.searchstr.length >= 2) {
+ this.blurSearchbar();
+
+ if (!searchsettings.types.length || searchsettings.types.length == this.$refs.searchbar.types.length) {
+ this.$router.push({
+ name: 'search',
+ params: {
+ studiensemester_kurzbz: this.studiensemesterKurzbz,
+ searchstr: searchsettings.searchstr
+ }
+ });
+ } else {
+ this.$router.push({
+ name: 'search_w_types',
+ params: {
+ studiensemester_kurzbz: this.studiensemesterKurzbz,
+ searchstr: searchsettings.searchstr,
+ types: searchsettings.types.join('+')
+ }
+ });
+ }
+ }
+ },
+ blurSearchbar() {
+ this.$refs.searchbar.$refs.input.blur();
+ this.$refs.searchbar.abort();
+ this.$refs.searchbar.hideresult();
}
},
created() {
@@ -381,10 +517,58 @@ export default {
+
+
+
+
@@ -394,14 +578,38 @@ export default {
@@ -416,5 +624,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/Details/CombinePeople.js b/public/js/components/Stv/Studentenverwaltung/Details/CombinePeople.js
new file mode 100644
index 000000000..81c1a6860
--- /dev/null
+++ b/public/js/components/Stv/Studentenverwaltung/Details/CombinePeople.js
@@ -0,0 +1,83 @@
+export default {
+ name: "TabCombinePeople",
+ inject: {
+ cisRoot: {
+ from: 'cisRoot'
+ },
+ },
+ props: {
+ modelValue: Object,
+ },
+ data(){
+ return {
+ iframeUrl: null,
+ viewLoaded: false
+ }
+ },
+ computed: {
+ personIds() {
+ return Array.isArray(this.modelValue)
+ ? this.modelValue.map(e => e.person_id)
+ : [this.modelValue.person_id];
+ },
+ detailStringPerson1(){
+ let person1 = this.modelValue[0];
+ return person1.vorname + " " + person1.nachname + "(" + person1.person_id + ")";
+ },
+ detailStringPerson2(){
+ let person2 = this.modelValue[1];
+ return person2.vorname + " " + person2.nachname + "(" + person2.person_id+ ")";
+ },
+
+ },
+ methods: {
+ combinePeople(){
+ this.viewLoaded = true;
+ let person1_id = this.personIds[0];
+ let person2_id = this.personIds[1];
+
+ if(person1_id == person2_id) {
+ return this.$fhcAlert.alertError(this.$p.t('stv', 'error_combinePeople_samePerson'));
+ }
+
+ let linkCombinePeople = this.cisRoot + 'vilesci/stammdaten/personen_wartung.php?person_id_1=' + person1_id + '&person_id_2='+ person2_id;
+ this.openLink(linkCombinePeople);
+ },
+ openLink(url) {
+ this.iframeUrl = url;
+ },
+ goBack(){
+ this.viewLoaded = false;
+ this.iframeUrl = null;
+ }
+ },
+ template: /*html*/ `
+
+
+
+
Personen zusammenlegen
+
+
+
{{$p.t('stv', 'question_combine_people', { person1: detailStringPerson1, person2: detailStringPerson2 })}}
+
+
+
+ ungültige Anzahl: {{this.modelValue.length}}
+
+
+
+
+
+
+
+
+
+
+
+ `
+ };
\ No newline at end of file
diff --git a/public/js/components/Stv/Studentenverwaltung/List.js b/public/js/components/Stv/Studentenverwaltung/List.js
index 23fea3f67..910a34d85 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 {
@@ -133,7 +135,12 @@ export default {
{
return Promise.resolve({ data: []});
}
- return this.$api.call({method: 'post', url, params});
+ /**
+ * NOTE(chris): Because of a bug in Tabulator
+ * we need to get the params from elsewhere.
+ * @see https://github.com/olifolkerd/tabulator/issues/4318
+ */
+ return this.$api.call({...config, url, params: this.tabulatorOptions.ajaxParams});
},
ajaxResponse: (url, params, response) => {
return response?.data;
@@ -228,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();
},
@@ -290,20 +374,22 @@ export default {
encodeURIComponent(this.currentSemester)
);
- const params = {};
+ const params = (endpoint?.params !== undefined) ? endpoint.params : {};
+ const method = (endpoint?.method !== undefined) ? endpoint.method : 'get';
if (this.filter.length)
params.filter = this.filter;
+ this.tabulatorOptions.ajaxURL = endpoint.url;
+ this.tabulatorOptions.ajaxParams = { ...params };
+ this.tabulatorOptions.ajaxConfig = method;
if (!this.$refs.table.tableBuilt) {
- if (!this.$refs.table.tabulator) {
- this.tabulatorOptions.ajaxURL = endpoint.url;
- this.tabulatorOptions.ajaxParams = params;
- } else
+ if (this.$refs.table.tabulator) {
this.$refs.table.tabulator.on("tableBuilt", () => {
- this.$refs.table.tabulator.setData(endpoint.url, params);
+ this.$refs.table.tabulator.setData(endpoint.url, params, method);
});
+ }
} else
- this.$refs.table.tabulator.setData(endpoint.url, params);
+ this.$refs.table.tabulator.setData(endpoint.url, params, method);
},
dragCleanup(evt) {
if (evt.dataTransfer.dropEffect == 'none')
@@ -400,6 +486,7 @@ export default {
new-btn-show
:new-btn-label="$p.t('stv/action_new')"
@click:new="actionNewPrestudent"
+ @table-built="translateTabulator"
>
diff --git a/public/js/components/Stv/Studentenverwaltung/Verband.js b/public/js/components/Stv/Studentenverwaltung/Verband.js
index 86e01a55b..1ff9d2ed9 100644
--- a/public/js/components/Stv/Studentenverwaltung/Verband.js
+++ b/public/js/components/Stv/Studentenverwaltung/Verband.js
@@ -16,11 +16,17 @@ export default {
inject: {
$reloadList: {
from: '$reloadList',
- required: true
+ default: () => {}
},
currentSemester: {
from: 'currentSemester',
required: true
+ },
+ appConfig: {
+ from: 'appConfig',
+ default: {
+ number_displayed_past_studiensemester: 5
+ }
}
},
emits: [
@@ -52,6 +58,9 @@ export default {
return this.nodes.filter(node => this.favorites.list.includes(node.key));
return this.nodes;
+ },
+ noSemReloadNodes() {
+ return this.nodes.reduce(this.mapNodesToNoSemReloadNodes, []);
}
},
watch: {
@@ -59,6 +68,14 @@ export default {
if (newVal !== oldVal) {
this.setPreselection();
}
+ },
+ 'appConfig.number_displayed_past_studiensemester'(newVal, oldVal) {
+ if (oldVal !== undefined) {
+ this.noSemReloadNodes.forEach(node => {
+ delete node.children;
+ this.onExpandTreeNode(node);
+ });
+ }
}
},
methods: {
@@ -114,7 +131,14 @@ export default {
},
onSelectTreeNode(node) {
if (node.data.link)
- this.$emit('selectVerband', {link: node.data.link, studiengang_kz: node.data.stg_kz});
+ this.$emit('selectVerband', {link: node.data.link, studiengang_kz: node.data.stg_kz, semester: node.data.semester, orgform_kurzbz: node.data.orgform_kurzbz});
+ },
+ mapNodesToNoSemReloadNodes(result, node) {
+ if (node.data.no_sem_reload)
+ result.push(node);
+ if (node.children)
+ result = node.children.reduce(this.mapNodesToNoSemReloadNodes, result);
+ return result;
},
mapResultToTreeData(el) {
const cp = {
@@ -187,22 +211,25 @@ export default {
if (!currentNode)
return;
- const currentSelectedKey = Object.keys(this.selectedKey).find(Boolean);
- if (currentSelectedKey) {
- if (currentSelectedKey == currentKey)
- return;
- /**
- * Do not select a new entry if the current is a child of the new one.
- * This happens if a child entry of a new stg is selected and the router
- * tries to select the stg root entry (because subtrees do not have
- * routes yet)
- */
- const isChild = this.findNodeByKey(
- currentSelectedKey,
- currentNode.children
- );
- if (isChild)
- return;
+ if(this.selectedKey)
+ {
+ const currentSelectedKey = Object.keys(this.selectedKey).find(Boolean);
+ if (currentSelectedKey) {
+ if (currentSelectedKey == currentKey)
+ return;
+ /**
+ * Do not select a new entry if the current is a child of the new one.
+ * This happens if a child entry of a new stg is selected and the router
+ * tries to select the stg root entry (because subtrees do not have
+ * routes yet)
+ */
+ const isChild = this.findNodeByKey(
+ currentSelectedKey,
+ currentNode.children || []
+ );
+ if (isChild)
+ return;
+ }
}
for (let i = 1; i < parts.length; i++)
diff --git a/public/js/components/Tabs.js b/public/js/components/Tabs.js
index 0b9b82027..56506bd9d 100644
--- a/public/js/components/Tabs.js
+++ b/public/js/components/Tabs.js
@@ -33,7 +33,8 @@ export default {
data() {
return {
current: null,
- tabs: {}
+ tabs: {},
+ count: null
}
},
computed: {
@@ -113,10 +114,12 @@ export default {
};
}
- if (Array.isArray(config))
+ if (Array.isArray(config)) {
config.forEach((item, key) => _addToTabs(key, item));
- else
+ }
+ else {
Object.entries(config).forEach(([key, item]) => _addToTabs(key, item));
+ }
if (this.current === null || !tabs[this.current]) {
if (tabs[this.default])
@@ -129,6 +132,57 @@ export default {
updateSuffix() {
this.getTabSuffix(this.currentTab);
},
+ removeInvalidCountTabs(){
+ if(this.modelValue.length)
+ {
+ let countIst = this.modelValue.length;
+ const tabsToDelete = [];
+
+ Object.entries(this.config).forEach(([key, item]) => {
+
+ const target = item?.config ? item : item?.value || item;
+
+ // check config for validCountMulti
+ if (target.config?.validCountMulti !== undefined) {
+ let tab;
+ let countSoll;
+ tab = key;
+ countSoll = target.config.validCountMulti;
+
+ //check if tab is existing
+ if (countSoll !== undefined && countSoll == countIst) {
+ //add tab if it was removed before
+ if (tab in this.tabs == false) {
+ const value = Vue.reactive({
+ suffix: '',
+ showSuffix: item.showSuffix || false
+ });
+
+ this.tabs[tab] = {
+ component: Vue.markRaw(Vue.defineAsyncComponent(() => import(item.component))),
+ title: Vue.computed(() => item.title || tab),
+ config: item.config,
+ tab,
+ value,
+ suffixhelper: item.suffixhelper ?? null
+ };
+ }
+ }
+
+ //add to toDeleteArray if count is not allowed
+ if (countSoll !== undefined && countSoll !== countIst) {
+ tabsToDelete.push(tab);
+ }
+ }
+ });
+
+ // Delete all tabs with count not allowed
+ tabsToDelete.forEach(k => {
+ delete this.tabs[k];
+ });
+
+ }
+ },
async getTabSuffix(tab) {
if (!tab.value.showSuffix) {
return;
@@ -151,9 +205,11 @@ export default {
},
mounted() {
this.getTabSuffixes();
+ this.removeInvalidCountTabs();
},
updated() {
this.getTabSuffixes();
+ this.removeInvalidCountTabs();
},
template: `
diff --git a/public/js/components/navigation/Language.js b/public/js/components/navigation/Language.js
new file mode 100644
index 000000000..4d26dadc0
--- /dev/null
+++ b/public/js/components/navigation/Language.js
@@ -0,0 +1,62 @@
+/**
+ * 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 {
+ emits: [
+ 'changed'
+ ],
+ props: {
+ activeClass: {
+ type: String,
+ default: 'active'
+ },
+ itemClass: {
+ type: [String, Array, Object],
+ default: ''
+ }
+ },
+ data() {
+ return {
+ languages: FHC_JS_DATA_STORAGE_OBJECT.server_languages
+ };
+ },
+ methods:{
+ onChange(lang) {
+ if (this.languages.some(l => l.sprache === lang)) {
+ this.$p
+ .setLanguage(lang)
+ .then(() => {
+ if (document.querySelector('[cis4Reload]'))
+ window.location.reload();
+ else
+ this.$emit('changed', lang);
+ });
+ }
+ }
+ },
+ template: /*html*/`
+
+
+
`
+};
\ No newline at end of file
diff --git a/public/js/components/searchbar/searchbar.js b/public/js/components/searchbar/searchbar.js
index 56fba15c6..d6c93198e 100644
--- a/public/js/components/searchbar/searchbar.js
+++ b/public/js/components/searchbar/searchbar.js
@@ -23,7 +23,17 @@ export default {
mergedStudent,
mergedPerson
},
- props: [ "searchoptions", "searchfunction" ],
+ props: {
+ searchoptions: {
+ type: Object,
+ required: true
+ },
+ searchfunction: {
+ type: Function,
+ required: true
+ },
+ showBtnSubmit: Boolean
+ },
provide() {
return {
query: Vue.computed(() => this.lastQuery)
@@ -102,11 +112,22 @@ export default {
>
+