diff --git a/application/config/routes.php b/application/config/routes.php index 0df4a5f00..b368202e5 100644 --- a/application/config/routes.php +++ b/application/config/routes.php @@ -120,7 +120,7 @@ $route['api/frontend/v1/stv/[sS]tudents/([WS]S[0-9]{4})/prestudent/(:num)'] = 'a $route['api/frontend/v1/stv/[sS]tudents/([WS]S[0-9]{4})/person/(:num)'] = 'api/frontend/v1/stv/Students/getPerson/$1/$2'; -$route['lehre/ClassSchedule/(:any)/(:any)'] = 'lehre/ClassSchedule/index'; +$route['lehre/ClassSchedule/.*'] = 'lehre/ClassSchedule/index'; // load routes from extensions, also look for environment-specific configs $subdirs = ['application/config/extensions', 'application/config/' . ENVIRONMENT . '/extensions']; diff --git a/application/controllers/api/frontend/v1/ClassScheduleApi.php b/application/controllers/api/frontend/v1/ClassScheduleApi.php index fb09ead03..6c4bac78b 100644 --- a/application/controllers/api/frontend/v1/ClassScheduleApi.php +++ b/application/controllers/api/frontend/v1/ClassScheduleApi.php @@ -27,6 +27,7 @@ class ClassScheduleApi extends FHCAPI_Controller { parent::__construct([ 'getAllClassTimeValidityPeriods'=> array('lehre/unterrichtszeiten_gk:r'), + 'getAllClassTimeValidityPeriodsPerOrganizationalUnit' => array('lehre/unterrichtszeiten_gk:r'), 'getClassTimeValidityPeriod' => array('lehre/unterrichtszeiten_gk:r'), 'createClassTimeSlotValidityPeriod' => array('lehre/unterrichtszeiten_gk:rw'), 'updateClassTimeSlotValidityPeriod' => array('lehre/unterrichtszeiten_gk:rw'), @@ -60,14 +61,40 @@ class ClassScheduleApi extends FHCAPI_Controller public function getAllClassTimeValidityPeriods() { + $this->ClassTimeSlotValidityPeriodModel->addSelect( + 'lehre.tbl_unterrichtszeiten_gueltigkeit.*,' . + 'public.tbl_organisationseinheit.bezeichnung as organisationseinheit_bezeichnung,' . + 'public.tbl_organisationseinheit.organisationseinheittyp_kurzbz as organisationseinheit_organisationseinheittyp_kurzbz,' . + 'lehre.tbl_studienplan.bezeichnung as studienplan_bezeichnung,' . + 'lehre.tbl_unterrichtszeiten_typ.bezeichnung_mehrsprachig as unterrichtszeiten_typ_bezeichnung_mehrsprachig, ' + ); $this->ClassTimeSlotValidityPeriodModel->addJoin('lehre.tbl_studienplan', 'lehre.tbl_studienplan.studienplan_id=lehre.tbl_unterrichtszeiten_gueltigkeit.studienplan_id', 'LEFT'); $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'); $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); } + public function getAllClassTimeValidityPeriodsPerOrganizationalUnit($organizationUnitId) + { + $this->ClassTimeSlotValidityPeriodModel->addSelect( + 'lehre.tbl_unterrichtszeiten_gueltigkeit.*,' . + 'public.tbl_organisationseinheit.bezeichnung as organisationseinheit_bezeichnung,' . + 'public.tbl_organisationseinheit.organisationseinheittyp_kurzbz as organisationseinheit_organisationseinheittyp_kurzbz,' . + 'lehre.tbl_studienplan.bezeichnung as studienplan_bezeichnung,' . + 'lehre.tbl_unterrichtszeiten_typ.bezeichnung_mehrsprachig as unterrichtszeiten_typ_bezeichnung_mehrsprachig, ' + ); + $this->ClassTimeSlotValidityPeriodModel->addJoin('lehre.tbl_studienplan', 'lehre.tbl_studienplan.studienplan_id=lehre.tbl_unterrichtszeiten_gueltigkeit.studienplan_id', 'LEFT'); + $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'); + $class_time_slot_validity_period_res = $this->ClassTimeSlotValidityPeriodModel->loadWhere(['lehre.tbl_unterrichtszeiten_gueltigkeit.oe_kurzbz' => $organizationUnitId]); + $class_time_slot_validity_period_res = $this->getDataOrTerminateWithError($class_time_slot_validity_period_res); + $this->terminateWithSuccess($class_time_slot_validity_period_res); + } + 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'); diff --git a/application/controllers/api/frontend/v1/organisation/Studienplan.php b/application/controllers/api/frontend/v1/organisation/Studienplan.php index ab4fb0755..94d195f90 100644 --- a/application/controllers/api/frontend/v1/organisation/Studienplan.php +++ b/application/controllers/api/frontend/v1/organisation/Studienplan.php @@ -21,6 +21,7 @@ class Studienplan extends FHCAPI_Controller // TODO(chris): access! parent::__construct([ 'getAllStudyPlans' => self::PERM_LOGGED, + 'getStudyPlansByOrganizationalUnitAndSemesterDates' => self::PERM_LOGGED, 'getBySemester' => self::PERM_LOGGED ]); } @@ -33,6 +34,26 @@ class Studienplan extends FHCAPI_Controller $this->terminateWithSuccess($studien_plan_result); } + public function getStudyPlansByOrganizationalUnitAndSemesterDates($organizationalUnitShortCode) + { + $this->load->model('organisation/Organisationseinheit_model', 'OrganisationseinheitModel'); + $this->load->model('organisation/Studiengang_model', 'StudiengangModel'); + $this->load->model('organisation/Studienordnung_model', 'StudienordnungModel'); + $this->load->model('organisation/Studienplan_model', 'StudienplanModel'); + + $startDate = date('Y-m-d', strtotime($this->input->get('filter[startDate]'))); + $endDate = date('Y-m-d', strtotime($this->input->get('filter[endDate]'))); + if (!$startDate || !$endDate) { + return $this->terminateWithError($this->p->t('ui', 'error_missingId', ['id'=> 'Start- oder Enddatum']), self::ERROR_TYPE_GENERAL); + } + + $studyPlansResponse = $this->StudienplanModel->getStudyPlansForOrganizationalUnitAndDatesQueryResponse($organizationalUnitShortCode, $startDate, $endDate); + if (isError($studyPlansResponse)) $this->terminateWithError(getError($studyPlansResponse), self::ERROR_TYPE_DB); + if (!hasData($studyPlansResponse)) return $this->terminateWithSuccess(null); + + return $this->terminateWithSuccess($this->getDataOrTerminateWithError($studyPlansResponse)); + } + public function getBySemester() { $this->load->model('organisation/Studienplan_model', 'StudienplanModel'); diff --git a/application/controllers/api/frontend/v1/organisation/Studiensemester.php b/application/controllers/api/frontend/v1/organisation/Studiensemester.php index 3c6b72d2f..0ede3e00b 100644 --- a/application/controllers/api/frontend/v1/organisation/Studiensemester.php +++ b/application/controllers/api/frontend/v1/organisation/Studiensemester.php @@ -26,7 +26,9 @@ class Studiensemester extends FHCAPI_Controller 'getAll' => self::PERM_LOGGED, 'getAktNext' => self::PERM_LOGGED, 'getStudienjahrByStudiensemester' => self::PERM_LOGGED, - 'getAllStudiensemesterAndAktOrNext' => self::PERM_LOGGED + 'getAllStudiensemesterAndAktOrNext' => self::PERM_LOGGED, + 'getStudySemestersByOrganizationalUnitAndDates' => self::PERM_LOGGED, + 'getStudySemestersByStudyPlanAndDates' => self::PERM_LOGGED, ) ); // Load model StudiensemesterModel @@ -166,4 +168,51 @@ class Studiensemester extends FHCAPI_Controller $this->terminateWithSuccess(array($studiensemester, $aktuell)); } + + public function getStudySemestersByOrganizationalUnitAndDates($organizationalUnitShortCode) + { + $this->load->model('organisation/Studiengang_model', 'StudiengangModel'); + $this->load->model('organisation/Studienordnung_model', 'StudienordnungModel'); + $this->load->model('organisation/Studienplan_model', 'StudienplanModel'); + + $startDate = date('Y-m-d', strtotime($this->input->get('filter[startDate]'))); + $endDate = date('Y-m-d', strtotime($this->input->get('filter[endDate]'))); + if (!$startDate || !$endDate) { + return $this->terminateWithError($this->p->t('ui', 'error_missingId', ['id'=> 'Start- oder Enddatum']), self::ERROR_TYPE_GENERAL); + } + + $studyPlansResponse = $this->StudienplanModel->getStudyPlansForOrganizationalUnitQueryResponse($organizationalUnitShortCode); + if (isError($studyPlansResponse)) $this->terminateWithError(getError($studyPlansResponse), self::ERROR_TYPE_DB); + if (!hasData($studyPlansResponse)) return $this->terminateWithSuccess(null); + + $studyPlans = $this->getDataOrTerminateWithError($studyPlansResponse); + $studyPlansIds = array_map(function ($studyPlan) { return $studyPlan->studienplan_id; }, $studyPlans); + + $studySemestersResponse = $this->StudiensemesterModel->getStudySemestersByStudyPlansAndDatesQueryResponse($studyPlansIds, $startDate, $endDate); + if (isError($studySemestersResponse)) $this->terminateWithError(getError($studySemestersResponse), self::ERROR_TYPE_DB); + if (!hasData($studySemestersResponse)) return $this->terminateWithSuccess([]); + + $studySemesters = $this->getDataOrTerminateWithError($studySemestersResponse); + + return $this->terminateWithSuccess($studySemesters); + } + + public function getStudySemestersByStudyPlanAndDates() + { + $studyPlansId = $this->input->get('filter[studyPlanId]'); + + $startDate = date('Y-m-d', strtotime($this->input->get('filter[startDate]'))); + $endDate = date('Y-m-d', strtotime($this->input->get('filter[endDate]'))); + if (!$startDate || !$endDate) { + return $this->terminateWithError($this->p->t('ui', 'error_missingId', ['id'=> 'Start- oder Enddatum']), self::ERROR_TYPE_GENERAL); + } + + $studySemestersResponse = $this->StudiensemesterModel->getStudySemestersByStudyPlansAndDatesQueryResponse([$studyPlansId], $startDate, $endDate); + if (isError($studySemestersResponse)) $this->terminateWithError(getError($studySemestersResponse), self::ERROR_TYPE_DB); + if (!hasData($studySemestersResponse)) return $this->terminateWithSuccess([]); + + $studySemesters = $this->getDataOrTerminateWithError($studySemestersResponse); + + return $this->terminateWithSuccess($studySemesters); + } } diff --git a/application/models/organisation/Studienplan_model.php b/application/models/organisation/Studienplan_model.php index 4a5f87832..e7c2d49de 100644 --- a/application/models/organisation/Studienplan_model.php +++ b/application/models/organisation/Studienplan_model.php @@ -168,5 +168,30 @@ class Studienplan_model extends DB_Model return $this->loadWhere([ 'person_id' => $person_id ]); + } + + public function getStudyPlansForOrganizationalUnitQueryResponse($organizationalUnitShortCode) + { + $query = "SELECT sp.* FROM lehre.tbl_studienplan sp + JOIN lehre.tbl_studienordnung so ON sp.studienordnung_id = so.studienordnung_id + JOIN public.tbl_studiengang sg ON so.studiengang_kz = sg.studiengang_kz + WHERE sg.oe_kurzbz = ?"; + + return $this->execReadOnlyQuery($query, array($organizationalUnitShortCode)); + } + + public function getStudyPlansForOrganizationalUnitAndDatesQueryResponse($organizationalUnitShortCode, $startDate, $endDate) + { + $query = "SELECT DISTINCT sp.* FROM lehre.tbl_studienplan sp + JOIN lehre.tbl_studienordnung so ON sp.studienordnung_id = so.studienordnung_id + JOIN public.tbl_studiengang sg ON so.studiengang_kz = sg.studiengang_kz + AND sg.oe_kurzbz = ? AND sp.studienplan_id IN ( + SELECT sps.studienplan_id + FROM lehre.tbl_studienplan_semester sps + JOIN public.tbl_studiensemester ss ON ss.studiensemester_kurzbz = sps.studiensemester_kurzbz + WHERE (ss.start >= ? AND ss.ende <= ?) OR (ss.start <= ? AND ss.ende >= ?) + )"; + + return $this->execReadOnlyQuery($query, array($organizationalUnitShortCode, $startDate, $endDate, $startDate, $endDate)); } } diff --git a/application/models/organisation/Studiensemester_model.php b/application/models/organisation/Studiensemester_model.php index 5fa6ffb14..db65a920f 100644 --- a/application/models/organisation/Studiensemester_model.php +++ b/application/models/organisation/Studiensemester_model.php @@ -347,4 +347,24 @@ class Studiensemester_model extends DB_Model $result = $this->load($studiensemester_kurzbz); return hasData($result); } + + public function getStudySemestersByStudyPlansAndDatesQueryResponse($studyPlanIds, $fromDate, $toDate) + { + $subQuery = "( + SELECT ARRAY_AGG(DISTINCT sp.semester) + FROM lehre.tbl_studienplan_semester sp + WHERE sp.studienplan_id IN ? + AND sp.studiensemester_kurzbz = s.studiensemester_kurzbz + ) AS semester_numbers"; + + $query = "SELECT DISTINCT s.*," . $subQuery . " + FROM public.tbl_studiensemester s + JOIN lehre.tbl_studienplan_semester sp ON sp.studiensemester_kurzbz = s.studiensemester_kurzbz + WHERE sp.studienplan_id IN ? AND ( + (s.start >= ? AND s.ende <= ?) OR + (s.start <= ? AND s.ende >= ?) + )"; + + return $this->execQuery($query, array($studyPlanIds, $studyPlanIds, $fromDate, $toDate, $fromDate, $toDate)); + } } diff --git a/public/css/components/classSchedule.css b/public/css/components/classSchedule.css index de2f9a109..80f854541 100644 --- a/public/css/components/classSchedule.css +++ b/public/css/components/classSchedule.css @@ -11,7 +11,7 @@ } .fhc-resize-vertical { - cursor: ns-resize; + cursor: ns-resize; } .fhc-drag-handle:hover { @@ -21,4 +21,24 @@ .fhc-w-fit { width: fit-content; -} \ No newline at end of file +} + +.tabulator-group-level-0 { + background-color: #c6c6c6 !important; + display: flex; + align-items: center; +} + +.tabulator-group-level-1 { + background-color: #f5f5f5 !important; + display: flex; + align-items: center; +} + +.tabulator-cell { + background-color: transparent !important; +} + +div[role="row"] { + background-color: white; +} diff --git a/public/js/api/factory/classSchedule.js b/public/js/api/factory/classSchedule.js index 431b7ce8f..cd4ce6bbb 100644 --- a/public/js/api/factory/classSchedule.js +++ b/public/js/api/factory/classSchedule.js @@ -22,6 +22,12 @@ export default { url: "/api/frontend/v1/ClassScheduleApi/getAllClassTimeValidityPeriods", }; }, + getAllClassTimeValidityPeriodsPerOrganizationalUnit(organizationUnitId) { + return { + method: "get", + url: `/api/frontend/v1/ClassScheduleApi/getAllClassTimeValidityPeriodsPerOrganizationalUnit/${organizationUnitId}`, + }; + }, getClassTimeValidityPeriod(classTimeSlotValidityPeriodId) { return { method: "get", diff --git a/public/js/api/factory/studienplan.js b/public/js/api/factory/studienplan.js index 4dc358767..133a8123d 100644 --- a/public/js/api/factory/studienplan.js +++ b/public/js/api/factory/studienplan.js @@ -5,12 +5,27 @@ export default { url: "api/frontend/v1/organisation/studienplan/getAllStudyPlans", }; }, - getStudienplaeneBySemester(studiengang_kz, studiensemester_kurzbz, ausbildungssemester, orgform_kurzbz) - { - return { - method: 'get', - url: 'api/frontend/v1/organisation/studienplan/getBySemester', - params: { studiengang_kz, studiensemester_kurzbz, ausbildungssemester, orgform_kurzbz }, - }; - } -} \ No newline at end of file + getStudyPlansByOrganizationalUnitAndSemesterDates(organizationalUnitShortCode, startDate, endDate) { + return { + method: "get", + url: `api/frontend/v1/organisation/studienplan/getStudyPlansByOrganizationalUnitAndSemesterDates/${organizationalUnitShortCode}?filter[startDate]=${startDate}&filter[endDate]=${endDate}`, + }; + }, + getStudienplaeneBySemester( + studiengang_kz, + studiensemester_kurzbz, + ausbildungssemester, + orgform_kurzbz, + ) { + return { + method: "get", + url: "api/frontend/v1/organisation/studienplan/getBySemester", + params: { + studiengang_kz, + studiensemester_kurzbz, + ausbildungssemester, + orgform_kurzbz, + }, + }; + }, +}; diff --git a/public/js/api/factory/studiensemester.js b/public/js/api/factory/studiensemester.js index 7fe4a22d3..29a90a458 100644 --- a/public/js/api/factory/studiensemester.js +++ b/public/js/api/factory/studiensemester.js @@ -29,5 +29,17 @@ export default { url: 'api/frontend/v1/organisation/studiensemester/getAll', params: { order, start } }; - } + }, + getStudySemestersByOrganizationalUnitAndDates(organizationalUnitShortCode, startDate, endDate) { + return { + method: "get", + url: `api/frontend/v1/organisation/studiensemester/getStudySemestersByOrganizationalUnitAndDates/${organizationalUnitShortCode}?filter[startDate]=${startDate}&filter[endDate]=${endDate}`, + }; + }, + getStudySemestersByStudyPlanAndDates(studyPlanId, startDate, endDate) { + return { + method: "get", + url: `api/frontend/v1/organisation/studiensemester/getStudySemestersByStudyPlanAndDates?filter[studyPlanId]=${studyPlanId}&filter[startDate]=${startDate}&filter[endDate]=${endDate}`, + }; + }, }; \ No newline at end of file diff --git a/public/js/apps/lehre/ClassScheduleApp.js b/public/js/apps/lehre/ClassScheduleApp.js index c5ff33274..0dc8e403f 100644 --- a/public/js/apps/lehre/ClassScheduleApp.js +++ b/public/js/apps/lehre/ClassScheduleApp.js @@ -17,6 +17,7 @@ import ClassScheduleOverview from "../../components/ClassSchedule/ClassScheduleOverview.js"; import ClassScheduleValidityPeriodOverview from "../../components/ClassSchedule/ClassScheduleValidityPeriodOverview.js"; +import ClassScheduleOrgUnitGroupedValidityPeriodsOverview from "../../components/ClassSchedule/ClassScheduleOrgUnitGroupedValidityPeriodsOverview.js"; import FhcAlert from "../../plugins/FhcAlert.js"; import Phrasen from "../../plugins/Phrasen.js"; @@ -34,6 +35,18 @@ const router = VueRouter.createRouter({ path: `/${ciPath}/lehre/ClassSchedule`, component: ClassScheduleOverview, }, + { + name: "validityPeriodsOverviewByOrganizationUnit", + path: `/${ciPath}/lehre/ClassSchedule/validityPeriods/groupedOverviewByOrganizationUnit/:organizationalUnitShortCode`, + component: ClassScheduleOrgUnitGroupedValidityPeriodsOverview, + params: true, + }, + { + name: "validityPeriodsOverviewByOrganizationUnitAndStudyPlan", + path: `/${ciPath}/lehre/ClassSchedule/validityPeriods/groupedOverviewByOrganizationUnit/:organizationalUnitShortCode/studyPlan/:studyPlanId`, + component: ClassScheduleOrgUnitGroupedValidityPeriodsOverview, + params: true, + }, { name: "validityPeriodOverview", path: `/${ciPath}/lehre/ClassSchedule/validityPeriods/:classTimeSlotValidityPeriodId`, @@ -47,6 +60,7 @@ const app = Vue.createApp({ components: { ClassScheduleOverview, ClassScheduleValidityPeriodOverview, + ClassScheduleOrgUnitGroupedValidityPeriodsOverview, }, }); diff --git a/public/js/components/ClassSchedule/ClassScheduleCalendarSelector.js b/public/js/components/ClassSchedule/ClassScheduleCalendarSelector.js index 9bf7ebb2b..b436bae49 100644 --- a/public/js/components/ClassSchedule/ClassScheduleCalendarSelector.js +++ b/public/js/components/ClassSchedule/ClassScheduleCalendarSelector.js @@ -1,12 +1,8 @@ import draggable from "../../directives/draggable.js"; import drop from "../../directives/drop.js"; -import FormInput from "../Form/Input.js"; export default { name: "ClassScheduleCalendarSelector", - components: { - FormInput, - }, directives: { draggable, drop, @@ -30,91 +26,91 @@ export default { emits: ["overlaysChanged"], watch: { editedOverlays: { - handler(newVal) { - setTimeout(() => { - this.overlays = []; + async handler(newVal) { + await this.$nextTick(); - this.$refs.calendarContainer - .querySelectorAll("div[id^='overlay-']") - .forEach((element) => { - element.remove(); - }); - newVal - .map((slot) => { - return { - ...slot, - startingTimeSlot: this.timeSlotsInDay.find((timeSlot) => - timeSlot.startsWith(slot.startTime.substr(0, 5)), - ), - endingTimeSlot: this.timeSlotsInDay.find((timeSlot) => - timeSlot.endsWith(slot.endTime.substr(0, 5)), - ), - }; - }) - .forEach((overlay) => { - let firstElementDataNumber = - (overlay.weekday - 1) * this.timeSlotsInDay.length + - this.timeSlotsInDay.indexOf(overlay.startingTimeSlot); - let lastElementDataNumber = - (overlay.weekday - 1) * this.timeSlotsInDay.length + - this.timeSlotsInDay.indexOf(overlay.endingTimeSlot); - - let firstSelectedElement = - this.$refs.calendarSelectorContainer.querySelector( - "div[data-number='" + firstElementDataNumber + "']", - ); - let lastSelectedElement = - this.$refs.calendarSelectorContainer.querySelector( - "div[data-number='" + lastElementDataNumber + "']", - ); - if (!firstSelectedElement || !lastSelectedElement) { - this.$fhcAlert.alertError( - this.$p.t("ui", "classTimeSlotLoadingErrorMessage"), - ); - return; - } - - firstSelectedElement.style.backgroundColor = - this.selectedTimeSlotLabelColor; - lastSelectedElement.style.backgroundColor = - this.selectedTimeSlotLabelColor; - - this.currentFirstSelectedElementNumber = firstSelectedElement - ? parseInt(firstSelectedElement.getAttribute("data-number")) - : null; - - this.currentLastSelectedElementNumber = lastSelectedElement - ? parseInt(lastSelectedElement.getAttribute("data-number")) - : null; - - this.createOverlay(); - - this.$refs.calendarSelectorContainer - .querySelectorAll("div[class*='part-body']") - .forEach((child) => { - child.style.backgroundColor = this.defaultTimeSlotLabelColor; - }); - - this.currentFirstSelectedElementNumber = null; - this.currentLastSelectedElementNumber = null; - }); - - this.overlays = this.overlays.map((existingOverlay, index) => { - let existingOverlayInNewVal = newVal[index]; - - existingOverlay.databaseId = existingOverlayInNewVal.databaseId; - existingOverlay.type = existingOverlayInNewVal.type; - existingOverlay.hexColor = - this.classTimeSlotTypes.find( - (type) => - type.unterrichtszeitentyp_kurzbz === - existingOverlayInNewVal.type, - )?.hintergrundfarbe || null; - return { - ...existingOverlay, - }; + this.overlays = []; + await this.$nextTick(); + this.$refs.calendarContainer + .querySelectorAll("div[id^='overlay-']") + .forEach((element) => { + element.remove(); }); - }, 500); + newVal + .map((slot) => { + return { + ...slot, + startingTimeSlot: this.timeSlotsInDay.find((timeSlot) => + timeSlot.startsWith(slot.startTime.substr(0, 5)), + ), + endingTimeSlot: this.timeSlotsInDay.find((timeSlot) => + timeSlot.endsWith(slot.endTime.substr(0, 5)), + ), + }; + }) + .forEach((overlay) => { + let firstElementDataNumber = + (overlay.weekday - 1) * this.timeSlotsInDay.length + + this.timeSlotsInDay.indexOf(overlay.startingTimeSlot); + let lastElementDataNumber = + (overlay.weekday - 1) * this.timeSlotsInDay.length + + this.timeSlotsInDay.indexOf(overlay.endingTimeSlot); + + let firstSelectedElement = + this.$refs.calendarSelectorContainer.querySelector( + "div[data-number='" + firstElementDataNumber + "']", + ); + let lastSelectedElement = + this.$refs.calendarSelectorContainer.querySelector( + "div[data-number='" + lastElementDataNumber + "']", + ); + if (!firstSelectedElement || !lastSelectedElement) { + this.$fhcAlert.alertError( + this.$p.t("ui", "classTimeSlotLoadingErrorMessage"), + ); + return; + } + + firstSelectedElement.style.backgroundColor = + this.selectedTimeSlotLabelColor; + lastSelectedElement.style.backgroundColor = + this.selectedTimeSlotLabelColor; + + this.currentFirstSelectedElementNumber = firstSelectedElement + ? parseInt(firstSelectedElement.getAttribute("data-number")) + : null; + + this.currentLastSelectedElementNumber = lastSelectedElement + ? parseInt(lastSelectedElement.getAttribute("data-number")) + : null; + + this.createOverlay(); + + this.$refs.calendarSelectorContainer + .querySelectorAll("div[class*='part-body']") + .forEach((child) => { + child.style.backgroundColor = this.defaultTimeSlotLabelColor; + }); + + this.currentFirstSelectedElementNumber = null; + this.currentLastSelectedElementNumber = null; + }); + + this.overlays = this.overlays.map((existingOverlay, index) => { + let existingOverlayInNewVal = newVal[index]; + + existingOverlay.databaseId = existingOverlayInNewVal.databaseId; + existingOverlay.type = existingOverlayInNewVal.type; + existingOverlay.hexColor = + this.classTimeSlotTypes.find( + (type) => + type.unterrichtszeitentyp_kurzbz === + existingOverlayInNewVal.type, + )?.hintergrundfarbe || null; + return { + ...existingOverlay, + }; + }); }, deep: true, immediate: true, @@ -129,13 +125,13 @@ export default { data() { return { daysInWeek: [ - "Monday", - "Tuesday", - "Wednesday", - "Thursday", - "Friday", - "Saturday", - "Sunday", + this.$p.t("ui", "monday"), + this.$p.t("ui", "tuesday"), + this.$p.t("ui", "wednesday"), + this.$p.t("ui", "thursday"), + this.$p.t("ui", "friday"), + this.$p.t("ui", "saturday"), + this.$p.t("ui", "sunday"), ], timeSlotsInDay: [ "08:00-08:45", @@ -173,6 +169,8 @@ export default { x: null, y: null, }, + currentlyEditedOverlayId: null, + visiblePopover: null, }; }, computed: { @@ -209,15 +207,9 @@ export default { createOverlay() { let overlayElement; - if (!this.$props.isPreviewMode) { - overlayElement = this.$refs.calendarSelectorContainer.querySelector( - "#overlays-container", - ).children[0]; - } else { - overlayElement = this.$refs.calendarSelectorContainer.querySelector( - "#overlays-container-preview", - ).children[0]; - } + overlayElement = this.$refs.calendarSelectorContainer.querySelector( + "#overlays-container", + ).children[0]; let firstSelectedChild = this.$refs.calendarSelectorContainer.querySelector( @@ -709,11 +701,15 @@ export default { let dropzoneItem = event.target; if (!dropzoneItem) return; - if (!dropzoneItem.getAttribute("data-time")) { - let dropzoneOverlay = this.overlays.find((overlay) => overlay.id === dropzoneItem.id); + let dropzoneOverlay = this.overlays.find( + (overlay) => overlay.id === dropzoneItem.id, + ); if (!dropzoneOverlay) { - console.error("Could not find overlay for dropzone item with id " + dropzoneItem.id); + console.error( + "Could not find overlay for dropzone item with id " + + dropzoneItem.id, + ); return; } @@ -726,7 +722,7 @@ export default { let startElementNumber = dropzoneOverlay.startingTimeSlotElementNumber; let startElement = this.$refs.calendarSelectorContainer.querySelector( - `[data-number='${startElementNumber}']` + `[data-number='${startElementNumber}']`, ); // get mouse position @@ -742,9 +738,10 @@ export default { // 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 + "']", - ); + 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; @@ -756,7 +753,7 @@ export default { }); if (!closestPartBody) return; - dropzoneItem = closestPartBody; + dropzoneItem = closestPartBody; } let newStartTimeSlot = @@ -1351,6 +1348,101 @@ export default { overlay.endingTimeSlotElementNumber ); }, + getOverlayClassScheduleTypeTitle(overlayId) { + let overlay = this.overlays.find((overlay) => overlay.id === overlayId); + if (!overlay) return ""; + + let typeDescriptions = + this.classTimeSlotTypes.find( + (type) => type.unterrichtszeitentyp_kurzbz === overlay.type, + )?.bezeichnung_mehrsprachig || ""; + if (!typeDescriptions) return ""; + + return typeDescriptions[0].value || ""; + }, + handleChangeClassTimeSlotTypeForOverlay(newType) { + let classTimeSlotType = this.classTimeSlotTypes.find( + (type) => type.unterrichtszeitentyp_kurzbz === newType, + ); + if (!classTimeSlotType) { + console.error( + "Could not find class time slot type for newType: " + newType, + ); + return; + } + + this.overlays = this.overlays.map((overlay) => { + if (overlay.id === this.currentlyEditedOverlayId) { + return { + ...overlay, + type: classTimeSlotType.unterrichtszeitentyp_kurzbz, + hexColor: classTimeSlotType.hintergrundfarbe, + }; + } + return overlay; + }); + + this.currentlyEditedOverlayId = null; + }, + showOverlayClassTimeTypePopover(overlayId) { + let overlayElement = this.$refs.calendarSelectorContainer.querySelector( + `#${overlayId}`, + ); + if (!overlayElement) return; + + if (this.visiblePopover) { + this.visiblePopover.dispose(); + this.visiblePopover = null; + return; + } + this.visiblePopover = new bootstrap.Popover(overlayElement, { + title: this.$p.t("ui", "classTimeSlotType"), + html: true, + content: this.$refs.classScheduleTypeSelectorContainer.innerHTML, + placement: "left", + }); + this.visiblePopover.show(); + setTimeout(() => { + document + .querySelectorAll(".class-schedule-type-selector-option") + .forEach((option) => { + option.addEventListener("click", (event) => { + let selectedTypeDescription = event.currentTarget.innerText; + + let selectedType = this.classTimeSlotTypes.find((type) => + type.bezeichnung_mehrsprachig.some( + (desc) => desc.value === selectedTypeDescription, + ), + ); + if (!selectedType) { + console.error( + "Could not find class time slot type for selected description: " + + selectedTypeDescription, + ); + return; + } + + this.overlays = this.overlays.map((overlay) => { + if (overlay.id === overlayId) { + return { + ...overlay, + type: selectedType.unterrichtszeitentyp_kurzbz, + hexColor: selectedType.hintergrundfarbe, + }; + } + return overlay; + }); + + this.visiblePopover.dispose(); + this.visiblePopover = null; + }); + }); + }, 10); + }, + }, + unmounted() { + this.visiblePopover?.dispose(); + this.visiblePopover = null; }, template: /*html*/ `
@@ -1601,21 +1693,34 @@ export default {
-
+
+ + +

{{ getOverlayTimeSlotSpan("overlay-item-" + index) }}

-
- - - -
+ + {{ this.getOverlayClassScheduleTypeTitle('overlay-item-' + index) }} +
- -
+
+ -
- - {{ this.overlays.find(overlay => overlay.id === 'overlay-item-preview-' + index)?.type }} - -

{{ getOverlayTimeSlotSpan("overlay-item-preview-" + index) }}

-
+ {{ type.bezeichnung_mehrsprachig[0].value }} +
-
+
`, }; diff --git a/public/js/components/ClassSchedule/ClassScheduleOrgUnitGroupedValidityPeriodsOverview.js b/public/js/components/ClassSchedule/ClassScheduleOrgUnitGroupedValidityPeriodsOverview.js new file mode 100644 index 000000000..8601dc6dd --- /dev/null +++ b/public/js/components/ClassSchedule/ClassScheduleOrgUnitGroupedValidityPeriodsOverview.js @@ -0,0 +1,115 @@ +import ApiClassSchedule from "../../../js/api/factory/classSchedule.js"; +import ClassScheduleValidityPeriodPreview from "./ClassScheduleValidityPeriodPreview.js"; + +export default { + name: "ClassScheduleOrgUnitGroupedValidityPeriodsOverview", + components: { + ClassScheduleValidityPeriodPreview, + }, + data: () => { + return { + organizationalUnitShortCode: null, + studyPlanId: null, + classScheduleValidityPeriods: [], + classScheduleTimeSlots: [], + classTimeSlotTypes: [], + }; + }, + computed: {}, + methods: {}, + async created() { + this.organizationalUnitShortCode = this.$route.params.organizationalUnitShortCode; + this.studyPlanId = parseInt(this.$route.params.studyPlanId); + + let getAllClassTimeValidityPeriodsPerOrganizationalUnitResponse = + await this.$api.call( + ApiClassSchedule.getAllClassTimeValidityPeriodsPerOrganizationalUnit( + this.organizationalUnitShortCode, + ), + ); + if ( + getAllClassTimeValidityPeriodsPerOrganizationalUnitResponse.meta + .status === "success" + ) { + this.classScheduleValidityPeriods = + getAllClassTimeValidityPeriodsPerOrganizationalUnitResponse.data.filter( + (period) => { + if (!this.studyPlanId) return true; + + return period.studienplan_id === this.studyPlanId; + } + ); + } else { + console.error( + "Error fetching class time slot validity periods:", + getAllClassTimeValidityPeriodsPerOrganizationalUnitResponse.meta + .message, + ); + } + + for (let validityPeriod of this.classScheduleValidityPeriods) { + if (this.studyPlanId && validityPeriod.studienplan_id !== this.studyPlanId) { + continue; + } + + let getClassTimeSlotsForValidityPeriodResponse = await this.$api.call( + ApiClassSchedule.getClassTimeSlotsForValidityPeriod( + validityPeriod.unterrichtszeitengueltigkeit_id, + ), + ); + if ( + getClassTimeSlotsForValidityPeriodResponse.meta.status === "success" + ) { + this.classScheduleTimeSlots = [ + ...this.classScheduleTimeSlots, + ...getClassTimeSlotsForValidityPeriodResponse.data, + ]; + } else { + console.error( + `Error fetching class time slots for validity period ${validityPeriod.unterrichtszeitengueltigkeit_id}:`, + getClassTimeSlotsForValidityPeriodResponse.meta.message, + ); + } + } + + let getAllClassTimeSlotTypesResponse = await this.$api.call( + ApiClassSchedule.getAllClassScheduleTypes("filter[aktiv]=true"), + ); + if (getAllClassTimeSlotTypesResponse.meta.status === "success") { + this.classTimeSlotTypes = getAllClassTimeSlotTypesResponse.data.map( + (type) => { + let descriptions = []; + for (let item of type.bezeichnung_mehrsprachig) { + let [lang, value] = item.split(":"); + descriptions.push({ lang, value }); + } + return { + ...type, + bezeichnung_mehrsprachig: descriptions, + }; + }, + ); + } else { + console.error( + "Error fetching class time slot types:", + getAllClassTimeSlotTypesResponse.meta.message, + ); + } + }, + template: /* html */ ` +
+
+

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

+

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

+
+ +
+ `, +}; diff --git a/public/js/components/ClassSchedule/ClassScheduleOverview.js b/public/js/components/ClassSchedule/ClassScheduleOverview.js index 5628aed20..02fbdfee8 100644 --- a/public/js/components/ClassSchedule/ClassScheduleOverview.js +++ b/public/js/components/ClassSchedule/ClassScheduleOverview.js @@ -38,34 +38,48 @@ export default { tabulatorOptions() { const options = { ajaxURL: "dummy", - ajaxRequestFunc: () => - this.$api.call(ApiClassSchedule.getAllClassTimeValidityPeriods()), - ajaxResponse: (url, params, response) => response.data, + ajaxRequestFunc: async () => + await this.getParsedClassTimeSlotValidityPeriodData(), + ajaxResponse: (url, params, response) => response, persistenceID: "core_class_schedule_validity_periods", selectableRows: true, columns: [ { - title: "gueltig von", - field: "gueltig_von", + title: this.$p.t("ui", "zeitraum"), + formatter: (cell, formatterParams, onRendered) => { + const data = cell.getData(); + const validFrom = new Date(data.gueltig_von).toLocaleDateString( + "de-AT", + { + day: "2-digit", + month: "2-digit", + year: "numeric", + }, + ); + data.gueltig_von; + const validTo = new Date(data.gueltig_bis).toLocaleDateString( + "de-AT", + { + day: "2-digit", + month: "2-digit", + year: "numeric", + }, + ); + return `${validFrom ? validFrom : "?"} - ${validTo ? validTo : "?"}`; + }, + }, + { + title: this.$p.t("global", "typ"), + field: "unterrichtszeiten_typ_bezeichnung_mehrsprachig", width: 150, }, - { title: "gueltig bis", field: "gueltig_bis", width: 150 }, - { title: "orgform kurzbz", field: "orgform_kurzbz", width: 150 }, { - title: "ausbildungssemester", + title: this.$p.t("lehre", "sem"), field: "ausbildungssemester", width: 150, }, { - title: "typ", - field: "unterrichtszeitentyp_kurzbz", - }, - { - field: "unterrichtszeitengueltigkeit_id", - visible: false, - }, - { - title: "Aktionen", + title: this.$p.t("global", "actions"), field: "actions", minWidth: 150, maxWidth: 150, @@ -135,38 +149,102 @@ export default { frozen: true, }, ], + groupBy: [ + "organisationseinheit_bezeichnung_extended", + "studienplan_bezeichnung", + ], + groupHeader: [ + function (value, count, data) { + let container = document.createElement("span"); + + container.className = + "d-flex align-items-center justify-content-between"; + container.style.display = "inline-block"; + container.style.width = "100%"; + + let label = document.createElement("span"); + label.textContent = + value + " (" + count + " item" + (count > 1 ? "s" : "") + ")"; + container.append(label); + + let button = document.createElement("button"); + button.className = + "btn btn-sm btn-outline-secondary fhc-btn-for-org-unit-grouping"; + button.style.marginLeft = "10px"; + button.innerHTML = ''; + + button.dataset.organizationalUnitShortCode = data[0].oe_kurzbz; + + container.append(button); + return container; + }, + function (value, count, data) { + let container = document.createElement("span"); + + container.className = + "d-flex align-items-center justify-content-between"; + container.style.display = "inline-block"; + container.style.width = "100%"; + + let label = document.createElement("span"); + label.textContent = + value + " (" + count + " item" + (count > 1 ? "s" : "") + ")"; + container.append(label); + + let button = document.createElement("button"); + button.className = + "btn btn-sm btn-outline-secondary fhc-btn-for-org-unit-and-study-plan-grouping"; + button.style.marginLeft = "10px"; + button.innerHTML = ''; + button.title = 22; + + button.dataset.organizationalUnitShortCode = data[0].oe_kurzbz; + button.dataset.studyPlanId = data[0].studienplan_id; + + container.append(button); + return container; + }, + ], }; return options; }, tabulatorEvents() { const events = [ { - event: "tableBuilt", + event: "renderComplete", handler: async () => { - const setHeader = (field, text) => { - const col = - this.$refs.classTimeSlotValidityPeriodsTable.tabulator.getColumn( - field, - ); - if (!col) return; + document + .querySelectorAll(".fhc-btn-for-org-unit-grouping") + .forEach((button) => { + button.addEventListener("click", (e) => { + let organizationalUnitShortCode = + button.dataset.organizationalUnitShortCode; - const el = col.getElement(); - if (!el || !el.querySelector) return; + this.$router.push({ + name: "validityPeriodsOverviewByOrganizationUnit", + params: { + organizationalUnitShortCode, + }, + }); + }); + }); - const titleEl = el.querySelector(".tabulator-col-title"); - if (titleEl) { - titleEl.textContent = text; - } - }; - - setHeader("nummer", this.$p.t("wawi", "nummer")); - setHeader("anmerkung", this.$p.t("global", "anmerkung")); - setHeader("retouram", this.$p.t("wawi", "retourdatum")); - setHeader("beschreibung", this.$p.t("global", "beschreibung")); - setHeader("kaution", this.$p.t("infocenter", "kaution")); - setHeader("ausgegebenam", this.$p.t("wawi", "ausgabedatum")); - setHeader("person_id", this.$p.t("person", "person_id")); - setHeader("uid", this.$p.t("person", "uid")); + document + .querySelectorAll(".fhc-btn-for-org-unit-and-study-plan-grouping") + .forEach((button) => { + button.addEventListener("click", (e) => { + let organizationalUnitShortCode = + button.dataset.organizationalUnitShortCode; + let studyPlanId = button.dataset.studyPlanId; + this.$router.push({ + name: "validityPeriodsOverviewByOrganizationUnitAndStudyPlan", + params: { + organizationalUnitShortCode, + studyPlanId, + }, + }); + }); + }); }, }, ]; @@ -174,6 +252,41 @@ export default { }, }, methods: { + test() { + alert("test"); + }, + async getParsedClassTimeSlotValidityPeriodData() { + let getAllClassTimeValidityPeriodsResponse = await this.$api.call( + ApiClassSchedule.getAllClassTimeValidityPeriods(), + ); + + if (getAllClassTimeValidityPeriodsResponse.meta.status === "success") { + let generalWord = this.$p.t("ui", "general"); + return getAllClassTimeValidityPeriodsResponse.data.map( + function (period) { + period.organisationseinheit_bezeichnung_extended = + period.organisationseinheit_bezeichnung + + " - " + + period.organisationseinheit_organisationseinheittyp_kurzbz; + if (!period.studienplan_bezeichnung) { + period.studienplan_bezeichnung = generalWord; + } + period.unterrichtszeiten_typ_bezeichnung_mehrsprachig = + period.unterrichtszeiten_typ_bezeichnung_mehrsprachig[0]?.split( + ":", + )[1]; + return { + ...period, + }; + }, + ); + } else { + console.error( + "Error fetching class time slot validity periods:", + getAllClassTimeValidityPeriodsResponse.meta.message, + ); + } + }, showClassTimeSlotValidityPeriodModal() { this.isClassTimeSlotValidityPeriodModalVisible = true; }, @@ -227,12 +340,12 @@ export default { }, mounted() { this.$p - .loadCategory(["global", "lehre", "ui", "gruppenmanagement"]) + .loadCategory(["global", "lehre", "ui", "gruppenmanagement", "core"]) .then(() => { this.phrasesLoaded = true; }); }, - template: /* html */` + template: /* html */ `

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

@@ -265,7 +378,7 @@ export default { table-only :side-menu="false" :tabulator-options="tabulatorOptions" - :tabulator-events="[{ event: 'cellEdited'}]" + :tabulator-events="tabulatorEvents" :new-btn-label="$p.t('ui', 'addClassTimeSlotValidityPeriodButton')" new-btn-show reload diff --git a/public/js/components/ClassSchedule/ClassScheduleTypeModal.js b/public/js/components/ClassSchedule/ClassScheduleTypeModal.js index 15cb83c00..6e867abcb 100644 --- a/public/js/components/ClassSchedule/ClassScheduleTypeModal.js +++ b/public/js/components/ClassSchedule/ClassScheduleTypeModal.js @@ -21,6 +21,7 @@ export default { required: true, }, }, + emits: ["hideBsModal", "classTimeSlotTypeCreated", "classTimeSlotTypeUpdated"], watch: { isVisible(newValue) { if (newValue) { diff --git a/public/js/components/ClassSchedule/ClassScheduleValidityPeriodModal.js b/public/js/components/ClassSchedule/ClassScheduleValidityPeriodModal.js index b050a4385..cb38231f3 100644 --- a/public/js/components/ClassSchedule/ClassScheduleValidityPeriodModal.js +++ b/public/js/components/ClassSchedule/ClassScheduleValidityPeriodModal.js @@ -1,6 +1,8 @@ import ApiClassSchedule from "../../../js/api/factory/classSchedule.js"; import ApiStudienPlan from "../../../js/api/factory/studienplan.js"; +import ApiStudienSemester from "../../../js/api/factory/studiensemester.js"; import ApiOrganizationalUnit from "../../../js/api/factory/organizationalUnit.js"; +import { formatDate } from "../../helpers/DateHelpers.js"; import BsModal from "../Bootstrap/Modal.js"; import CoreForm from "../Form/Form.js"; @@ -66,6 +68,35 @@ export default { this.$fhcAlert.handleSystemError(error); }); }, + async "classTimeSlotValidityPeriodFormData.organizationalUnitShortCode"( + newValue, + ) { + if (!newValue) { + this.classTimeSlotValidityPeriodFormData.studyPlanId = null; + return; + } + + this.refetchFilterableOptions(); + }, + async "classTimeSlotValidityPeriodFormData.validityPeriodFrom"(newValue) { + if (!newValue) { + this.classTimeSlotValidityPeriodFormData.studyPlanId = null; + return; + } + + this.refetchFilterableOptions(); + }, + async "classTimeSlotValidityPeriodFormData.validityPeriodTo"(newValue) { + if (!newValue) { + this.classTimeSlotValidityPeriodFormData.studyPlanId = null; + return; + } + + this.refetchFilterableOptions(); + }, + async "classTimeSlotValidityPeriodFormData.studyPlanId"(newValue) { + this.refetchFilterableOptions(); + }, }, data: () => { return { @@ -73,6 +104,8 @@ export default { isEditInProgress: false, organizationalUnits: [], studyPlans: [], + studySemesters: [], + studySemestersByNumber: [], classTimeSlotTypes: [], classTimeSlotValidityPeriodFormData: { id: null, @@ -86,7 +119,157 @@ export default { }, }; }, + computed: { + isStudyPlanSelectDisabled() { + return ( + !this.classTimeSlotValidityPeriodFormData.organizationalUnitShortCode || + !this.classTimeSlotValidityPeriodFormData.validityPeriodFrom || + !this.classTimeSlotValidityPeriodFormData.validityPeriodTo + ); + }, + formattedValidityPeriodFrom() { + if (!this.classTimeSlotValidityPeriodFormData.validityPeriodFrom) + return null; + + return formatDate( + this.classTimeSlotValidityPeriodFormData.validityPeriodFrom, + ); + }, + formattedValidityPeriodTo() { + if (!this.classTimeSlotValidityPeriodFormData.validityPeriodTo) + return null; + return formatDate( + this.classTimeSlotValidityPeriodFormData.validityPeriodTo, + ); + }, + }, methods: { + async refetchFilterableOptions() { + this.studyPlans = await this.getTargetedStudyPlans( + this.classTimeSlotValidityPeriodFormData.organizationalUnitShortCode, + this.formattedValidityPeriodFrom, + this.formattedValidityPeriodTo, + ); + + let studySemestersByDates = + await this.getStudySemestersByOrganizationalUnitAndDates( + this.classTimeSlotValidityPeriodFormData.organizationalUnitShortCode, + this.formattedValidityPeriodFrom, + this.formattedValidityPeriodTo, + ); + + let studySemesters = new Array( + ...new Set( + studySemestersByDates + .map((s) => s.semester_numbers) + .flat() + .sort((a, b) => a - b), + ), + ); + + this.studySemestersByNumber = studySemesters; + if (this.classTimeSlotValidityPeriodFormData.studyPlanId) { + let studySemestersByStudyProgramId = + await this.getStudySemestersByStudyPlanAndDates( + this.classTimeSlotValidityPeriodFormData.studyPlanId, + this.formattedValidityPeriodFrom, + this.formattedValidityPeriodTo, + ); + studySemestersByStudyProgramId = new Array( + ...new Set( + studySemestersByStudyProgramId + .map((s) => s.semester_numbers) + .flat() + .sort((a, b) => a - b), + ), + ); + + studySemesters = studySemestersByStudyProgramId; + } + + this.studySemestersByNumber = studySemesters; + }, + async getTargetedStudyPlans( + organizationalUnitShortCode, + validityPeriodFrom, + validityPeriodTo, + ) { + if ( + !organizationalUnitShortCode || + !validityPeriodFrom || + !validityPeriodTo + ) { + return []; + } + + let getAllStudyPlansResponse = await this.$api.call( + ApiStudienPlan.getStudyPlansByOrganizationalUnitAndSemesterDates( + organizationalUnitShortCode, + validityPeriodFrom, + validityPeriodTo, + ), + ); + if (getAllStudyPlansResponse.meta.status !== "success") { + console.error( + "Error fetching study plans:", + getAllStudyPlansResponse.meta.message, + ); + } + + return getAllStudyPlansResponse.data?.length + ? getAllStudyPlansResponse.data + : []; + }, + async getStudySemestersByOrganizationalUnitAndDates( + organizationalUnitShortCode, + validityPeriodFrom, + validityPeriodTo, + ) { + if ( + !organizationalUnitShortCode || + !validityPeriodFrom || + !validityPeriodTo + ) { + return []; + } + + let getStudySemestersResponse = await this.$api.call( + ApiStudienSemester.getStudySemestersByOrganizationalUnitAndDates( + organizationalUnitShortCode, + validityPeriodFrom, + validityPeriodTo, + ), + ); + if (getStudySemestersResponse.meta.status !== "success") { + console.error( + "Error fetching study semesters by organizational unit and dates:", + getStudySemestersResponse.meta.message, + ); + } + + return getStudySemestersResponse.data?.length + ? getStudySemestersResponse.data + : []; + }, + async getStudySemestersByStudyPlanAndDates(studyProgramId, validityPeriodFrom, validityPeriodTo) { + if (!studyProgramId || !validityPeriodFrom || !validityPeriodTo) { + return []; + } + + let getStudySemestersResponse = await this.$api.call( + ApiStudienSemester.getStudySemestersByStudyPlanAndDates(studyProgramId, validityPeriodFrom, validityPeriodTo), + ); + if (getStudySemestersResponse.meta.status !== "success") { + console.error( + "Error fetching study semesters:", + getStudySemestersResponse.meta.message, + ); + } + + return getStudySemestersResponse.data?.length + ? getStudySemestersResponse.data + : []; + }, createClassTimeSlotValidityPeriod() { return this.$refs.classTimeSlotValidityPeriodData .call( @@ -153,7 +336,9 @@ export default { ApiOrganizationalUnit.getAllOrganizationalUnits(), ); if (getAllOrganizationalUnitsResponse.meta.status === "success") { - this.organizationalUnits = getAllOrganizationalUnitsResponse.data; + this.organizationalUnits = getAllOrganizationalUnitsResponse.data.sort( + (a, b) => a.bezeichnung.localeCompare(b.bezeichnung), + ); } else { console.error( "Error fetching organizational units:", @@ -197,7 +382,7 @@ export default { ); } }, - template: ` + template: /*html*/ `