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*/ `
{{ getOverlayTimeSlotSpan("overlay-item-" + index) }}
-{{ getOverlayTimeSlotSpan("overlay-item-preview-" + index) }}
-{{$p.t('ui', 'addClassTimeSlotValidityPeriodModalTitle')}}
@@ -207,33 +392,82 @@ export default {{{ $p.t("lehre", "ausbildungssemester") }}: {{ classTimeSlotValidityPeriod?.ausbildungssemester }}