From 11f93f0c918cd418bdcc9d92c074ba931a9504f9 Mon Sep 17 00:00:00 2001 From: Ivymaster Date: Mon, 27 Apr 2026 18:09:57 +0200 Subject: [PATCH] Added additional filters to 'classScheduleOverview' component --- .../api/frontend/v1/ClassScheduleApi.php | 84 ++++++- .../v1/organisation/OrganizationalUnitApi.php | 7 +- .../frontend/v1/organisation/Studienplan.php | 14 +- .../views/lehre/class_schedule/index.php | 3 +- public/js/api/factory/classSchedule.js | 3 +- public/js/api/factory/studienplan.js | 6 + public/js/apps/lehre/ClassScheduleApp.js | 4 + .../ClassScheduleCalendarSelector.js | 44 +++- .../ClassSchedule/ClassScheduleOverview.js | 212 ++++++++++++++++-- .../ClassScheduleValidityPeriodModal.js | 200 ++++++++++++----- .../ClassScheduleValidityPeriodOverview.js | 12 +- .../ClassScheduleValidityPeriodPreview.js | 2 +- system/phrasesupdate.php | 40 ++++ 13 files changed, 541 insertions(+), 90 deletions(-) diff --git a/application/controllers/api/frontend/v1/ClassScheduleApi.php b/application/controllers/api/frontend/v1/ClassScheduleApi.php index 6c4bac78b..2402cb5fc 100644 --- a/application/controllers/api/frontend/v1/ClassScheduleApi.php +++ b/application/controllers/api/frontend/v1/ClassScheduleApi.php @@ -61,6 +61,13 @@ class ClassScheduleApi extends FHCAPI_Controller public function getAllClassTimeValidityPeriods() { + $entitledOrganizationalUnitsShortCodes = $this->permissionlib->getOE_isEntitledFor('lehre/unterrichtszeiten_gk'); + + $organizationalUnitShortCode = $this->input->get('organizationalUnitShortCode'); + $validityPeriodFrom = $this->input->get('validityPeriodFrom'); + $validityPeriodTo = $this->input->get('validityPeriodTo'); + + $this->ClassTimeSlotValidityPeriodModel->db->where_in('lehre.tbl_unterrichtszeiten_gueltigkeit.oe_kurzbz', $entitledOrganizationalUnitsShortCodes); $this->ClassTimeSlotValidityPeriodModel->addSelect( 'lehre.tbl_unterrichtszeiten_gueltigkeit.*,' . 'public.tbl_organisationseinheit.bezeichnung as organisationseinheit_bezeichnung,' . @@ -72,6 +79,20 @@ class ClassScheduleApi extends FHCAPI_Controller $this->ClassTimeSlotValidityPeriodModel->addJoin('public.tbl_organisationseinheit', 'public.tbl_organisationseinheit.oe_kurzbz=lehre.tbl_unterrichtszeiten_gueltigkeit.oe_kurzbz', 'LEFT'); $this->ClassTimeSlotValidityPeriodModel->addJoin('lehre.tbl_unterrichtszeiten_typ', 'lehre.tbl_unterrichtszeiten_typ.unterrichtszeitentyp_kurzbz=lehre.tbl_unterrichtszeiten_gueltigkeit.unterrichtszeitentyp_kurzbz', 'LEFT'); $this->ClassTimeSlotValidityPeriodModel->addOrder('gueltig_von', 'DESC'); + + if ($organizationalUnitShortCode) { + $this->ClassTimeSlotValidityPeriodModel->db->where('lehre.tbl_unterrichtszeiten_gueltigkeit.oe_kurzbz', $organizationalUnitShortCode); + } + + if ($validityPeriodFrom) { + $this->ClassTimeSlotValidityPeriodModel->db + ->where('lehre.tbl_unterrichtszeiten_gueltigkeit.gueltig_von >=', date('Y-m-d', strtotime($validityPeriodFrom))); + } + if ($validityPeriodTo) { + $this->ClassTimeSlotValidityPeriodModel->db + ->where('lehre.tbl_unterrichtszeiten_gueltigkeit.gueltig_bis <=', date('Y-m-d', strtotime($validityPeriodTo))); + } + $class_time_slot_validity_period_res = $this->ClassTimeSlotValidityPeriodModel->load(); $class_time_slot_validity_period_res = $this->getDataOrTerminateWithError($class_time_slot_validity_period_res); $this->terminateWithSuccess($class_time_slot_validity_period_res); @@ -79,6 +100,10 @@ class ClassScheduleApi extends FHCAPI_Controller public function getAllClassTimeValidityPeriodsPerOrganizationalUnit($organizationUnitId) { + if (!$this->isUserEntitledForOrganizationalUnit($organizationUnitId, 'lehre/unterrichtszeiten_gk')) { + $this->terminateWithError($this->p->t('ui', 'keineBerechtigung')); + } + $this->ClassTimeSlotValidityPeriodModel->addSelect( 'lehre.tbl_unterrichtszeiten_gueltigkeit.*,' . 'public.tbl_organisationseinheit.bezeichnung as organisationseinheit_bezeichnung,' . @@ -97,11 +122,24 @@ class ClassScheduleApi extends FHCAPI_Controller public function getClassTimeValidityPeriod($classTimeSlotValidityPeriodId) { - $this->ClassTimeSlotValidityPeriodModel->addSelect('lehre.tbl_unterrichtszeiten_gueltigkeit.*, public.tbl_organisationseinheit.oe_kurzbz as oe_kurzbz, lehre.tbl_studienplan.studienplan_id, lehre.tbl_studienplan.bezeichnung as studienplan_bezeichnung'); + $this->ClassTimeSlotValidityPeriodModel->addSelect(' + lehre.tbl_unterrichtszeiten_gueltigkeit.*, + public.tbl_organisationseinheit.oe_kurzbz as oe_kurzbz, + public.tbl_organisationseinheit.bezeichnung as oe_bezeichnung, + public.tbl_organisationseinheit.organisationseinheittyp_kurzbz as oe_organisationseinheittyp_kurzbz, + lehre.tbl_studienplan.studienplan_id, + lehre.tbl_studienplan.bezeichnung as studienplan_bezeichnung, + '); $this->ClassTimeSlotValidityPeriodModel->addJoin('public.tbl_organisationseinheit', 'public.tbl_organisationseinheit.oe_kurzbz=lehre.tbl_unterrichtszeiten_gueltigkeit.oe_kurzbz', 'LEFT'); $this->ClassTimeSlotValidityPeriodModel->addJoin('lehre.tbl_studienplan', 'lehre.tbl_studienplan.studienplan_id=lehre.tbl_unterrichtszeiten_gueltigkeit.studienplan_id', 'LEFT'); $class_time_slot_validity_period_res = $this->ClassTimeSlotValidityPeriodModel->load($classTimeSlotValidityPeriodId); + $class_time_slot_validity_period_res = $this->getDataOrTerminateWithError($class_time_slot_validity_period_res); + + if (!$this->isUserEntitledForOrganizationalUnit($class_time_slot_validity_period_res[0]->oe_kurzbz, 'lehre/unterrichtszeiten_gk')) { + $this->terminateWithError($this->p->t('ui', 'keineBerechtigung')); + } + $this->terminateWithSuccess($class_time_slot_validity_period_res); } @@ -126,6 +164,10 @@ class ClassScheduleApi extends FHCAPI_Controller if($this->form_validation->run() == FALSE) $this->terminateWithValidationErrors($this->form_validation->error_array()); + if (!$this->isUserEntitledForOrganizationalUnit($this->input->post('organizationalUnitShortCode'), 'lehre/unterrichtszeiten_gk')) { + $this->terminateWithError($this->p->t('ui', 'keineBerechtigung')); + } + $this->db->trans_start(); $result = $this->ClassTimeSlotValidityPeriodModel->insert([ @@ -170,6 +212,10 @@ class ClassScheduleApi extends FHCAPI_Controller if($this->form_validation->run() == FALSE) $this->terminateWithValidationErrors($this->form_validation->error_array()); + if (!$this->isUserEntitledForOrganizationalUnit($this->input->post('organizationalUnitShortCode'), 'lehre/unterrichtszeiten_gk')) { + $this->terminateWithError($this->p->t('ui', 'keineBerechtigung')); + } + $this->db->trans_start(); $result = $this->ClassTimeSlotValidityPeriodModel->update($classTimeSlotValidityPeriodId, [ @@ -193,6 +239,12 @@ class ClassScheduleApi extends FHCAPI_Controller public function deleteClassTimeSlotValidityPeriod($classTimeSlotValidityPeriodId) { + $validityPeriodResult = $this->ClassTimeSlotValidityPeriodModel->loadWhere(['unterrichtszeitengueltigkeit_id' => $classTimeSlotValidityPeriodId]); + $validityPeriodResult = $this->getDataOrTerminateWithError($validityPeriodResult)[0]; + if (!$this->isUserEntitledForOrganizationalUnit($validityPeriodResult->oe_kurzbz, 'lehre/unterrichtszeiten_gk')) { + $this->terminateWithError($this->p->t('ui', 'keineBerechtigung')); + } + $this->db->trans_start(); $result = $this->ClassTimeSlotModel->delete(['unterrichtszeitengueltigkeit_id'=> $classTimeSlotValidityPeriodId]); @@ -212,6 +264,12 @@ class ClassScheduleApi extends FHCAPI_Controller public function getClassTimeSlotsForValidityPeriod($classTimeSlotValidityPeriodId) { + $validityPeriodResult = $this->ClassTimeSlotValidityPeriodModel->loadWhere(['unterrichtszeitengueltigkeit_id' => $classTimeSlotValidityPeriodId]); + $validityPeriodResult = $this->getDataOrTerminateWithError($validityPeriodResult)[0]; + if (!$this->isUserEntitledForOrganizationalUnit($validityPeriodResult->oe_kurzbz, 'lehre/unterrichtszeiten_gk')) { + $this->terminateWithError($this->p->t('ui', 'keineBerechtigung')); + } + $this->ClassTimeSlotModel->addOrder('insertamum', 'DESC'); $class_time_slots_res = $this->ClassTimeSlotModel->loadWhere(['unterrichtszeitengueltigkeit_id' => $classTimeSlotValidityPeriodId]); $class_time_slots_res = $this->getDataOrTerminateWithError($class_time_slots_res); @@ -223,6 +281,12 @@ class ClassScheduleApi extends FHCAPI_Controller $this->form_validation->set_rules('unterrichtszeiten', 'Class Time Slots', 'callback_validate_items_in_class_time_slots'); if($this->form_validation->run() == FALSE) $this->terminateWithValidationErrors($this->form_validation->error_array()); + $validityPeriodResult = $this->ClassTimeSlotValidityPeriodModel->loadWhere(['unterrichtszeitengueltigkeit_id' => $classTimeSlotValidityPeriodId]); + $validityPeriodResult = $this->getDataOrTerminateWithError($validityPeriodResult)[0]; + if (!$this->isUserEntitledForOrganizationalUnit($validityPeriodResult->oe_kurzbz, 'lehre/unterrichtszeiten_gk')) { + $this->terminateWithError($this->p->t('ui', 'keineBerechtigung')); + } + $this->db->trans_start(); $timeSlotGroupIdentifier = uniqid(); @@ -253,6 +317,12 @@ class ClassScheduleApi extends FHCAPI_Controller $this->form_validation->set_rules('unterrichtszeiten', 'Class Time Slots', 'callback_validate_items_in_class_time_slots'); if($this->form_validation->run() == FALSE) $this->terminateWithValidationErrors($this->form_validation->error_array()); + $validityPeriodResult = $this->ClassTimeSlotValidityPeriodModel->loadWhere(['unterrichtszeitengueltigkeit_id' => $classTimeSlotValidityPeriodId]); + $validityPeriodResult = $this->getDataOrTerminateWithError($validityPeriodResult)[0]; + if (!$this->isUserEntitledForOrganizationalUnit($validityPeriodResult->oe_kurzbz, 'lehre/unterrichtszeiten_gk')) { + $this->terminateWithError($this->p->t('ui', 'keineBerechtigung')); + } + $this->db->trans_start(); $timeSlotGroupIdentifier = uniqid(); @@ -286,6 +356,12 @@ class ClassScheduleApi extends FHCAPI_Controller } public function deleteClassTimeSlotsForValidityPeriodPerGroup($classTimeSlotValidityPeriodId, $groupIdentifikator) { + $validityPeriodResult = $this->ClassTimeSlotValidityPeriodModel->loadWhere(['unterrichtszeitengueltigkeit_id' => $classTimeSlotValidityPeriodId]); + $validityPeriodResult = $this->getDataOrTerminateWithError($validityPeriodResult)[0]; + if (!$this->isUserEntitledForOrganizationalUnit($validityPeriodResult->oe_kurzbz, 'lehre/unterrichtszeiten_gk')) { + $this->terminateWithError($this->p->t('ui', 'keineBerechtigung')); + } + $this->db->trans_start(); $result = $this->ClassTimeSlotModel->delete(['unterrichtszeitengueltigkeit_id'=> $classTimeSlotValidityPeriodId, 'unterrichtszeit_gruppe_identifikator' => $groupIdentifikator]); @@ -542,5 +618,11 @@ class ClassScheduleApi extends FHCAPI_Controller return true; } + + private function isUserEntitledForOrganizationalUnit($organizationalUnitShortCode, $requiredPermission) + { + $entitledOrganizationalUnitsShortCodes = $this->permissionlib->getOE_isEntitledFor($requiredPermission); + return in_array($organizationalUnitShortCode, $entitledOrganizationalUnitsShortCodes); + } } diff --git a/application/controllers/api/frontend/v1/organisation/OrganizationalUnitApi.php b/application/controllers/api/frontend/v1/organisation/OrganizationalUnitApi.php index 6fa5a50f8..f79b67b51 100644 --- a/application/controllers/api/frontend/v1/organisation/OrganizationalUnitApi.php +++ b/application/controllers/api/frontend/v1/organisation/OrganizationalUnitApi.php @@ -30,7 +30,7 @@ class OrganizationalUnitApi extends FHCAPI_Controller $this->load->library('form_validation'); - $this->load->model('education/ClassTimeSlotValidityPeriod_model', "ClassTimeSlotValidityPeriodModel"); + $this->load->model('organisation/Organisationseinheit_model', 'OrganisationseinheitModel'); // Loads phrases system $this->loadPhrases([ @@ -44,8 +44,11 @@ class OrganizationalUnitApi extends FHCAPI_Controller public function getAllOrganizationalUnits() { - $this->load->model('organisation/Organisationseinheit_model', 'OrganisationseinheitModel'); + $entitledOrganizationalUnitsShortCodes = $this->permissionlib->getOE_isEntitledFor('basis/organisationseinheit'); + + $this->OrganisationseinheitModel->db->where_in('oe_kurzbz', $entitledOrganizationalUnitsShortCodes); $result = $this->OrganisationseinheitModel->load(); + $organization_units_result = $this->getDataOrTerminateWithError($result); $this->terminateWithSuccess($organization_units_result); diff --git a/application/controllers/api/frontend/v1/organisation/Studienplan.php b/application/controllers/api/frontend/v1/organisation/Studienplan.php index 94d195f90..f22e6eb9f 100644 --- a/application/controllers/api/frontend/v1/organisation/Studienplan.php +++ b/application/controllers/api/frontend/v1/organisation/Studienplan.php @@ -22,7 +22,8 @@ class Studienplan extends FHCAPI_Controller parent::__construct([ 'getAllStudyPlans' => self::PERM_LOGGED, 'getStudyPlansByOrganizationalUnitAndSemesterDates' => self::PERM_LOGGED, - 'getBySemester' => self::PERM_LOGGED + 'getBySemester' => self::PERM_LOGGED, + 'getStudyPlan' => self::PERM_LOGGED, ]); } @@ -96,4 +97,15 @@ class Studienplan extends FHCAPI_Controller $this->terminateWithSuccess(hasData($result) ? getData($result) : []); } + + public function getStudyPlan($id) + { + $this->load->model('organisation/Studienplan_model', 'StudienplanModel'); + + $result = $this->StudienplanModel->loadWhere(['studienplan_id' => $id]); + if (isError($result)) $this->terminateWithError(getError($result), self::ERROR_TYPE_DB); + if (!hasData($result)) return $this->terminateWithSuccess(null); + + $this->terminateWithSuccess(getData($result)[0]); + } } diff --git a/application/views/lehre/class_schedule/index.php b/application/views/lehre/class_schedule/index.php index 7d00e8c9c..b27a97457 100644 --- a/application/views/lehre/class_schedule/index.php +++ b/application/views/lehre/class_schedule/index.php @@ -21,7 +21,8 @@ $includesArray = array( 'public/css/components/verticalsplit.css', 'public/extensions/FHC-Core-Developer/css/FhcMain.css', 'public/css/components/calendar.css', - 'public/css/components/classSchedule.css' + 'public/css/components/classSchedule.css', + 'public/css/components/vue-datepicker.css', ) ); diff --git a/public/js/api/factory/classSchedule.js b/public/js/api/factory/classSchedule.js index cd4ce6bbb..a0a037f80 100644 --- a/public/js/api/factory/classSchedule.js +++ b/public/js/api/factory/classSchedule.js @@ -16,10 +16,11 @@ */ export default { - getAllClassTimeValidityPeriods() { + getAllClassTimeValidityPeriods(params) { return { method: "get", url: "/api/frontend/v1/ClassScheduleApi/getAllClassTimeValidityPeriods", + params, }; }, getAllClassTimeValidityPeriodsPerOrganizationalUnit(organizationUnitId) { diff --git a/public/js/api/factory/studienplan.js b/public/js/api/factory/studienplan.js index 133a8123d..42c7a9471 100644 --- a/public/js/api/factory/studienplan.js +++ b/public/js/api/factory/studienplan.js @@ -28,4 +28,10 @@ export default { }, }; }, + getStudyPlan(studienplan_id) { + return { + method: "get", + url: `api/frontend/v1/organisation/studienplan/getStudyPlan/${studienplan_id}`, + }; + }, }; diff --git a/public/js/apps/lehre/ClassScheduleApp.js b/public/js/apps/lehre/ClassScheduleApp.js index 0dc8e403f..abaf60dae 100644 --- a/public/js/apps/lehre/ClassScheduleApp.js +++ b/public/js/apps/lehre/ClassScheduleApp.js @@ -23,6 +23,8 @@ import FhcAlert from "../../plugins/FhcAlert.js"; import Phrasen from "../../plugins/Phrasen.js"; import FhcApi from "../../plugins/Api.js"; +import {capitalize} from "../../helpers/StringHelpers.js"; + const ciPath = FHC_JS_DATA_STORAGE_OBJECT.app_root.replace(/(https:|)(^|\/\/)(.*?\/)/g, "") + FHC_JS_DATA_STORAGE_OBJECT.ci_router; @@ -64,6 +66,8 @@ const app = Vue.createApp({ }, }); +app.config.globalProperties.$capitalize = capitalize; + app .use(router) .use(primevue.config.default, { zIndex: { overlay: 9999 } }) diff --git a/public/js/components/ClassSchedule/ClassScheduleCalendarSelector.js b/public/js/components/ClassSchedule/ClassScheduleCalendarSelector.js index b436bae49..f9cdb8e72 100644 --- a/public/js/components/ClassSchedule/ClassScheduleCalendarSelector.js +++ b/public/js/components/ClassSchedule/ClassScheduleCalendarSelector.js @@ -115,6 +115,17 @@ export default { deep: true, immediate: true, }, + classTimeSlotTypes() { + this.overlays = this.overlays.map((overlay) => { + let type = this.classTimeSlotTypes.find( + (type) => type.unterrichtszeitentyp_kurzbz === overlay.type + ); + if (type) { + overlay.hexColor = type.hintergrundfarbe; + } + return overlay; + }); + }, overlays: { handler(newVal) { this.$emit("overlaysChanged", newVal); @@ -202,9 +213,14 @@ export default { return firstTimeSlotFragment + "-" + lastTimeSlotFragment; }, + userLanguage() { + return Vue.ref(FHC_JS_DATA_STORAGE_OBJECT.user_language); + } }, methods: { createOverlay() { + this.hideOverlayClassTimeTypePopover(); + let overlayElement; overlayElement = this.$refs.calendarSelectorContainer.querySelector( @@ -350,6 +366,8 @@ export default { this.$refs.calendarSelectorContainer .querySelector(`#${overlayId}`) .remove(); + + this.hideOverlayClassTimeTypePopover(); }, getLineNumberFromSelectedElementNumber(selectedElementNumber) { let timeSlotsCount = this.timeSlotsInDay.length; @@ -696,6 +714,7 @@ export default { this.selected = []; }, handleOverlayDrop(event) { + this.hideOverlayClassTimeTypePopover(); if (this.$props.isPreviewMode) return; let dropzoneItem = event.target; @@ -725,25 +744,22 @@ export default { `[data-number='${startElementNumber}']`, ); - // get mouse position const mouseY = event.clientY; - // get delta Y from mouse position to top of the dropzone item + const dropzoneItemRect = dropzoneItem.getBoundingClientRect(); const deltaY = mouseY - dropzoneItemRect.top; - // get top of the start element const startElementRect = startElement.getBoundingClientRect(); const startElementTop = startElementRect.top; - // add delta Y to top of the start element to get new top for the dropzone item const newTop = startElementTop + deltaY; - //find which item has the closest top to the new top and get its data-number attribute + const partBodies = this.$refs.calendarSelectorContainer.querySelectorAll( "div[data-weekday='" + dropzoneOverlay.weekday + "']", ); - // see which item has the new top in between its top and bottom and get its data-number attribute + let closestPartBody = null; partBodies.forEach((partBody) => { const rect = partBody.getBoundingClientRect(); @@ -1439,10 +1455,20 @@ export default { }); }, 10); }, + hideOverlayClassTimeTypePopover() { + if (this.visiblePopover) { + this.visiblePopover.dispose(); + this.visiblePopover = null; + } + }, + getClassTimeSlotTypeLabel(classTimeSlotType) { + if (!classTimeSlotType) return ""; + return this.userLanguage?.value === 'English' ? + classTimeSlotType.bezeichnung_mehrsprachig[1].value : classTimeSlotType.bezeichnung_mehrsprachig[0].value; + } }, unmounted() { - this.visiblePopover?.dispose(); - this.visiblePopover = null; + this.hideOverlayClassTimeTypePopover(); }, template: /*html*/ `
@@ -1760,7 +1786,7 @@ export default { :data-type="type.unterrichtszeitentyp_kurzbz" class="btn btn-sm btn-outline-dark class-schedule-type-selector-option" > - {{ type.bezeichnung_mehrsprachig[0].value }} + {{ getClassTimeSlotTypeLabel(type) }}
diff --git a/public/js/components/ClassSchedule/ClassScheduleOverview.js b/public/js/components/ClassSchedule/ClassScheduleOverview.js index 02fbdfee8..d58595230 100644 --- a/public/js/components/ClassSchedule/ClassScheduleOverview.js +++ b/public/js/components/ClassSchedule/ClassScheduleOverview.js @@ -1,9 +1,13 @@ -// Import the Core Filter- and Core RESTClient Component to build your table and handle data import { CoreFilterCmpt } from "../filter/Filter.js"; import ApiClassSchedule from "../../../js/api/factory/classSchedule.js"; +import ApiStudienSemester from "../../../js/api/factory/studiensemester.js"; +import { formatDate } from "../../helpers/DateHelpers.js"; import ClassScheduleTypeModal from "./ClassScheduleTypeModal.js"; import ClassScheduleValidityPeriodModal from "./ClassScheduleValidityPeriodModal.js"; +import CoreForm from "../Form/Form.js"; +import FormInput from "../Form/Input.js"; +import ApiOrganizationalUnit from "../../../js/api/factory/organizationalUnit.js"; export default { name: "ClassScheduleOverview", @@ -11,6 +15,8 @@ export default { CoreFilterCmpt, ClassScheduleTypeModal, ClassScheduleValidityPeriodModal, + CoreForm, + FormInput, }, props: { permissions: Object, @@ -22,6 +28,29 @@ export default { this.permissions["lehre/unterrichtszeiten_typ_w"] || false, }; }, + watch: { + filterData: { + handler(newValue) { + this.$refs.classTimeSlotValidityPeriodsTable.tabulator.setData("/", { + organizationalUnitShortCode: + newValue.selectedOrganizationalUnit?.value, + validityPeriodFrom: newValue.validityPeriodFrom + ? formatDate(newValue.validityPeriodFrom, "yyyy-MM-dd") + : null, + validityPeriodTo: newValue.validityPeriodTo + ? formatDate(newValue.validityPeriodTo, "yyyy-MM-dd") + : null, + }); + }, + deep: true, + }, + selectedSemester: { + handler(newValue) { + this.filterData.validityPeriodFrom = this.selectedSemester.start; + this.filterData.validityPeriodTo = this.selectedSemester.ende; + }, + }, + }, data: () => { return { phrasesLoaded: false, @@ -32,14 +61,25 @@ export default { mondayClassTimeSlots: [], isClassTimeSlotTypeModalVisible: false, isClassTimeSlotValidityPeriodModalVisible: false, + organizationalUnits: [], + filteredOrganizationalUnits: [], + allSemesters: [], + filteredSemesters: [], + filterData: { + selectedOrganizationalUnit: null, + validityPeriodFrom: null, + validityPeriodTo: null, + }, + selectedSemester: null, }; }, computed: { tabulatorOptions() { const options = { ajaxURL: "dummy", - ajaxRequestFunc: async () => - await this.getParsedClassTimeSlotValidityPeriodData(), + ajaxRequestFunc: async () => { + return await this.getParsedClassTimeSlotValidityPeriodData(); + }, ajaxResponse: (url, params, response) => response, persistenceID: "core_class_schedule_validity_periods", selectableRows: true, @@ -163,8 +203,7 @@ export default { container.style.width = "100%"; let label = document.createElement("span"); - label.textContent = - value + " (" + count + " item" + (count > 1 ? "s" : "") + ")"; + label.textContent = value; container.append(label); let button = document.createElement("button"); @@ -187,8 +226,7 @@ export default { container.style.width = "100%"; let label = document.createElement("span"); - label.textContent = - value + " (" + count + " item" + (count > 1 ? "s" : "") + ")"; + label.textContent = value; container.append(label); let button = document.createElement("button"); @@ -250,14 +288,38 @@ export default { ]; return events; }, + dropdownParsedOrganizationalUnits() { + return this.organizationalUnits.map((unit) => { + return { + label: `${unit.bezeichnung} (${unit.organisationseinheittyp_kurzbz})`, + value: unit.oe_kurzbz, + }; + }); + }, + dropdownParsedSemesters() { + return this.allSemesters.map((semester) => { + return { + label: semester.studiensemester_kurzbz, + value: semester.studiensemester_kurzbz, + start: semester.start, + ende: semester.ende, + }; + }); + } }, methods: { - test() { - alert("test"); - }, async getParsedClassTimeSlotValidityPeriodData() { let getAllClassTimeValidityPeriodsResponse = await this.$api.call( - ApiClassSchedule.getAllClassTimeValidityPeriods(), + ApiClassSchedule.getAllClassTimeValidityPeriods({ + organizationalUnitShortCode: + this.filterData.selectedOrganizationalUnit?.value, + validityPeriodFrom: this.filterData.validityPeriodFrom + ? formatDate(this.filterData.validityPeriodFrom, "yyyy-MM-dd") + : null, + validityPeriodTo: this.filterData.validityPeriodTo + ? formatDate(this.filterData.validityPeriodTo, "yyyy-MM-dd") + : null, + }), ); if (getAllClassTimeValidityPeriodsResponse.meta.status === "success") { @@ -323,6 +385,36 @@ export default { resetClassTimeSlotValidityPeriodModal() { this.isClassTimeSlotValidityPeriodModalVisible = false; }, + filterOrganizationalUnits(event) { + let defaultItem = { + label: '----------', + value: null, + }; + + const query = event.query.toLowerCase(); + if (!query) { + return this.filteredOrganizationalUnits = [defaultItem, ...this.dropdownParsedOrganizationalUnits]; + } + + return this.filteredOrganizationalUnits = [defaultItem].concat(this.dropdownParsedOrganizationalUnits.filter((unit) => { + return unit.label.toLowerCase().includes(query); + })); + }, + filterSemesters(event) { + let defaultItem = { + label: '----------', + value: null, + }; + + const query = event.query.toLowerCase(); + if (!query) { + return this.filteredSemesters = [defaultItem, ...this.dropdownParsedSemesters]; + } + + return this.filteredSemesters = [defaultItem].concat(this.dropdownParsedSemesters.filter((semester) => { + return semester.label.toLowerCase().includes(query); + })); + } }, async created() { let getAllClassTimeValidityPeriodsResponse = await this.$api.call( @@ -337,6 +429,32 @@ export default { getAllClassTimeValidityPeriodsResponse.meta.message, ); } + + let getAllOrganizationalUnitsResponse = await this.$api.call( + ApiOrganizationalUnit.getAllOrganizationalUnits(), + ); + if (getAllOrganizationalUnitsResponse.meta.status === "success") { + this.organizationalUnits = getAllOrganizationalUnitsResponse.data.sort( + (a, b) => a.bezeichnung.localeCompare(b.bezeichnung), + ); + } else { + console.error( + "Error fetching organizational units:", + getAllOrganizationalUnitsResponse.meta.message, + ); + } + + let getAllSemestersResponse = await this.$api.call( + ApiStudienSemester.getAll('DESC'), + ); + if (getAllSemestersResponse.meta.status === "success") { + this.allSemesters = getAllSemestersResponse.data; + } else { + console.error( + "Error fetching semesters:", + getAllSemestersResponse.meta.message, + ); + } }, mounted() { this.$p @@ -349,8 +467,9 @@ export default {

{{ $p.t("ui", "classScheduleOverviewHeading") }}

`, }; diff --git a/public/js/components/ClassSchedule/ClassScheduleValidityPeriodModal.js b/public/js/components/ClassSchedule/ClassScheduleValidityPeriodModal.js index cb38231f3..c2871063f 100644 --- a/public/js/components/ClassSchedule/ClassScheduleValidityPeriodModal.js +++ b/public/js/components/ClassSchedule/ClassScheduleValidityPeriodModal.js @@ -47,9 +47,23 @@ export default { .call(ApiClassSchedule.getClassTimeValidityPeriod(newValue)) .then((response) => { let validityPeriodData = response.data[0]; + let organizationalUnit = this.organizationalUnits.find( + (unit) => unit.oe_kurzbz === validityPeriodData.oe_kurzbz, + ); + if (!organizationalUnit) { + console.error( + "Organizational unit not found for validity period:", + validityPeriodData, + ); + this.$fhcAlert.alertError(this.$p.t("ui", "errorLoadingData")); + return; + } + + this.editedClassTimeSlotValidityPeriod = validityPeriodData; + this.classTimeSlotValidityPeriodFormData = { id: validityPeriodData.unterrichtszeitengueltigkeit_id, - organizationalUnitShortCode: validityPeriodData.oe_kurzbz, + organizationalUnit, studyPlanId: validityPeriodData.studienplan_id, classTimeSlotTypeShortcode: validityPeriodData.unterrichtszeitentyp_kurzbz, @@ -68,48 +82,29 @@ export default { this.$fhcAlert.handleSystemError(error); }); }, - async "classTimeSlotValidityPeriodFormData.organizationalUnitShortCode"( - newValue, - ) { - if (!newValue) { - this.classTimeSlotValidityPeriodFormData.studyPlanId = null; - return; - } - - this.refetchFilterableOptions(); + "classTimeSlotValidityPeriodFormData.organizationalUnit"() { + this.updateClassTimeSlotValidityPeriodFormDataWatcher(); }, - async "classTimeSlotValidityPeriodFormData.validityPeriodFrom"(newValue) { - if (!newValue) { - this.classTimeSlotValidityPeriodFormData.studyPlanId = null; - return; - } - - this.refetchFilterableOptions(); + "classTimeSlotValidityPeriodFormData.validityPeriodFrom"() { + this.updateClassTimeSlotValidityPeriodFormDataWatcher(); }, - async "classTimeSlotValidityPeriodFormData.validityPeriodTo"(newValue) { - if (!newValue) { - this.classTimeSlotValidityPeriodFormData.studyPlanId = null; - return; - } - - this.refetchFilterableOptions(); - }, - async "classTimeSlotValidityPeriodFormData.studyPlanId"(newValue) { - this.refetchFilterableOptions(); + "classTimeSlotValidityPeriodFormData.validityPeriodTo"() { + this.updateClassTimeSlotValidityPeriodFormDataWatcher(); }, }, data: () => { return { + editedValidityPeriod: null, isFormVisible: false, - isEditInProgress: false, organizationalUnits: [], + filteredOrganizationalUnits: [], studyPlans: [], studySemesters: [], studySemestersByNumber: [], classTimeSlotTypes: [], classTimeSlotValidityPeriodFormData: { id: null, - organizationalUnitShortCode: null, + organizationalUnit: null, studyPlanId: null, classTimeSlotTypeShortcode: null, validityPeriodFrom: null, @@ -120,9 +115,13 @@ export default { }; }, computed: { + isEditMode() { + return !!this.$props.editedClassTimeSlotValidityPeriodId; + }, isStudyPlanSelectDisabled() { return ( - !this.classTimeSlotValidityPeriodFormData.organizationalUnitShortCode || + !this.classTimeSlotValidityPeriodFormData.organizationalUnit + ?.oe_kurzbz || !this.classTimeSlotValidityPeriodFormData.validityPeriodFrom || !this.classTimeSlotValidityPeriodFormData.validityPeriodTo ); @@ -142,23 +141,62 @@ export default { this.classTimeSlotValidityPeriodFormData.validityPeriodTo, ); }, + userLanguage() { + return Vue.ref(FHC_JS_DATA_STORAGE_OBJECT.user_language); + }, }, methods: { + updateClassTimeSlotValidityPeriodFormDataWatcher() { + if ( + !this.classTimeSlotValidityPeriodFormData.organizationalUnit + ?.oe_kurzbz || + !this.classTimeSlotValidityPeriodFormData.validityPeriodFrom || + !this.classTimeSlotValidityPeriodFormData.validityPeriodTo + ) { + this.classTimeSlotValidityPeriodFormData.studyPlanId = null; + return; + } + + this.refetchFilterableOptions(); + }, async refetchFilterableOptions() { this.studyPlans = await this.getTargetedStudyPlans( - this.classTimeSlotValidityPeriodFormData.organizationalUnitShortCode, + this.classTimeSlotValidityPeriodFormData.organizationalUnit?.oe_kurzbz, this.formattedValidityPeriodFrom, this.formattedValidityPeriodTo, ); + if (this.isEditMode) { + let isStudyPlanStillValid = this.studyPlans.some( + (plan) => + plan.studienplan_id === + this.editedClassTimeSlotValidityPeriod.studienplan_id, + ); + + if (!isStudyPlanStillValid) { + let editedStudyPlan = await this.getStudyPlan( + this.editedClassTimeSlotValidityPeriod.studienplan_id, + ); + if (editedStudyPlan) { + this.studyPlans.push(editedStudyPlan); + } else { + console.error( + "Edited study plan not found:", + this.editedClassTimeSlotValidityPeriod.studienplan_id, + ); + } + } + } + let studySemestersByDates = await this.getStudySemestersByOrganizationalUnitAndDates( - this.classTimeSlotValidityPeriodFormData.organizationalUnitShortCode, + this.classTimeSlotValidityPeriodFormData.organizationalUnit + ?.oe_kurzbz, this.formattedValidityPeriodFrom, this.formattedValidityPeriodTo, ); - let studySemesters = new Array( + let studySemesters = new Array( ...new Set( studySemestersByDates .map((s) => s.semester_numbers) @@ -220,6 +258,24 @@ export default { ? getAllStudyPlansResponse.data : []; }, + async getStudyPlan(studienplan_id) { + if (!studienplan_id) { + return null; + } + + let getStudyPlanResponse = await this.$api.call( + ApiStudienPlan.getStudyPlan(studienplan_id), + ); + if (getStudyPlanResponse.meta.status !== "success") { + console.error( + "Error fetching study plan details:", + getStudyPlanResponse.meta.message, + ); + return null; + } + + return getStudyPlanResponse.data ? getStudyPlanResponse.data : null; + }, async getStudySemestersByOrganizationalUnitAndDates( organizationalUnitShortCode, validityPeriodFrom, @@ -251,13 +307,21 @@ export default { ? getStudySemestersResponse.data : []; }, - async getStudySemestersByStudyPlanAndDates(studyProgramId, validityPeriodFrom, validityPeriodTo) { + async getStudySemestersByStudyPlanAndDates( + studyProgramId, + validityPeriodFrom, + validityPeriodTo, + ) { if (!studyProgramId || !validityPeriodFrom || !validityPeriodTo) { return []; } let getStudySemestersResponse = await this.$api.call( - ApiStudienSemester.getStudySemestersByStudyPlanAndDates(studyProgramId, validityPeriodFrom, validityPeriodTo), + ApiStudienSemester.getStudySemestersByStudyPlanAndDates( + studyProgramId, + validityPeriodFrom, + validityPeriodTo, + ), ); if (getStudySemestersResponse.meta.status !== "success") { console.error( @@ -273,10 +337,12 @@ export default { createClassTimeSlotValidityPeriod() { return this.$refs.classTimeSlotValidityPeriodData .call( - ApiClassSchedule.createClassTimeSlotValidityPeriod( - this.id, - this.classTimeSlotValidityPeriodFormData, - ), + ApiClassSchedule.createClassTimeSlotValidityPeriod(this.id, { + ...this.classTimeSlotValidityPeriodFormData, + organizationalUnitShortCode: + this.classTimeSlotValidityPeriodFormData.organizationalUnit + ?.oe_kurzbz, + }), ) .then((response) => { this.$fhcAlert.alertSuccess(this.$p.t("ui", "successSave")); @@ -299,7 +365,12 @@ export default { ApiClassSchedule.updateClassTimeSlotValidityPeriod( this.id, this.classTimeSlotValidityPeriodFormData.id, - this.classTimeSlotValidityPeriodFormData, + { + ...this.classTimeSlotValidityPeriodFormData, + organizationalUnitShortCode: + this.classTimeSlotValidityPeriodFormData.organizationalUnit + ?.oe_kurzbz, + }, ), ) .then((response) => { @@ -307,6 +378,9 @@ export default { this.$refs.classTimeSlotValidityPeriodModal.hide(); this.resetClassTimeSlotValidityPeriodModal(); window.scrollTo(0, 0); + + this.editedClassTimeSlotValidityPeriod = null; + this.$emit("classTimeSlotValidityPeriodUpdated"); }) .catch((error) => { @@ -321,7 +395,7 @@ export default { this.$refs.classTimeSlotValidityPeriodData?.clearValidation(); this.classTimeSlotValidityPeriodFormData = { id: null, - organizationalUnitShortCode: null, + organizationalUnit: null, studyPlanId: null, classTimeSlotTypeShortcode: null, validityPeriodFrom: null, @@ -330,6 +404,26 @@ export default { description: null, }; }, + filterOrganizationalUnits(event) { + const query = event.query.toLowerCase(); + if (!query) { + return (this.filteredOrganizationalUnits = [ + ...this.organizationalUnits, + ]); + } + + return (this.filteredOrganizationalUnits = + this.organizationalUnits.filter((unit) => { + let label = `${unit.bezeichnung} (${unit.organisationseinheittyp_kurzbz})`; + return label.toLowerCase().includes(query); + })); + }, + getClassTimeSlotTypeLabel(classTimeSlotType) { + if (!classTimeSlotType) return ""; + return this.userLanguage?.value === "English" + ? classTimeSlotType.bezeichnung_mehrsprachig[1].value + : classTimeSlotType.bezeichnung_mehrsprachig[0].value; + }, }, async created() { let getAllOrganizationalUnitsResponse = await this.$api.call( @@ -392,18 +486,17 @@ export default {
-
@@ -449,7 +542,7 @@ export default { :key="studyPlan.studienplan_id" :value="studyPlan.studienplan_id" > - {{studyPlan.bezeichnung}} - {{studyPlan.studienplan_id}} + {{studyPlan.bezeichnung}}
@@ -478,12 +571,13 @@ export default { :label="$p.t('ui/classTimeSlotType')" v-model="classTimeSlotValidityPeriodFormData.classTimeSlotTypeShortcode" > +
diff --git a/public/js/components/ClassSchedule/ClassScheduleValidityPeriodOverview.js b/public/js/components/ClassSchedule/ClassScheduleValidityPeriodOverview.js index 2215cafed..88debe983 100644 --- a/public/js/components/ClassSchedule/ClassScheduleValidityPeriodOverview.js +++ b/public/js/components/ClassSchedule/ClassScheduleValidityPeriodOverview.js @@ -248,8 +248,14 @@ export default {

{{ classScheduleValidityPeriodStartDate }} - {{ classScheduleValidityPeriodEndDate }}

-

{{ $p.t("lehre", "ausbildungssemester") }}: {{ classTimeSlotValidityPeriod?.ausbildungssemester }}

-

+

{{ $p.t("lehre", "organisationseinheit") }}: {{ classTimeSlotValidityPeriod?.oe_bezeichnung }} ({{ classTimeSlotValidityPeriod?.oe_organisationseinheittyp_kurzbz }})

+

{{ $p.t("lehre", "studienplan") }}: {{ classTimeSlotValidityPeriod?.studienplan_bezeichnung }}

+
{{ $p.t("lehre", "ausbildungssemester") }}: + {{ classTimeSlotValidityPeriod?.ausbildungssemester }} +
+
{{ $p.t("global", "anmerkung") }}: + {{ classTimeSlotValidityPeriod?.anmerkung }} +
@@ -290,7 +296,7 @@ export default {
-

{{ $p.t("ui", "noClassScheduleValidityPeriodTimeSlotsFound") }}

+

{{ $p.t("ui", "noClassScheduleValidityPeriodTimeSlotsFound") }}

-

{{ $p.t("ui", "noClassScheduleValidityPeriodTimeSlotsFound") }}

+

{{ $p.t("ui", "noClassScheduleValidityPeriodTimeSlotsFound") }}

diff --git a/system/phrasesupdate.php b/system/phrasesupdate.php index 5913e1d67..6337f61a5 100644 --- a/system/phrasesupdate.php +++ b/system/phrasesupdate.php @@ -58416,6 +58416,46 @@ I have been informed that I am under no obligation to consent to the transmissio ) ) ), + array( + 'app' => 'core', + 'category' => 'ui', + 'phrase' => 'item', + 'insertvon' => 'system', + 'phrases' => array( + array( + 'sprache' => 'German', + 'text' => 'element', + 'description' => '', + 'insertvon' => 'system' + ), + array( + 'sprache' => 'English', + 'text' => 'item', + 'description' => '', + 'insertvon' => 'system' + ) + ) + ), + array( + 'app' => 'core', + 'category' => 'ui', + 'phrase' => 'items', + 'insertvon' => 'system', + 'phrases' => array( + array( + 'sprache' => 'German', + 'text' => 'elemente', + 'description' => '', + 'insertvon' => 'system' + ), + array( + 'sprache' => 'English', + 'text' => 'items', + 'description' => '', + 'insertvon' => 'system' + ) + ) + ), );