diff --git a/application/config/routes.php b/application/config/routes.php index bb0b0f65e..7a42e6c69 100644 --- a/application/config/routes.php +++ b/application/config/routes.php @@ -140,6 +140,8 @@ $route['api/frontend/v1/treemenudata/lvverwaltung/stdsem/(:any)/emp/(:any)/stg/( // load routes from extensions, also look for environment-specific configs $subdirs = ['application/config/extensions', 'application/config/' . ENVIRONMENT . '/extensions']; +$route['api/frontend/v1/tempus/Tags/getTagsByCalendar/(:num)'] = 'api/frontend/v1/tempus/Tags/getTagsByCalendar/$1'; + foreach($subdirs as $subdir) { if(is_dir($subdir)) diff --git a/application/config/tempus.php b/application/config/tempus.php index e3e18b954..833f5655f 100644 --- a/application/config/tempus.php +++ b/application/config/tempus.php @@ -2,4 +2,11 @@ $config['send_update_mails'] = false; $config['calendar_start'] = 7; -$config['calendar_end'] = 23; \ No newline at end of file +$config['calendar_end'] = 23; + + +$config['tempus_tags'] = [ + 'prioone' => ['readonly' => false], + 'priotwo' => ['readonly' => true], + 'hinweis' => ['readonly' => false], +]; \ No newline at end of file diff --git a/application/controllers/api/frontend/v1/tempus/Tags.php b/application/controllers/api/frontend/v1/tempus/Tags.php new file mode 100644 index 000000000..c5223cd9a --- /dev/null +++ b/application/controllers/api/frontend/v1/tempus/Tags.php @@ -0,0 +1,53 @@ + self::BERECHTIGUNG_KURZBZ, + 'getTags' => self::BERECHTIGUNG_KURZBZ, + 'getTagsByCalendar' => self::BERECHTIGUNG_KURZBZ, + 'addTag' => self::BERECHTIGUNG_KURZBZ, + 'updateTag' => self::BERECHTIGUNG_KURZBZ, + 'doneTag' => self::BERECHTIGUNG_KURZBZ, + 'deleteTag' => self::BERECHTIGUNG_KURZBZ + ]); + + $this->config->load('tempus'); + } + + public function getTag($readonly_tags = null) + { + parent::getTag($this->config->item('tempus_tags')); + } + public function getTags($tags = null) + { + parent::getTags($this->config->item('tempus_tags')); + } + public function getTagsByCalendar($eindeutige_kalender_gruppen_id = null) + { + parent::getTagsByAssignmentTypeValue('eindeutige_kalender_gruppen_id', $eindeutige_kalender_gruppen_id, $this->config->item('tempus_tags')); + } + public function addTag($withZuordnung = true, $updatable_tags = null) + { + parent::addTag(true, $this->config->item('tempus_tags')); + } + public function updateTag($updatable_tags = null) + { + parent::updateTag($this->config->item('tempus_tags')); + } + public function deleteTag($withZuordnung = true, $updatable_tags = null) + { + parent::deleteTag(true, $this->config->item('tempus_tags')); + } + public function doneTag($updatable_tags = null) + { + parent::doneTag($this->config->item('tempus_tags')); + } +} diff --git a/application/core/Tag_Controller.php b/application/core/Tag_Controller.php index c4b6edb81..3659dfbff 100644 --- a/application/core/Tag_Controller.php +++ b/application/core/Tag_Controller.php @@ -44,7 +44,6 @@ class Tag_Controller extends FHCAPI_Controller { $language = $this->_getLanguageIndex(); $index_bezeichnung_mehrsprachig = $language - 1; - $id = $this->input->get('id'); if (is_array($readonly_tags) && !isEmptyArray($readonly_tags)) @@ -123,7 +122,36 @@ class Tag_Controller extends FHCAPI_Controller $notiztypen = $this->NotiztypModel->loadWhere(array('aktiv' => true)); $this->terminateWithSuccess(hasData($notiztypen) ? getData($notiztypen) : array()); } + public function getTagsByAssignmentTypeValue($zuordnung_typ, $zuordnung_id, $tags = null) + { + $language = $this->_getLanguageIndex(); + $index_bezeichnung_mehrsprachig = $language - 1; + $this->NotizzuordnungModel->addSelect( + 'tbl_notizzuordnung.notiz_id as notiz_id, + typ_kurzbz as tag_typ_kurzbz, + array_to_json(bezeichnung_mehrsprachig::varchar[])->>'. $index_bezeichnung_mehrsprachig. ' as bezeichnung, + style, + beschreibung, + tag, + tbl_notiz.erledigt as done + ' + ); + + if (is_array($tags) && !isEmptyArray($tags)) + { + $tags = $this->_filterTag($tags, false); + $this->NotizzuordnungModel->db->where_in('tbl_notiz_typ.typ_kurzbz', $tags); + } + + $this->NotizzuordnungModel->addJoin('public.tbl_notiz', 'public.tbl_notizzuordnung.notiz_id = public.tbl_notiz.notiz_id'); + $this->NotizzuordnungModel->addJoin('public.tbl_notiz_typ', 'public.tbl_notiz.typ = public.tbl_notiz_typ.typ_kurzbz'); + + $this->NotizzuordnungModel->addOrder('prioritaet'); + + $notiztypen = $this->NotizzuordnungModel->loadWhere(array('aktiv' => true, $zuordnung_typ => $zuordnung_id)); + $this->terminateWithSuccess(hasData($notiztypen) ? getData($notiztypen) : array()); + } public function addTag($withZuordnung = true, $updatable_tags = null) { $postData = $this->getPostJson(); @@ -143,7 +171,7 @@ class Tag_Controller extends FHCAPI_Controller if (!in_array($postData->tag_typ_kurzbz, $tags)) $this->terminateWithError($this->p->t('ui', 'keineBerechtigung')); } - + if ($withZuordnung) { $return = array(); @@ -152,7 +180,7 @@ class Tag_Controller extends FHCAPI_Controller $this->terminateWithError('Error occurred', self::ERROR_TYPE_GENERAL); $values = array_unique($postData->values); - + foreach ($values as $value) { $insertResult = $this->addNotiz($postData); diff --git a/application/libraries/KalenderLib.php b/application/libraries/KalenderLib.php index 6c9cc634e..623784c9d 100644 --- a/application/libraries/KalenderLib.php +++ b/application/libraries/KalenderLib.php @@ -23,6 +23,7 @@ class KalenderLib $this->_ci->load->model('ressource/Stunde_model', 'StundeModel'); $this->_ci->load->model('organisation/Studiensemester_model', 'StudiensemesterModel'); $this->_ci->load->model('system/Variable_model', 'VariableModel'); + $this->_ci->load->model('system/Sprache_model', 'SpracheModel'); $this->_ci->load->model('organisation/Ferien_model', 'FerienModel'); @@ -35,69 +36,75 @@ class KalenderLib private function _getBasePlan($start_date, $end_date) { + $language = $this->getLanguageIndex(); + $index_bezeichnung_mehrsprachig = $language - 1; + $end_date = date('Y-m-d', strtotime($end_date . ' +1 day')); $this->_ci->KalenderModel->addSelect('tbl_kalender.kalender_id, - tbl_kalender.eindeutige_kalender_gruppen_id, - tbl_kalender.status_kurzbz, - tbl_kalender.typ, - tbl_kalender.von, - tbl_kalender.bis, - tbl_kalender_ort.location, - tbl_kalender_ort.ort_kurzbz, - tbl_lehreinheit.lehreinheit_id, - tbl_lehreinheit.lehrveranstaltung_id, - tbl_lehreinheit.lehrfach_id, - tbl_lehreinheit.lehrform_kurzbz, - tbl_lehrveranstaltung.oe_kurzbz, - lehrfach.kurzbz AS lehrfach_kurzbz, - lehrfach.bezeichnung AS lehrfach_bezeichnung, - lehrfach.farbe, - COALESCE(orginasator.uid, tbl_lehreinheitmitarbeiter.mitarbeiter_uid) as mitarbeiter_uid, - COALESCE(reservierung_person.vorname, tbl_person.vorname) as vorname, - COALESCE(reservierung_person.nachname, tbl_person.nachname) as nachname, - COALESCE(reservierung_ma.kurzbz, tbl_mitarbeiter.kurzbz) AS ma_kurzbz, - teilnehmergruppe.gruppe_kurzbz as teilnehmerg_grp, - COALESCE ( - UPPER(tbl_studiengang.typ || tbl_studiengang.kurzbz) || - COALESCE(verbandgruppe.semester::varchar, \'\') || - COALESCE(verbandgruppe.verband::varchar, \'\') || - COALESCE(verbandgruppe.gruppe, \'\'), - \'\') as verband_grp, - tbl_kalender_event.beschreibung, - tbl_kalender_event.titel, - teilmitarbeiter.kurzbz as teilnehmer_kurzbz, - teilperson.vorname as teilnehmer_vorname, - teilperson.nachname as teilnehmer_nachname, - teilbenutzer.uid as teilnehmer_uid, - CASE - WHEN tbl_lehreinheitgruppe.gruppe_kurzbz IS NULL THEN - COALESCE( - UPPER(le_studiengang.typ || le_studiengang.kurzbz) || - COALESCE(tbl_lehreinheitgruppe.semester::varchar, \'\') || - COALESCE(tbl_lehreinheitgruppe.verband::varchar, \'\') || - COALESCE(tbl_lehreinheitgruppe.gruppe, \'\'), - \'\') - ELSE tbl_lehreinheitgruppe.gruppe_kurzbz - END AS lehreinheit_gruppe_bezeichnung, - tbl_lehreinheitgruppe.gruppe_kurzbz as le_gruppe_kurzbz, - tbl_lehreinheitgruppe.studiengang_kz as le_studiengang_kz, - tbl_lehreinheitgruppe.semester as le_semester, - tbl_lehreinheitgruppe.verband as le_verband, - tbl_lehreinheitgruppe.gruppe as le_gruppe, - le_gruppe.direktinskription as le_direktinskription, - CASE - WHEN vonstunde.stunde IS NOT NULL AND bisstunde.stunde IS NOT NULL - THEN bisstunde.stunde - vonstunde.stunde + 1 - ELSE ROUND( - COALESCE( - ( - SELECT SUM(EXTRACT(EPOCH FROM (LEAST(stunde.ende, tbl_kalender.bis::time) - GREATEST(stunde.beginn, tbl_kalender.von::time))) / 60) / 45 - FROM lehre.tbl_stunde stunde - WHERE stunde.beginn < tbl_kalender.bis::time AND stunde.ende > tbl_kalender.von::time) - ,0) - ,2 - ) END AS verplante_stunden'); + tbl_kalender.eindeutige_kalender_gruppen_id, + tbl_kalender.status_kurzbz, + tbl_kalender.typ, + tbl_kalender.von, + tbl_kalender.bis, + tbl_kalender_ort.location, + tbl_kalender_ort.ort_kurzbz, + tbl_lehreinheit.lehreinheit_id, + tbl_lehreinheit.lehrveranstaltung_id, + tbl_lehreinheit.lehrfach_id, + tbl_lehreinheit.lehrform_kurzbz, + tbl_lehrveranstaltung.oe_kurzbz, + lehrfach.kurzbz AS lehrfach_kurzbz, + lehrfach.bezeichnung AS lehrfach_bezeichnung, + lehrfach.farbe, + COALESCE(orginasator.uid, tbl_lehreinheitmitarbeiter.mitarbeiter_uid) as mitarbeiter_uid, + COALESCE(reservierung_person.vorname, tbl_person.vorname) as vorname, + COALESCE(reservierung_person.nachname, tbl_person.nachname) as nachname, + COALESCE(reservierung_ma.kurzbz, tbl_mitarbeiter.kurzbz) AS ma_kurzbz, + teilnehmergruppe.gruppe_kurzbz as teilnehmerg_grp, + COALESCE ( + UPPER(tbl_studiengang.typ || tbl_studiengang.kurzbz) || + COALESCE(verbandgruppe.semester::varchar, \'\') || + COALESCE(verbandgruppe.verband::varchar, \'\') || + COALESCE(verbandgruppe.gruppe, \'\'), + \'\') as verband_grp, + tbl_kalender_event.beschreibung, + tbl_kalender_event.titel, + teilmitarbeiter.kurzbz as teilnehmer_kurzbz, + teilperson.vorname as teilnehmer_vorname, + teilperson.nachname as teilnehmer_nachname, + teilbenutzer.uid as teilnehmer_uid, + CASE + WHEN tbl_lehreinheitgruppe.gruppe_kurzbz IS NULL THEN + COALESCE( + UPPER(le_studiengang.typ || le_studiengang.kurzbz) || + COALESCE(tbl_lehreinheitgruppe.semester::varchar, \'\') || + COALESCE(tbl_lehreinheitgruppe.verband::varchar, \'\') || + COALESCE(tbl_lehreinheitgruppe.gruppe, \'\'), + \'\') + ELSE tbl_lehreinheitgruppe.gruppe_kurzbz + END AS lehreinheit_gruppe_bezeichnung, + tbl_lehreinheitgruppe.gruppe_kurzbz as le_gruppe_kurzbz, + tbl_lehreinheitgruppe.studiengang_kz as le_studiengang_kz, + tbl_lehreinheitgruppe.semester as le_semester, + tbl_lehreinheitgruppe.verband as le_verband, + tbl_lehreinheitgruppe.gruppe as le_gruppe, + le_gruppe.direktinskription as le_direktinskription, + tag_data_agg.tags as tags, + resource_data_agg.resources as resources, + CASE + WHEN vonstunde.stunde IS NOT NULL AND bisstunde.stunde IS NOT NULL + THEN bisstunde.stunde - vonstunde.stunde + 1 + ELSE ROUND( + COALESCE( + ( + SELECT SUM(EXTRACT(EPOCH FROM (LEAST(stunde.ende, tbl_kalender.bis::time) - GREATEST(stunde.beginn, tbl_kalender.von::time))) / 60) / 45 + FROM lehre.tbl_stunde stunde + WHERE stunde.beginn < tbl_kalender.bis::time AND stunde.ende > tbl_kalender.von::time) + ,0) + ,2 + ) END AS verplante_stunden + '); $this->_ci->KalenderModel->addJoin('lehre.tbl_kalender_lehreinheit', 'tbl_kalender.kalender_id = tbl_kalender_lehreinheit.kalender_id', 'LEFT'); $this->_ci->KalenderModel->addJoin('lehre.tbl_kalender_event', 'tbl_kalender.kalender_id = tbl_kalender_event.kalender_id', 'LEFT'); @@ -143,6 +150,69 @@ class KalenderLib ); $this->_ci->KalenderModel->addJoin('public.tbl_studiengang le_studiengang', 'le_lehrverband.studiengang_kz = le_studiengang.studiengang_kz', 'LEFT'); + // Include tags in the query + $this->_ci->load->config('tempus'); + $tags = $this->_ci->config->item('tempus_tags'); + + $whereTags = ''; + if (is_array($tags) && !isEmptyArray($tags)) { + $tags = array_keys($tags); + + foreach ($tags as $key => $tag) { + $tags[$key] = $this->_ci->KalenderModel->escape($tag); + } + $whereTags = " AND nt.typ_kurzbz IN (" . implode(",", $tags) . ")"; + } + $subQueryTag = "( + SELECT + tag.eindeutige_kalender_gruppen_id, + COALESCE(json_agg(tag ORDER BY tag.done, tag.prioritaet), '[]'::json) AS tags + FROM ( + SELECT DISTINCT ON (n.notiz_id) + n.notiz_id AS id, + nt.typ_kurzbz, + array_to_json(nt.bezeichnung_mehrsprachig::varchar[])->>". $index_bezeichnung_mehrsprachig. " as beschreibung, + n.text AS notiz, + nt.style, + n.erledigt AS done, + nt.prioritaet AS prioritaet, + nz.eindeutige_kalender_gruppen_id + FROM public.tbl_notizzuordnung AS nz + JOIN public.tbl_notiz AS n ON nz.notiz_id = n.notiz_id + JOIN public.tbl_notiz_typ AS nt ON n.typ = nt.typ_kurzbz " . $whereTags . " + ) AS tag + GROUP BY tag.eindeutige_kalender_gruppen_id + ) AS tag_data_agg"; + + $this->_ci->KalenderModel->addJoin($subQueryTag, 'tag_data_agg.eindeutige_kalender_gruppen_id = tbl_kalender.eindeutige_kalender_gruppen_id', 'LEFT'); + // End of tags inclusion + + // Include assigned operational resources in the query + $subQueryResource = "( + SELECT + resource.eindeutige_kalender_gruppen_id, + COALESCE(json_agg(resource ORDER BY resource.beschreibung), '[]'::json) AS resources + FROM ( + SELECT + betriebsmittel_kalender.*, + betriebsmittel.beschreibung, + betriebsmittel.verplanen + FROM lehre.tbl_betriebsmittel_kalender AS betriebsmittel_kalender + JOIN wawi.tbl_betriebsmittel AS betriebsmittel + ON betriebsmittel.betriebsmittel_id = betriebsmittel_kalender.betriebsmittel_id + WHERE betriebsmittel.verplanen = TRUE + ) AS resource + GROUP BY resource.eindeutige_kalender_gruppen_id + ) AS resource_data_agg"; + + $this->_ci->KalenderModel->addJoin( + $subQueryResource, + 'resource_data_agg.eindeutige_kalender_gruppen_id = tbl_kalender.eindeutige_kalender_gruppen_id', + 'LEFT' + ); + // End of operational resources inclusion + + $this->_ci->KalenderModel->db->join('lehre.tbl_stunde vonstunde', 'tbl_kalender.von::time = vonstunde.beginn', 'LEFT', FALSE); $this->_ci->KalenderModel->db->join('lehre.tbl_stunde bisstunde', 'tbl_kalender.bis::time = bisstunde.ende', 'LEFT', FALSE); @@ -169,6 +239,8 @@ class KalenderLib { $von = new DateTime($row->von); $bis = new DateTime($row->bis); + $resources = isset($row->resources) ? $row->resources : []; + $parsedResources = is_string($resources) ? json_decode($resources, true) : $resources; $events[$id] = (object) [ 'eindeutige_kalender_gruppen_id' => $row->eindeutige_kalender_gruppen_id, @@ -199,7 +271,9 @@ class KalenderLib 'beschreibung' => isset($row->beschreibung) ? $row->beschreibung : '', 'topic' => [], 'collisions' => false, - 'has_assigned_resources' => isset($row->has_assigned_resources) ? $row->has_assigned_resources : false, + 'has_assigned_resources' => !empty($parsedResources), + 'resources' => is_array($parsedResources) ? $parsedResources : [], + 'tags' => isset($row->tags) ? $row->tags : [], ]; } @@ -1912,4 +1986,11 @@ class KalenderLib return $allowed; } -} \ No newline at end of file + private function getLanguageIndex() + { + $this->_ci->SpracheModel->addSelect('index'); + $result = $this->_ci->SpracheModel->loadWhere(array('sprache' => getUserLanguage())); + + return hasData($result) ? getData($result)[0]->index : 1; + } +} diff --git a/application/views/Tempus.php b/application/views/Tempus.php index 438323eb3..db49c57db 100644 --- a/application/views/Tempus.php +++ b/application/views/Tempus.php @@ -10,6 +10,7 @@ 'filtercomponent' => true, 'navigationcomponent' => true, 'vuedatepicker11' => true, + 'tags' => true, 'phrases' => array( 'global', 'ui', diff --git a/public/css/components/calendar/events.css b/public/css/components/calendar/events.css index 8946c1856..f224c6f7b 100644 --- a/public/css/components/calendar/events.css +++ b/public/css/components/calendar/events.css @@ -86,6 +86,9 @@ pointer-events: none; background-color: transparent !important; border: none !important; + width: fit-content; + margin-left: auto; + margin-right: auto; } .fhc-resize-bar--top { diff --git a/public/css/tags.css b/public/css/tags.css index 0230cfbda..ec32f0c69 100644 --- a/public/css/tags.css +++ b/public/css/tags.css @@ -17,6 +17,13 @@ transform: scale(1.15); } +/* add prop that disasbles hover and cursor*/ +.tag.disabled:hover +{ + transform: none; + cursor: default; +} + .tag_rot { background-color: #ff0000ff; } diff --git a/public/js/api/factory/tempus/tag.js b/public/js/api/factory/tempus/tag.js new file mode 100644 index 000000000..6b8a4ce01 --- /dev/null +++ b/public/js/api/factory/tempus/tag.js @@ -0,0 +1,59 @@ +export default { + + getTag(data) + { + return { + method: 'get', + url: '/api/frontend/v1/tempus/Tags/getTag', + params: data + }; + }, + + getTags(data) + { + return { + method: 'get', + url: '/api/frontend/v1/tempus/Tags/getTags' + }; + }, + getTagsByCalendar(calenderGroupId) + { + return { + method: 'get', + url: '/api/frontend/v1/tempus/Tags/getTagsByCalendar/' + calenderGroupId, + }; + }, + addTag(data) + { + return { + method: 'post', + url: '/api/frontend/v1/tempus/Tags/addTag', + params: data + }; + }, + updateTag(data) + { + return { + method: 'post', + url: '/api/frontend/v1/tempus/Tags/updateTag', + params: data + }; + }, + doneTag(data) + { + return { + method: 'post', + url: '/api/frontend/v1/tempus/Tags/doneTag', + params: data + }; + }, + + deleteTag(data) + { + return { + method: 'post', + url: '/api/frontend/v1/tempus/Tags/deleteTag', + params: data + }; + }, +}; diff --git a/public/js/components/Calendar/Tempus.js b/public/js/components/Calendar/Tempus.js index abec49ba1..64b5ef298 100644 --- a/public/js/components/Calendar/Tempus.js +++ b/public/js/components/Calendar/Tempus.js @@ -76,7 +76,8 @@ export default { "drop", "resize", "event-hover", - "event-unhover" + "event-unhover", + "open-reservierung" ], data() { @@ -301,4 +302,4 @@ export default { ` -} \ No newline at end of file +} diff --git a/public/js/components/Tag/Tag.js b/public/js/components/Tag/Tag.js index fc20213c9..f85b57134 100644 --- a/public/js/components/Tag/Tag.js +++ b/public/js/components/Tag/Tag.js @@ -15,6 +15,10 @@ export default { 'deleted', ], props: { + isListItemShown: { + type: Boolean, + default: true + }, endpoint: { type: Object, required: true @@ -79,7 +83,7 @@ export default { 'id': tag_id }; - this.$api.call(this.endpoint.getTag(getData)) + await this.$api.call(this.endpoint.getTag(getData)) .then(result => result.data) .then(result => this.openModal(result)) }, @@ -132,7 +136,7 @@ export default { { postData.id = this.selectedTagId; this.tagData.id = this.selectedTagId; - this.$api.call(this.endpoint.updateTag(postData)); + await this.$api.call(this.endpoint.updateTag(postData)); this.$emit("updated", this.tagData); this.$refs.tagModal.hide(); } @@ -144,7 +148,7 @@ export default { return; } - this.$api.call(this.endpoint.addTag(postData)) + await this.$api.call(this.endpoint.addTag(postData)) .then(response => response.data) .then(response => { if (typeof response === 'number') { @@ -171,7 +175,7 @@ export default { done: !this.tagData.done, notiz: this.tagData.notiz, } - this.$api.call(this.endpoint.doneTag(postData)) + await this.$api.call(this.endpoint.doneTag(postData)) this.$emit("updated", this.tagData); this.$refs.tagModal.hide(); }, @@ -180,7 +184,7 @@ export default { let postData = { id: this.selectedTagId } - this.$api.call(this.endpoint.deleteTag(postData)) + await this.$api.call(this.endpoint.deleteTag(postData)) this.$emit("deleted", this.selectedTagId) this.$refs.tagModal.hide(); }, @@ -228,7 +232,7 @@ export default { } }, template: ` -
+
+
+
+ +
+ + `, +}; diff --git a/public/js/components/Tempus/Header.js b/public/js/components/Tempus/Header.js new file mode 100644 index 000000000..d1dbc2dfa --- /dev/null +++ b/public/js/components/Tempus/Header.js @@ -0,0 +1,108 @@ +import CoreSearchbar from "../searchbar/searchbar.js"; +import NavLanguage from "../navigation/Language.js"; + +export default { + name: "TempusHeader", + components: { + CoreSearchbar, + NavLanguage, + }, + props: { + tempusRoot: String, + avatarUrl: String, + logoutUrl: String, + searchbaroptions: { + type: Object, + required: true, + }, + searchfunction: { + type: Function, + required: true, + }, + }, + emits: ["language-changed"], + methods: { + focusSearchbar() { + this.$refs.searchbar?.$refs?.input?.focus(); + }, + }, + template: /* html */ ` + + `, +}; diff --git a/public/js/components/Tempus/HistoryModal.js b/public/js/components/Tempus/HistoryModal.js new file mode 100644 index 000000000..9716bc1f1 --- /dev/null +++ b/public/js/components/Tempus/HistoryModal.js @@ -0,0 +1,68 @@ +/** + * Copyright (C) 2024 fhcomplete.org + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +import BsModal from "../Bootstrap/Modal.js"; + +export default { + name: "HistoryModal", + components: { + BsModal, + }, + props: { + entries: { + type: Array, + default: () => [], + }, + }, + methods: { + show() { + this.$refs.modal.show(); + }, + hide() { + this.$refs.modal.hide(); + }, + }, + template: ` + + + + + `, +}; diff --git a/public/js/components/Tempus/Renderer/Lehreinheit/calendarEvent.js b/public/js/components/Tempus/Renderer/Lehreinheit/calendarEvent.js index 887f38945..4d2a92fc4 100644 --- a/public/js/components/Tempus/Renderer/Lehreinheit/calendarEvent.js +++ b/public/js/components/Tempus/Renderer/Lehreinheit/calendarEvent.js @@ -1,13 +1,19 @@ export default { - props:{ + props: { event: { type: Object, - required: true - } + required: true, + }, }, - computed:{ + computed: { classes() { - const classes = ['cis-renderer-lehreinheit-calendar-event', 'calendar-event-default', 'h-100', 'w-100', 'p-1']; + const classes = [ + 'cis-renderer-lehreinheit-calendar-event', + 'calendar-event-default', + 'h-100', + 'w-100', + 'p-1', + ]; if (this.event.collisions) { classes.push('calendar-event-collisions'); @@ -15,6 +21,23 @@ export default { return classes; }, + topicString() { + return Array.isArray(this.event.topic) + ? this.event.topic.join(', ') + : this.event.topic; + }, + ortString() { + let orte = [ + ...(this.event.ort_kurzbz || []), + this.event.location?.trim(), + ].filter(Boolean); + return orte.join(', '); + }, + gruppeString() { + return Array.isArray(this.event.gruppe) + ? this.event.gruppe.map((gruppe) => gruppe.bezeichnung).join(', ') + : this.event.gruppe; + }, statusIcon() { if (['planning', 'sync_preview'].includes(this.event.status_kurzbz)) @@ -24,80 +47,119 @@ export default { else if (['live', 'to_delete_live'].includes(this.event.status_kurzbz)) return 'fa-solid fa-user-graduate text-muted' }, - topicString() { - return Array.isArray(this.event.topic) ? this.event.topic.join(', ') : this.event.topic; - }, - ortString() { - let orte = [...(this.event.ort_kurzbz || []), this.event.location?.trim()].filter(Boolean) - return orte.join(', ') - }, - gruppeString() { - return Array.isArray(this.event.gruppe) - ? this.event.gruppe.map(gruppe => gruppe.bezeichnung).join(', ') - : this.event.gruppe; - }, tooltipString() { const tooltipArray = []; - tooltipArray.push([ - this.$p.t('global/uhrzeit'), - [this.start, this.end].join(' - ') - ].join(": ")); + tooltipArray.push( + [this.$p.t('global/uhrzeit'), [this.start, this.end].join(' - ')].join( + ': ', + ), + ); - tooltipArray.push([ - this.$p.t('profilUpdate/topic'), - this.topicString - ].join(": ")); + tooltipArray.push( + [this.$p.t('profilUpdate/topic'), this.topicString].join(': '), + ); - tooltipArray.push([ - this.$p.t('person/ort'), - this.ortString - ].join(": ")); + tooltipArray.push([this.$p.t('person/ort'), this.ortString].join(': ')); if (this.gruppeString) { - tooltipArray.push([ - this.$p.t('lehre/gruppe'), - this.gruppeString - ].join(": ")); + tooltipArray.push( + [this.$p.t('lehre/gruppe'), this.gruppeString].join(': '), + ); } if (Array.isArray(this.event.lektor) && this.event.lektor.length > 0) { if (this.event.lektor.length > 3) { - tooltipArray.push([ - this.$p.t('lehre/lektor'), - this.event.lektor.slice(0, 3).map(lektor => lektor.kurzbz).join("\n") - + "\n" + this.$p.t('lehre/weitereLektoren', [this.event.lektor.length - 3]) - ].join(": ")); + tooltipArray.push( + [ + this.$p.t('lehre/lektor'), + this.event.lektor + .slice(0, 3) + .map((lektor) => lektor.kurzbz) + .join('\n') + + '\n' + + this.$p.t('lehre/weitereLektoren', [ + this.event.lektor.length - 3, + ]), + ].join(': '), + ); } else { - tooltipArray.push([ - this.$p.t('lehre/lektor'), - this.event.lektor.map(lektor => lektor.kurzbz).join("\n") - ].join(": ")); + tooltipArray.push( + [ + this.$p.t('lehre/lektor'), + this.event.lektor.map((lektor) => lektor.kurzbz).join('\n'), + ].join(': '), + ); } } - return tooltipArray.join("\n"); + return tooltipArray.join('\n'); }, start() { - return luxon.Duration - .fromISOTime(this.event.beginn) - .toISOTime({ suppressSeconds: true }); + return luxon.Duration.fromISOTime(this.event.beginn).toISOTime({ + suppressSeconds: true, + }); }, end() { - return luxon.Duration - .fromISOTime(this.event.ende) - .toISOTime({ suppressSeconds: true }); - } + return luxon.Duration.fromISOTime(this.event.ende).toISOTime({ + suppressSeconds: true, + }); + }, + tags() { + if (typeof this.event.tags === 'string') { + try { + return JSON.parse(this.event.tags); + } catch (e) { + console.error('Failed to parse tags:', e); + return []; + } + } + + return this.event.tags || []; + }, + tagsTooltip() { + if (this.tags.length === 0) { + return ''; + } + + return this.tags.map((tag) => tag.beschreibung).join('\n'); + }, + resourcesTooltip() { + if (!this.event.resources || this.event.resources.length === 0) { + return ''; + } + + let resources = this.event.resources; + if (typeof this.event.resources === 'string') { + try { + resources = JSON.parse(this.event.resources); + } catch (e) { + console.error('Failed to parse resources:', e); + return ''; + } + } + + return resources.map((resource) => resource.beschreibung).join('\n'); + }, }, - template: /*html*/` + template: /*html*/ `
-
+
- + +
{{event.verplante_stunden}} @@ -131,4 +193,4 @@ export default {
`, -} +}; diff --git a/public/js/components/Tempus/Renderer/Lehreinheit/modalContent.js b/public/js/components/Tempus/Renderer/Lehreinheit/modalContent.js index 47e7e0a73..aebf1455a 100644 --- a/public/js/components/Tempus/Renderer/Lehreinheit/modalContent.js +++ b/public/js/components/Tempus/Renderer/Lehreinheit/modalContent.js @@ -53,11 +53,33 @@ export default { ortString() { return Array.isArray(this.event.ort_kurzbz) ? this.event.ort_kurzbz.join(', ') : this.event.ort_kurzbz; }, + tags() { + if (typeof this.event.tags === 'string') { + try { + return JSON.parse(this.event.tags); + } catch (e) { + console.error('Failed to parse tags:', e); + return []; + } + } + + return this.event.tags || []; + }, + resourcesValue() { + let resources = this.event.resources; + if (typeof this.event.resources === 'string') { + try { + resources = JSON.parse(this.event.resources); + } catch (e) { + console.error('Failed to parse resources:', e); + return null; + } + } + + return resources.map(resource => resource.beschreibung).join(', '); + }, }, methods: { - mehtodNumberPadding: function (number) { - return numberPadding(number); - }, methodFormatDate: function (d) { return formatDate(d); }, @@ -78,7 +100,7 @@ export default { }); } }, - template: ` + template: /*html*/`
{{$p.t('lvinfo','lehrveranstaltungsinformationen')}} @@ -143,9 +165,36 @@ export default { }} {{event.organisationseinheit}} + + {{ + $p.t('ui','betriebsmittel')? + $p.t('ui','betriebsmittel')+':' + :'' + }} + + {{ resourcesValue }} + + + + {{ + $p.t('ui','tags')? + $p.t('ui','tags')+':' + :'' + }} + +
+ {{ tag.beschreibung }} +
+ +
`, -} \ No newline at end of file +} diff --git a/public/js/components/Tempus/Renderer/Reservierungen/modalContent.js b/public/js/components/Tempus/Renderer/Reservierungen/modalContent.js index 121c70935..69dbca70e 100644 --- a/public/js/components/Tempus/Renderer/Reservierungen/modalContent.js +++ b/public/js/components/Tempus/Renderer/Reservierungen/modalContent.js @@ -52,9 +52,6 @@ export default { }, }, methods: { - mehtodNumberPadding: function (number) { - return numberPadding(number); - }, methodFormatDate: function (d) { return formatDate(d); }, @@ -133,4 +130,4 @@ export default {
`, -} \ No newline at end of file +} diff --git a/public/js/components/Tempus/ResourcesAssignmentModal.js b/public/js/components/Tempus/ResourcesAssignmentModal.js new file mode 100644 index 000000000..fb366f4ba --- /dev/null +++ b/public/js/components/Tempus/ResourcesAssignmentModal.js @@ -0,0 +1,282 @@ +/** + * Copyright (C) 2024 fhcomplete.org + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +import BsModal from "../Bootstrap/Modal.js"; +import FormInput from "../Form/Input.js"; +import ApiOperationalResourceToCalender from "../../api/factory/operationalResourceToCalender.js"; + +export default { + name: "ResourcesAssignmentModal", + components: { + BsModal, + FormInput, + }, + emits: ["saveFinished"], + data() { + return { + calendar: null, + availableResources: [], + filteredAvailableResources: [], + selectedAvailableResource: null, + assignedResources: [], + areFormButtonsDisplayed: false, + }; + }, + computed: { + dropdownParsedAvailableResources() { + return this.availableResources + .map((unit) => ({ + label: unit.beschreibung, + value: unit.betriebsmittel_id, + data: unit, + })) + .sort((a, b) => a.label?.localeCompare(b.label)); + }, + }, + watch: { + selectedAvailableResource(newValue) { + if (!newValue) return; + + this.assignedResources.push({ + betriebsmittel_kalender_id: null, + betriebsmittel_id: newValue.data.betriebsmittel_id, + beschreibung: newValue.data.beschreibung, + anmerkung: "", + isNoteTextareaShown: false, + }); + + this.areFormButtonsDisplayed = true; + }, + }, + methods: { + async open(calendar) { + if (!calendar?.kalender_id) return; + + this.calendar = calendar; + this.availableResources = + await this.fetchSchedulableResourcesByCalender(calendar.kalender_id); + this.filteredAvailableResources = [ + ...this.dropdownParsedAvailableResources, + ]; + this.assignedResources = + await this.fetchAssignedResourcesByCalender(calendar.kalender_id); + + this.show(); + }, + show() { + this.$refs.modal.show(); + }, + hide() { + this.$refs.modal.hide(); + }, + reset() { + this.calendar = null; + this.availableResources = []; + this.filteredAvailableResources = []; + this.selectedAvailableResource = null; + this.assignedResources = []; + this.areFormButtonsDisplayed = false; + }, + async fetchAssignedResourcesByCalender(calenderId) { + const result = await this.$api.call( + ApiOperationalResourceToCalender.getAssignedResourcesByCalender( + calenderId, + ), + ); + + if (result.meta.status === "success") { + return result.data + .filter((unit) => !!unit) + .map((unit) => ({ + isNoteTextareaShown: + unit.anmerkung && unit.anmerkung.trim() !== "", + ...unit, + })); + } + + this.$fhcAlert.alertError( + this.$p.t("ui", "failed_assigned_resources_fetch_error_message"), + ); + return []; + }, + async fetchSchedulableResourcesByCalender(calendarId) { + const result = await this.$api.call( + ApiOperationalResourceToCalender.getSchedulableResourcesByCalendar( + calendarId, + ), + ); + + if (result.meta.status === "success") return result.data; + + this.$fhcAlert.alertError( + this.$p.t("ui", "failed_schedulable_resources_fetch_error_message"), + ); + return []; + }, + filterAvailableResources(event) { + const query = event.query.toLowerCase(); + const unassignedResources = this.dropdownParsedAvailableResources.filter( + (unit) => + !this.assignedResources.some( + (assigned) => assigned.betriebsmittel_id === unit.value, + ), + ); + + this.filteredAvailableResources = query + ? unassignedResources.filter((unit) => + unit.label.toLowerCase().includes(query), + ) + : unassignedResources; + }, + toggleAssignedResourceNoteInput(resource) { + const assignedResource = this.assignedResources.find( + (assigned) => assigned.betriebsmittel_id === resource.betriebsmittel_id, + ); + + if (assignedResource) { + assignedResource.isNoteTextareaShown = + !assignedResource.isNoteTextareaShown; + } + + this.areFormButtonsDisplayed = true; + }, + removeAssignedResource(resource) { + this.assignedResources = this.assignedResources.filter( + (assigned) => + assigned.betriebsmittel_id !== resource.betriebsmittel_id, + ); + this.areFormButtonsDisplayed = true; + }, + async refreshResourcesAssignmentModalData() { + if (!this.calendar?.kalender_id) return; + + this.availableResources = + await this.fetchSchedulableResourcesByCalender( + this.calendar.kalender_id, + ); + this.filteredAvailableResources = [ + ...this.dropdownParsedAvailableResources, + ]; + this.assignedResources = await this.fetchAssignedResourcesByCalender( + this.calendar.kalender_id, + ); + this.selectedAvailableResource = null; + this.areFormButtonsDisplayed = false; + }, + async saveAssignedResourcesToCalendarItem() { + if (!this.calendar?.kalender_id) return; + + const result = await this.$api.call( + ApiOperationalResourceToCalender.storeResourcesToCalendarRelationship( + this.calendar.kalender_id, + this.assignedResources, + ), + ); + + if (result.meta.status === "success") { + this.$fhcAlert.alertSuccess( + this.$p.t("ui", "assigned_resources_save_success_message"), + ); + await this.refreshResourcesAssignmentModalData(); + } else { + this.$fhcAlert.alertError( + this.$p.t("ui", "failed_assigned_resources_save_error_message"), + ); + } + + this.$emit("saveFinished"); + this.hide(); + }, + }, + template: ` + + + + + `, +}; diff --git a/public/js/components/Tempus/SidebarMenu.js b/public/js/components/Tempus/SidebarMenu.js new file mode 100644 index 000000000..264105d56 --- /dev/null +++ b/public/js/components/Tempus/SidebarMenu.js @@ -0,0 +1,190 @@ +import FhcCoursepicker from './Coursepicker.js'; +import LectureSelection from './Filters/LectureSelection.js'; +import VerbandSelection from './Filters/VerbandSelection.js'; +import RoomSelection from './Filters/RoomSelection.js'; +import ParkingSlot from './ParkingSlot.js'; +import StvStudiensemester from '../Stv/Studentenverwaltung/Studiensemester.js'; + +export default { + name: 'TempusSidebarMenu', + components: { + FhcCoursepicker, + LectureSelection, + VerbandSelection, + RoomSelection, + ParkingSlot, + StvStudiensemester, + }, + props: { + previewRole: { + type: String, + required: true, + }, + rooms: { + type: Array, + required: true, + }, + studiengaenge: { + type: Array, + required: true, + }, + studiengaengeAll: { + type: Array, + default: () => [], + }, + lecturers: { + type: Array, + required: true, + }, + selectedStudiensemester: { + type: String, + required: true, + }, + raumvorschlagPreview: { + type: Object, + default: null, + }, + raumvorschlagLoading: { + type: Boolean, + default: false, + }, + }, + emits: [ + 'update:previewRole', + 'update:rooms', + 'update:studiengaenge', + 'update:parkedKeys', + 'update:selectedStudiensemester', + 'remove-lecturer', + 'select-lecturer', + 'select-kw', + 'sync', + 'preview-raumvorschlag', + ], + methods: { + reloadCoursepicker() { + return this.$refs.coursepicker?.reload(); + }, + isParked(id) { + return this.$refs.parking?.isParked(id) ?? false; + }, + park(items) { + return this.$refs.parking?.park(null, items); + }, + unpark(item) { + return this.$refs.parking?.unpark(item); + }, + }, + computed: { + courseLecturers() { + if (!this.lecturers.length) return []; + return this.lecturers + .filter((lecture) => lecture.showCoursePicker) + .map((lecture) => lecture.uid); + }, + }, + template: /* html */ ` + + `, +}; diff --git a/public/js/components/Tempus/TagsAssignmentModal.js b/public/js/components/Tempus/TagsAssignmentModal.js new file mode 100644 index 000000000..9874d7b46 --- /dev/null +++ b/public/js/components/Tempus/TagsAssignmentModal.js @@ -0,0 +1,239 @@ +/** + * Copyright (C) 2024 fhcomplete.org + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +import BsModal from "../Bootstrap/Modal.js"; +import FormInput from "../Form/Input.js"; +import CoreTag from "../Tag/Tag.js"; +import ApiTempusTag from "../../api/factory/tempus/tag.js"; + +export default { + name: "TagsAssignmentModal", + components: { + BsModal, + FormInput, + CoreTag, + }, + emits: ["tagsChanged"], + data() { + return { + calendar: null, + availableTags: [], + filteredAvailableTags: [], + selectedAvailableTag: null, + assignedTags: [], + tagEndpoint: ApiTempusTag, + tagModalEscapeHandler: null, + }; + }, + computed: { + dropdownParsedAvailableTags() { + return this.availableTags.map((tag) => ({ + label: tag.bezeichnung, + value: tag.tag_typ_kurzbz, + data: tag, + })); + }, + }, + methods: { + async open(calendar) { + if (!calendar?.kalender_id) return; + + this.calendar = calendar; + this.availableTags = await this.fetchAvailableTags(); + this.filteredAvailableTags = [...this.dropdownParsedAvailableTags]; + this.assignedTags = await this.fetchAssignedTagsByCalender( + calendar.eindeutige_kalender_gruppen_id, + ); + + this.show(); + }, + show() { + this.$refs.modal.show(); + }, + hide() { + this.$refs.modal.hide(); + }, + reset() { + this.removeTagModalEscapeHandler(); + this.calendar = null; + this.availableTags = []; + this.filteredAvailableTags = []; + this.selectedAvailableTag = null; + this.assignedTags = []; + }, + prepareTagModal() { + const tagModal = this.$refs.tagComponent?.$refs.tagModal; + const tagModalElement = tagModal?.$el; + + if (!tagModalElement) return; + + tagModalElement.addEventListener( + "shown.bs.modal", + () => { + this.tagModalEscapeHandler = (event) => { + if (event.key !== "Escape") return; + + event.preventDefault(); + event.stopImmediatePropagation(); + tagModal.hide(); + }; + document.addEventListener("keydown", this.tagModalEscapeHandler, true); + }, + { once: true }, + ); + tagModalElement.addEventListener( + "hidden.bs.modal", + () => this.restoreAssignmentModalFocus(), + { once: true }, + ); + }, + removeTagModalEscapeHandler() { + if (!this.tagModalEscapeHandler) return; + + document.removeEventListener( + "keydown", + this.tagModalEscapeHandler, + true, + ); + this.tagModalEscapeHandler = null; + }, + restoreAssignmentModalFocus() { + this.removeTagModalEscapeHandler(); + this.$refs.modal?.$el?.focus(); + }, + async fetchAvailableTags() { + const result = await this.$api.call(ApiTempusTag.getTags()); + + if (result.meta.status === "success") return result.data; + + this.$fhcAlert.alertError( + this.$p.t("ui", "failed_available_tags_fetch_error_message"), + ); + return []; + }, + async fetchAssignedTagsByCalender(calendarGroupId) { + const result = await this.$api.call( + ApiTempusTag.getTagsByCalendar(calendarGroupId), + ); + + if (result.meta.status === "success") { + return result.data.filter((tag) => !!tag); + } + + this.$fhcAlert.alertError( + this.$p.t("ui", "failed_assigned_tags_fetch_error_message"), + ); + return []; + }, + filterAvailableTags(event) { + const query = event.query.toLowerCase(); + const unassignedTags = this.dropdownParsedAvailableTags.filter( + (tag) => + !this.assignedTags.some( + (assigned) => assigned.tag_id === tag.value, + ), + ); + + this.filteredAvailableTags = query + ? unassignedTags.filter((tag) => + tag.label.toLowerCase().includes(query), + ) + : unassignedTags; + }, + selectTag(event) { + this.selectedAvailableTag = event.value; + this.prepareTagModal(); + this.$refs.tagComponent?.openModal(event.value.data); + }, + editTag(tag) { + this.prepareTagModal(); + this.$refs.tagComponent?.editTag(tag.notiz_id); + }, + async handleCalendarTagChange() { + this.removeTagModalEscapeHandler(); + + const calendarGroupId = this.calendar?.eindeutige_kalender_gruppen_id; + if (!calendarGroupId) return; + + this.assignedTags = + await this.fetchAssignedTagsByCalender(calendarGroupId); + this.$emit("tagsChanged"); + }, + }, + template: /* html */` + + + + + + `, +}; diff --git a/public/js/components/Tempus/Tempus.js b/public/js/components/Tempus/Tempus.js index d2bce6718..7eb4ca69c 100644 --- a/public/js/components/Tempus/Tempus.js +++ b/public/js/components/Tempus/Tempus.js @@ -14,67 +14,49 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -import CoreSearchbar from "../searchbar/searchbar.js"; -import NavLanguage from "../navigation/Language.js"; -import VerticalSplit from "../verticalsplit/verticalsplit.js"; -import FhcCalendar from "../Calendar/Tempus.js"; -import FhcCoursepicker from "./Coursepicker.js"; -import LectureSelection from "./Filters/LectureSelection.js"; -import VerbandSelection from "./Filters/VerbandSelection.js"; -import RoomSelection from "./Filters/RoomSelection.js"; -import ParkingSlot from "./ParkingSlot.js"; -import ApiKalender from "../../api/factory/tempus/kalender.js"; -import ApiSearchbar from "../../api/factory/searchbar.js"; -import ApiRenderers from "../../api/factory/renderers.js"; -import ApiTempusConfig from "../../api/factory/tempus/config.js"; -import ApiOperationalResourceToCalender from "../../api/factory/operationalResourceToCalender.js"; -import AppMenu from "../AppMenu.js"; -import drop from "../../directives/drop.js"; -import AppConfig from "../AppConfig.js"; - -import BsModal from "../Bootstrap/Modal.js"; - -import BaseTreemenu from '../Base/Treemenu.js'; - -import ApiStudiengangTree from "../../api/factory/tempus/studiengangtree.js"; -import ApiInfo from "../../api/factory/tempus/info.js"; -import StvStudiensemester from "../Stv/Studentenverwaltung/Studiensemester.js"; -import FormInput from "../../../js/components/Form/Input.js"; -import Reservierung from "./Reservierung.js"; -import { getTempusShortcuts } from "./shortcuts.js"; -import KeyboardShortcuts from "./KeyboardShortcuts.js"; -import { useContextMenuActions } from "../../composables/Tempus/ContextMenuActions.js"; -import MultiWeekPlanModal from "./MultiWeekPlanModal.js"; -import { getTempusSearchbarOptions } from "./Filters/searchbarOptions.js"; -import RaumauswahlModal from "./RaumauswahlModal.js"; -import LehreinheitModal from "./LehreinheitModal.js"; -import HorizontalSplit from "../horizontalsplit/horizontalsplit.js"; +import FhcCalendar from '../Calendar/Tempus.js'; +import ApiKalender from '../../api/factory/tempus/kalender.js'; +import ApiSearchbar from '../../api/factory/searchbar.js'; +import ApiRenderers from '../../api/factory/renderers.js'; +import ApiTempusConfig from '../../api/factory/tempus/config.js'; +import drop from '../../directives/drop.js'; +import AppConfig from '../AppConfig.js'; +import ApiInfo from '../../api/factory/tempus/info.js'; +import Reservierung from './Reservierung.js'; +import { getTempusShortcuts } from './shortcuts.js'; +import KeyboardShortcuts from './KeyboardShortcuts.js'; +import { useContextMenuActions } from '../../composables/Tempus/ContextMenuActions.js'; +import MultiWeekPlanModal from './MultiWeekPlanModal.js'; +import HistoryModal from './HistoryModal.js'; +import ResourcesAssignmentModal from './ResourcesAssignmentModal.js'; +import TagsAssignmentModal from './TagsAssignmentModal.js'; +import { getTempusSearchbarOptions } from './Filters/searchbarOptions.js'; +import TempusHeader from './Header.js'; +import TempusAppMenu from './AppMenu.js'; +import TempusVerbandMenu from './VerbandMenu.js'; +import TempusSidebarMenu from './SidebarMenu.js'; +import RaumauswahlModal from './RaumauswahlModal.js'; +import LehreinheitModal from './LehreinheitModal.js'; +import HorizontalSplit from '../horizontalsplit/horizontalsplit.js'; export default { - name: "Tempus", + name: 'Tempus', components: { - CoreSearchbar, - VerticalSplit, FhcCalendar, - FhcCoursepicker, - LectureSelection, - VerbandSelection, - RoomSelection, - ParkingSlot, AppConfig, - AppMenu, - NavLanguage, - BsModal, - BaseTreemenu, - StvStudiensemester, - Multiselect: primevue.multiselect, - FormInput, + TempusAppMenu, + TempusHeader, + TempusVerbandMenu, + TempusSidebarMenu, Reservierung, KeyboardShortcuts, MultiWeekPlanModal, + HistoryModal, + ResourcesAssignmentModal, + TagsAssignmentModal, RaumauswahlModal, LehreinheitModal, - HorizontalSplit + HorizontalSplit, }, props: { defaultSemester: String, @@ -100,7 +82,9 @@ export default { contextMenuActions: useContextMenuActions({ openRaumauswahl: (orig) => this.$refs.raumModal.show(orig), openLehreinheit: (orig) => this.$refs.lehreinheitModal.show(orig), - openResourcesAssignmentModal: (orig) => this.openResourcesAssignmentModal(orig), + openResourcesAssignmentModal: (orig) => + this.$refs.resourcesAssignmentModal?.open(orig), + openTagsModal: (orig) => this.$refs.tagsAssignmentModal?.open(orig), openHistory: (orig) => this.openHistory(orig), deleteEntry: (orig) => this.deleteEntry(orig), syncToLecturer: (orig) => this.syncToLecturer(orig), @@ -110,55 +94,35 @@ export default { deleteEntries: (origList) => this.deleteEntries(origList), openRaumauswahl: (orig) => this.$refs.raumModal.show(orig), }, - canToggleGrid: this.permissions.stundenraster + canToggleGrid: this.permissions.stundenraster, }; }, data() { return { appconfig: {}, - currentMode: "week", + currentMode: 'week', configEndpoints: ApiTempusConfig, - endpoint: ApiStudiengangTree, - raumVorschlaege: [], - selected: [], - - lv_id: null, - events: null, - minimized: false, - currentlySelectedEvent: null, hoveredEvent: null, - //currentDay: new Date(), - studiensemesterKurzbz: this.defaultSemester, - lists: { - nations: [], - sprachen: [], - geschlechter: [], - }, - renderers: null, + renderers: {}, ort_kurzbz: null, - view: "room", parkedKeys: new Set(), lecturers: [], studiengaenge: [], rooms: [], - overlayCache: [], + overlayCache: {}, extraBackgrounds: [], lastRange: null, - stg: null, - semester: null, - studiensemester_kurzbz: null, - - visibleStatusArray: {}, - visibleStatus: ["all"], - selectedStudiensemester: - this.studiensemester_kurzbz ?? this.defaultSemester, + visibleStatus: ['all'], + selectedStudiensemester: this.defaultSemester, calendarDatesByMode: { week: luxon.DateTime.now().setZone(this.config.timezone).toISODate(), month: luxon.DateTime.now().setZone(this.config.timezone).toISODate(), - tableList: luxon.DateTime.now().setZone(this.config.timezone).toISODate(), + tableList: luxon.DateTime.now() + .setZone(this.config.timezone) + .toISODate(), }, historyEntries: [], - previewRole: "planer", + previewRole: 'planer', multiWeekModal: { show: false, lehreinheitId: null, @@ -167,25 +131,19 @@ export default { endTime: null, }, studiengaenge_all: [], - resourcesAssignmentModal: { - calendar: null, - availableResources: [], - filteredAvailableResources: [], - selectedAvailableResource: null, - assignedResources: [], - areFormButtonsDisplayed: false, - }, raumvorschlagPreview: null, raumvorschlagLoading: false, - showEvents: true + showEvents: true, + reservierungPending: false, + bcc: null, }; }, computed: { - currentDay() { - return luxon.DateTime.now().setZone(this.config.timezone).toISODate(); - }, calendarDate() { - return this.calendarDatesByMode[this.currentMode] ?? this.calendarDatesByMode.week; + return ( + this.calendarDatesByMode[this.currentMode] ?? + this.calendarDatesByMode.week + ); }, visibleLecturerUids() { if (!this.lecturers.length) return null; @@ -193,52 +151,20 @@ export default { .filter((lecture) => lecture.showEvents) .map((lecture) => lecture.uid); }, - courseLecturers() { - if (!this.lecturers.length) return []; - return this.lecturers - .filter((lecture) => lecture.showCoursePicker) - .map((lecture) => lecture.uid); - }, keyboardShortcuts() { return getTempusShortcuts(this); }, searchbaroptions() { return getTempusSearchbarOptions(this); }, - dropdownParsedAvailableResources() { - return this.resourcesAssignmentModal.availableResources - .map((unit) => { - return { - label: unit.beschreibung, - value: unit.betriebsmittel_id, - data: unit, - }; - }) - .sort((a, b) => a.label?.localeCompare(b.label)); - }, }, methods: { - async openResourcesAssignmentModal(orig) { - if (!orig?.kalender_id) return; - - this.resourcesAssignmentModal.calendar = orig; - this.resourcesAssignmentModal.availableResources = - await this.fetchSchedulableResourcesByCalender(orig.kalender_id); - this.resourcesAssignmentModal.filteredAvailableResources = [ - ...this.dropdownParsedAvailableResources, - ]; - - this.resourcesAssignmentModal.assignedResources = - await this.fetchAssignedResourcesByCalender(orig.kalender_id); - - this.$refs.resourcesAssignmentModal.show(); - }, async deleteEntry(orig) { if (!orig?.kalender_id) return; await this.deleteEntryCall(orig); this.$refs.calendar.resetEventLoader(); - this.$refs.coursepicker.reload(); + this.$refs.sidebar.reloadCoursepicker(); this.rebuildRaumvorschlag(); }, async deleteEntries(origList) { @@ -249,13 +175,13 @@ export default { validList.map((orig) => this.deleteEntryCall(orig)), ); this.$refs.calendar.resetEventLoader(); - this.$refs.coursepicker.reload(); + this.$refs.sidebar.reloadCoursepicker(); }, async deleteEntryCall(orig) { await this.$api .call(ApiKalender.deleteEntry(orig.kalender_id)) .then(() => { - this.$refs.parking.unpark({ type: orig.type, id: orig.kalender_id }); + this.$refs.sidebar.unpark({ type: orig.type, id: orig.kalender_id }); }); }, async openHistory(orig) { @@ -264,7 +190,7 @@ export default { .call(ApiKalender.getHistory(orig.kalender_id)) .then((result) => { this.historyEntries = result.data ?? []; - this.$refs.historyModel.show(); + this.$refs.historyModal.show(); }); }, syncToLecturer(orig) { @@ -279,22 +205,31 @@ export default { .call(ApiKalender.syncToStudent(orig.kalender_id)) .then(() => this.$refs.calendar.resetEventLoader()); }, - rebuildRaumvorschlag() - { + rebuildRaumvorschlag() { if (this.raumvorschlagPreview) - this.previewRaumvorschlag({ lehreinheit_id: this.raumvorschlagPreview?.lehreinheit_id }); + this.previewRaumvorschlag({ + lehreinheit_id: this.raumvorschlagPreview?.lehreinheit_id, + }); }, - async previewRaumvorschlag(orig) - { - if (!orig) - { + + clearRaumvorschlagPreview() { + this.raumvorschlagPreview = null; + this.raumvorschlagLoading = false; + this.rebuildExtraBackgrounds(); + this.showEvents = true; + }, + async previewRaumvorschlag(orig) { + if (!orig) { this.clearRaumvorschlagPreview(); return; } if (!this.lastRange) return; this.raumvorschlagLoading = true; - this.raumvorschlagPreview = { lehreinheit_id: orig.lehreinheit_id, slots: [] }; + this.raumvorschlagPreview = { + lehreinheit_id: orig.lehreinheit_id, + slots: [], + }; await this.$api .call( @@ -323,49 +258,50 @@ export default { this.rebuildExtraBackgrounds(); }); }, - clearRaumvorschlagPreview() { - this.raumvorschlagPreview = null; - this.rebuildExtraBackgrounds(); - this.showEvents = true; - }, findRaumvorschlagSlot(lehreinheit_id, startDT, endDT) { - if (this.raumvorschlagPreview?.lehreinheit_id !== lehreinheit_id) return null; + if (this.raumvorschlagPreview?.lehreinheit_id !== lehreinheit_id) + return null; - return this.raumvorschlagPreview.slots.find((slot) => { - const slotStart = luxon.DateTime.fromISO(slot.start); - const slotEnd = luxon.DateTime.fromISO(slot.matchEnd); - return ( - slotStart.toMillis() === startDT.toMillis() && - slotEnd.toMillis() === endDT.toMillis() - ); - }) ?? null; + return ( + this.raumvorschlagPreview.slots.find((slot) => { + const slotStart = luxon.DateTime.fromISO(slot.start); + const slotEnd = luxon.DateTime.fromISO(slot.matchEnd); + return ( + slotStart.toMillis() === startDT.toMillis() && + slotEnd.toMillis() === endDT.toMillis() + ); + }) ?? null + ); }, - setOrt (data) { + setOrt(data) { this.ort_kurzbz = data.ort_kurzbz; this.rooms = [{ ort_kurzbz: data.ort_kurzbz }]; }, - setEmp (data) { + setEmp(data) { const uid = data.uid; const label = data.name; - this.lecturers = [{ - uid, - label, - showEvents: true, - overlays: {blocks: true, wishes: true}, - showCoursePicker: false - }]; + this.lecturers = [ + { + uid, + label, + showEvents: true, + overlays: { blocks: true, wishes: true }, + showCoursePicker: false, + }, + ]; this.$refs.calendar.resetEventLoader(); if (this.lastRange) this.handleRange(this.lastRange); }, onSelectVerbandAndClose(payload) { this.onSelectVerband(payload); - bootstrap.Offcanvas.getOrCreateInstance(this.$refs.verbandMenu).hide(); + + const verbandMenu = this.$refs.verbandMenu?.$el; + if (verbandMenu) + bootstrap.Offcanvas.getOrCreateInstance(verbandMenu).hide(); }, - onSelectVerband({ stg_kz, semester, orgform_kurzbz, name }) { - let exists = this.studiengaenge.some( (stg) => stg.stg_kz == stg_kz && @@ -380,59 +316,67 @@ export default { ]; } }, - addToCoursePicker(data) - { + setEmp(data) { + const uid = data.uid; + const label = data.name; + + for (const lect of this.lecturers) delete this.overlayCache[lect.uid]; + + this.lecturers = [ + { + uid, + label, + showEvents: true, + overlays: { blocks: true, wishes: true }, + showCoursePicker: false, + }, + ]; + + this.$refs.calendar.resetEventLoader(); + if (this.lastRange) this.handleRange(this.lastRange); + }, + addToCoursePicker(data) { const uid = data.uid; let lecturer = this.lecturers.find((lecture) => lecture.uid === uid); - if (!lecturer) - { + if (!lecturer) { this.addToFilter(data, 'mitarbeiter'); lecturer = this.lecturers.find((lecture) => lecture.uid === uid); - if (lecturer) - { + if (lecturer) { lecturer.overlays = { blocks: false, - wishes: false - } + wishes: false, + }; } } - if (lecturer) - { - lecturer.showCoursePicker = true + if (lecturer) { + lecturer.showCoursePicker = true; } }, - addToFilter (filter, type) { - if (type === "ort") - { + addToFilter(filter, type) { + if (type === 'ort') { const ort_kurzbz = filter.ort_kurzbz; - if (!this.rooms.some((room) => room.ort_kurzbz === ort_kurzbz)) - { + if (!this.rooms.some((room) => room.ort_kurzbz === ort_kurzbz)) { this.rooms.push({ ort_kurzbz }); } - } - else if (type === "mitarbeiter") - { + } else if (type === 'mitarbeiter') { const uid = filter.uid; const label = filter.name; let lecturer = this.lecturers.find((lecture) => lecture.uid === uid); - if (!lecturer) - { + if (!lecturer) { this.lecturers.push({ uid, label, showEvents: true, overlays: { blocks: true, wishes: true }, - showCoursePicker: false + showCoursePicker: false, }); - } - else - { + } else { lecturer.showEvents = true; lecturer.overlays = { blocks: true, wishes: true }; } @@ -453,20 +397,29 @@ export default { }, { zone: this.config.timezone }, ); - this.calendarDatesByMode = {...this.calendarDatesByMode, week: date.toISODate()}; - this.currentMode = "week"; + this.calendarDatesByMode = { + ...this.calendarDatesByMode, + week: date.toISODate(), + }; + this.currentMode = 'week'; }, handleChangeDate(newDate) { if (!(newDate && luxon.DateTime.isDateTime(newDate) && newDate.isValid)) return; - this.calendarDatesByMode = {...this.calendarDatesByMode, [this.currentMode]: newDate.toISODate()}; + this.calendarDatesByMode = { + ...this.calendarDatesByMode, + [this.currentMode]: newDate.toISODate(), + }; }, handleChangeMode(newMode, newDate) { if (!newMode) return; if (newDate && luxon.DateTime.isDateTime(newDate) && newDate.isValid) { - this.calendarDatesByMode = {...this.calendarDatesByMode, [this.currentMode]: newDate.toISODate()}; + this.calendarDatesByMode = { + ...this.calendarDatesByMode, + [this.currentMode]: newDate.toISODate(), + }; } this.currentMode = newMode; @@ -476,25 +429,10 @@ export default { .call(ApiTempusConfig.updateCollision()) .then(() => { this.$refs.config.reload(); - this.$fhcAlert.alertSuccess(this.$p.t("ui/settings_saved")); + this.$fhcAlert.alertSuccess(this.$p.t('ui/settings_saved')); }) .catch(this.$fhcAlert.handleSystemErrors); }, - toggleStatus(selected) { - if (!selected || selected.length === 0) { - this.visibleStatus = ["all"]; - return; - } - const hasAll = selected.includes("all"); - const hadAll = this.visibleStatus.includes("all"); - - if (hasAll && !hadAll) { - this.visibleStatus = ["all"]; - return; - } - this.visibleStatus = selected.filter((k) => k !== "all"); - if (this.visibleStatus.length === 0) this.visibleStatus = ["all"]; - }, searchfunction(params) { return this.$api.call(ApiSearchbar.search(params)); }, @@ -518,14 +456,14 @@ export default { if (hasLektoren) filter.uid = this.lecturers.map((lecture) => lecture.uid); - if (this.previewRole === "lektor") + if (this.previewRole === 'lektor') return [ this.$api.call( ApiKalender.getPlanLecturer(start.toISODate(), end.toISODate()), ), ]; - if (this.previewRole === "student") + if (this.previewRole === 'student') return [ this.$api.call( ApiKalender.getPlanStudent(start.toISODate(), end.toISODate()), @@ -538,65 +476,20 @@ export default { ), ]; }, - toDateTime(value, timezone) { - if (luxon.DateTime.isDateTime(value)) return value; - - if (value?.date?.isValid) return value.date; - - if (typeof value === "number") - return luxon.DateTime.fromMillis(value, { zone: timezone }); - - if (value instanceof Date) - return luxon.DateTime.fromJSDate(value, { zone: timezone }); - - if (typeof value === "string") - return luxon.DateTime.fromISO(value, { zone: timezone }); - - return luxon.DateTime.invalid("invalid datetime"); - }, - getLastEndOfSameDay(startDT, ends) { - if (!ends?.length) return null; - - const dayKey = startDT.toISODate(); - let lastSameDay = null; - - for (const end of ends) { - const dt = luxon.DateTime.isDateTime(end) - ? end - : luxon.DateTime.fromISO(String(end), { zone: startDT.zoneName }); - - if (!dt.isValid) continue; - - if (dt.toISODate() === dayKey) lastSameDay = dt; - } - - return lastSameDay; - }, - clampEndToGrid(startDT, durationMin, ends) { - const calculatedEnd = startDT.plus({ minutes: durationMin }); - - const lastGridEndSameDay = this.getLastEndOfSameDay(startDT, ends); - - if (!lastGridEndSameDay) return calculatedEnd; - - return calculatedEnd > lastGridEndSameDay - ? lastGridEndSameDay - : calculatedEnd; - }, _parseDates(start, end) { const startDT = luxon.DateTime.fromISO(start); const endDT = luxon.DateTime.fromISO(end); if (!startDT.isValid || !endDT.isValid) { - alert("Ungültiges Datum"); + alert('Ungültiges Datum'); return null; } return { startDT, endDT, - start_time: startDT.toFormat("yyyy-MM-dd HH:mm"), - end_time: endDT.toFormat("yyyy-MM-dd HH:mm"), + start_time: startDT.toFormat('yyyy-MM-dd HH:mm'), + end_time: endDT.toFormat('yyyy-MM-dd HH:mm'), }; }, @@ -613,7 +506,7 @@ export default { const updatedInfos = { ort_kurzbz: this.rooms.length ? this.rooms.map((room) => room.ort_kurzbz) - : obj.orig.ort_kurzbz ?? [], + : (obj.orig.ort_kurzbz ?? []), start_time, end_time, }; @@ -628,13 +521,13 @@ export default { }, resizeHandler(payload) { - if (this.previewRole !== "planer") + if (this.previewRole !== 'planer') //TODO (david) testzweck return; const { item, start, end } = payload; const obj = item[0]; if (!obj?.orig?.kalender_id) - return alert("Kein gültiges Kalender-Event zum Resizen"); + return alert('Kein gültiges Kalender-Event zum Resizen'); const dates = this._parseDates(start, end); @@ -648,28 +541,26 @@ export default { dates.end_time, () => { this.$refs.calendar.resetEventLoader(); - this.$refs.coursepicker.reload(); + this.$refs.sidebar.reloadCoursepicker(); this.rebuildRaumvorschlag(); }, ); }, dropHandler(payload) { - if (this.previewRole !== "planer") + if (this.previewRole !== 'planer') //TODO (david) testzweck return; const { item, start, end } = payload; - if (!item?.length) return alert("Keine Daten gedroppt"); + if (!item?.length) return alert('Keine Daten gedroppt'); const obj = item[0]; - if (!obj?.type) return alert("Unbekannter Drop-Typ"); + if (!obj?.type) return alert('Unbekannter Drop-Typ'); - - if (payload.targetKalenderId) - { - if (obj.type !== "lehreinheit" || !obj.orig?.lehreinheit_id) + if (payload.targetKalenderId) { + if (obj.type !== 'lehreinheit' || !obj.orig?.lehreinheit_id) return alert( - "Nur Lehreinheiten können einem Termin hinzugefügt werden", + 'Nur Lehreinheiten können einem Termin hinzugefügt werden', ); return this.$api @@ -681,9 +572,9 @@ export default { ) .then(() => { this.$refs.calendar.resetEventLoader(); - this.$refs.coursepicker.reload(); + this.$refs.sidebar.reloadCoursepicker(); this.rebuildRaumvorschlag(); - this.bcc.postMessage("dropped"); + this.bcc.postMessage('dropped'); }); } @@ -692,15 +583,11 @@ export default { const { startDT, endDT, start_time, end_time } = dates; - if (obj.type === "reservierung") - { + if (obj.type === 'reservierung') { this.reservierungPending = true; this.$refs.reservierung.show(start_time, end_time); - } - else if (obj.type === "lehreinheit") - { - if (obj.multiweek) - { + } else if (obj.type === 'lehreinheit') { + if (obj.multiweek) { this.openMultiWeekPreview( obj.orig.lehreinheit_id, this.ort_kurzbz ? this.ort_kurzbz : obj.orig.ort_kurzbz, @@ -717,36 +604,38 @@ export default { ); const raeume = matchedSlot?.raeume ?? null; - if (!raeume) - { + if (!raeume) { return this.$api .call( ApiKalender.addKalenderEvent( obj.orig.lehreinheit_id, this.rooms.length ? this.rooms.map((r) => r.ort_kurzbz) - : obj.orig.ort_kurzbz ?? [], + : (obj.orig.ort_kurzbz ?? []), start_time, end_time, ), ) .then((result) => result.data) .then((result) => { - if (result.needs_room_selection) - { - this.$refs.raumModal.showForNew(obj.orig.lehreinheit_id, start_time, end_time, result.raum_vorschlaege) + if (result.needs_room_selection) { + this.$refs.raumModal.showForNew( + obj.orig.lehreinheit_id, + start_time, + end_time, + result.raum_vorschlaege, + ); return; } this.$refs.calendar.resetEventLoader(); - this.$refs.coursepicker.reload(); + this.$refs.sidebar.reloadCoursepicker(); this.rebuildRaumvorschlag(); - this.bcc.postMessage("dropped"); + this.bcc.postMessage('dropped'); }); } - if (raeume.length === 1) - { + if (raeume.length === 1) { return this.$api .call( ApiKalender.addKalenderEvent( @@ -758,16 +647,19 @@ export default { ) .then(() => { this.$refs.calendar.resetEventLoader(); - this.$refs.coursepicker.reload(); - this.bcc.postMessage("dropped"); + this.$refs.sidebar.reloadCoursepicker(); + this.bcc.postMessage('dropped'); this.rebuildRaumvorschlag(); }); } - this.$refs.raumModal.showForNew(obj.orig.lehreinheit_id, start_time, end_time, raeume) - } - else if (obj.type === "kalender") - { + this.$refs.raumModal.showForNew( + obj.orig.lehreinheit_id, + start_time, + end_time, + raeume, + ); + } else if (obj.type === 'kalender') { return this._updateKalenderEvent( obj, startDT, @@ -775,19 +667,17 @@ export default { start_time, end_time, () => { - this.$refs.parking.unpark({ + this.$refs.sidebar.unpark({ type: obj.type, id: obj.orig.kalender_id, }); this.$refs.calendar.resetEventLoader(); this.rebuildRaumvorschlag(); - this.bcc.postMessage("dropped"); + this.bcc.postMessage('dropped'); }, ); - } - else - { - alert("Unbekannter Drop-Typ: " + obj.type); + } else { + alert('Unbekannter Drop-Typ: ' + obj.type); } }, openMultiWeekPreview(lehreinheit_id, ort_kurzbz, start_time, end_time) { @@ -802,19 +692,19 @@ export default { }, onMultiWeekConfirmed() { this.$refs.calendar.resetEventLoader(); - this.$refs.coursepicker.reload(); + this.$refs.sidebar.reloadCoursepicker(); this.rebuildRaumvorschlag(); - this.bcc.postMessage("dropped"); + this.bcc.postMessage('dropped'); }, handleRange(range) { if (!range?.start || !range?.end) return; - if (this.currentMode === "week") { + if (this.currentMode === 'week') { //Workaround because, updateRange is emitting 2 times - const startDay = range.start.startOf("day"); - const endDay = range.end.startOf("day"); + const startDay = range.start.startOf('day'); + const endDay = range.end.startOf('day'); - const days = Math.round(endDay.diff(startDay, "days").days) + 1; + const days = Math.round(endDay.diff(startDay, 'days').days) + 1; if (days > 8) return; } @@ -829,7 +719,9 @@ export default { this.rebuildExtraBackgrounds(); if (this.raumvorschlagPreview) - this.previewRaumvorschlag({ lehreinheit_id: this.raumvorschlagPreview.lehreinheit_id }); + this.previewRaumvorschlag({ + lehreinheit_id: this.raumvorschlagPreview.lehreinheit_id, + }); }, getOverlays(uid, range, rangeKey) { @@ -922,138 +814,6 @@ export default { this.extraBackgrounds = res; }, - async fetchAssignedResourcesByCalender(calenderId) { - let getAssignedResources = await this.$api.call( - ApiOperationalResourceToCalender.getAssignedResourcesByCalender( - calenderId, - ), - ); - if (getAssignedResources.meta.status === "success") { - return getAssignedResources.data - .filter((unit) => !!unit) - .map((unit) => { - return { - isNoteTextareaShown: - unit.anmerkung && unit.anmerkung.trim() !== "", - ...unit, - }; - }) - .filter((unit) => !!unit); - } else { - this.$fhcAlert.alertError( - this.$p.t("ui", "failed_assigned_resources_fetch_error_message"), - ); - } - - return []; - }, - async fetchSchedulableResourcesByCalender(calendarID) { - let getSchedulableResourcesByCalendar = await this.$api.call( - ApiOperationalResourceToCalender.getSchedulableResourcesByCalendar( - calendarID, - ), - ); - if (getSchedulableResourcesByCalendar.meta.status === "success") { - return getSchedulableResourcesByCalendar.data; - } else { - this.$fhcAlert.alertError( - this.$p.t("ui", "failed_schedulable_resources_fetch_error_message"), - ); - } - - return []; - }, - filterAvailableResources(event) { - this.resourcesAssignmentModal.filteredAvailableResources; - const query = event.query.toLowerCase(); - if (!query) { - return (this.resourcesAssignmentModal.filteredAvailableResources = [ - ...this.dropdownParsedAvailableResources.filter((unit) => { - return !this.resourcesAssignmentModal.assignedResources.some( - (assigned) => assigned.betriebsmittel_id === unit.value, - ); - }), - ]); - } - - return (this.resourcesAssignmentModal.filteredAvailableResources = - this.dropdownParsedAvailableResources - .filter((unit) => { - return !this.resourcesAssignmentModal.assignedResources.some( - (assigned) => assigned.betriebsmittel_id === unit.value, - ); - }) - .filter((unit) => { - return unit.label.toLowerCase().includes(query); - })); - }, - toggleAssignedResourceNoteInput(resource) { - const index = this.resourcesAssignmentModal.assignedResources.findIndex( - (assigned) => assigned.betriebsmittel_id === resource.betriebsmittel_id, - ); - if (index !== -1) { - this.resourcesAssignmentModal.assignedResources[ - index - ].isNoteTextareaShown = - !this.resourcesAssignmentModal.assignedResources[index] - .isNoteTextareaShown; - } - - this.resourcesAssignmentModal.areFormButtonsDisplayed = true; - }, - removeAssignedResource(resource) { - this.resourcesAssignmentModal.assignedResources = - this.resourcesAssignmentModal.assignedResources.filter( - (assigned) => - assigned.betriebsmittel_id !== resource.betriebsmittel_id, - ); - - this.resourcesAssignmentModal.areFormButtonsDisplayed = true; - }, - async refreshResourcesAssignmentModalData(calenderItem) { - this.resourcesAssignmentModal.availableResources = - await this.fetchSchedulableResourcesByCalender( - calenderItem.kalender_id, - ); - this.resourcesAssignmentModal.filteredAvailableResources = [ - ...this.dropdownParsedAvailableResources, - ]; - - this.resourcesAssignmentModal.assignedResources = - await this.fetchAssignedResourcesByCalender(calenderItem.kalender_id); - this.resourcesAssignmentModal.selectedAvailableResource = null; - this.resourcesAssignmentModal.areFormButtonsDisplayed = false; - }, - async saveAssignedResourcesToCalendarItem(calenderItem, assignedResources) { - let getSchedulableResourcesByCalendar = await this.$api.call( - ApiOperationalResourceToCalender.storeResourcesToCalendarRelationship( - calenderItem.kalender_id, - assignedResources, - ), - ); - if (getSchedulableResourcesByCalendar.meta.status === "success") { - this.$fhcAlert.alertSuccess( - this.$p.t("ui", "assigned_resources_save_success_message"), - ); - await this.refreshResourcesAssignmentModalData(calenderItem); - } else { - this.$fhcAlert.alertError( - this.$p.t("ui", "failed_assigned_resources_save_error_message"), - ); - } - - this.$refs.calendar.resetEventLoader(); - this.$refs.resourcesAssignmentModal.hide(); - }, - closeResourcesAssignmentModal() { - this.resourcesAssignmentModal = { - availableResources: [], - filteredAvailableResources: [], - selectedAvailableResource: null, - assignedResources: [], - areFormButtonsDisplayed: false, - }; - }, removeLecturer(uid) { if (uid == null) { for (const lect of this.lecturers) delete this.overlayCache[lect.uid]; @@ -1068,10 +828,18 @@ export default { this.$refs.calendar.resetEventLoader(); } }, + handlePreviewRoleChange(role) { + this.previewRole = role; + this.$refs.calendar.resetEventLoader(); + }, + openReservierung() { + this.reservierungPending = false; + this.$refs.reservierung?.show(); + }, triggerSync() { this.$api .call(ApiKalender.sync()) - .then(this.$refs.calendar.resetEventLoader()); + .then(() => this.$refs.calendar.resetEventLoader()); }, onEventHover(event) { this.hoveredEvent = event; @@ -1085,11 +853,11 @@ export default { const event = this.hoveredEvent; if (!event?.kalender_id) return; - if (this.$refs.parking.isParked(event.kalender_id)) { - this.$refs.parking.unpark({ type: event.type, id: event.kalender_id }); + if (this.$refs.sidebar.isParked(event.kalender_id)) { + this.$refs.sidebar.unpark({ type: event.type, id: event.kalender_id }); } else { - this.$refs.parking.park(null, { - type: "kalender", + this.$refs.sidebar.park({ + type: 'kalender', id: event.kalender_id, orig: event, }); @@ -1101,11 +869,8 @@ export default { this.deleteEntry(event); }, - clearHoveredEvent() { - this.hoveredEvent = null; - }, focusSearchbar() { - this.$refs.searchbar?.$refs?.input.focus(); + this.$refs.header?.focusSearchbar(); }, }, watch: { @@ -1125,37 +890,17 @@ export default { this.$refs.calendar.resetEventLoader(); }, }, - "resourcesAssignmentModal.selectedAvailableResource": function (newVal) { - if (!newVal) return; - - this.resourcesAssignmentModal.assignedResources.push({ - betriebsmittel_kalender_id: null, - betriebsmittel_id: newVal.data.betriebsmittel_id, - beschreibung: newVal.data.beschreibung, - anmerkung: "", - isNoteTextareaShown: false, - }); - - this.resourcesAssignmentModal.areFormButtonsDisplayed = true; - }, - sidebarCollapsed(newVal) - { - if(newVal) - this.$refs.hSplit.collapseLeft() - else - this.$refs.hSplit.showBoth() - }, }, mounted() { this.reservierungPending = false; - this.bcc = new BroadcastChannel("fhc-dnd"); - this.bcc.addEventListener("message", (e) => { - if (e.data === "dropped" && !this.reservierungPending) + this.bcc = new BroadcastChannel('fhc-dnd'); + this.bcc.addEventListener('message', (e) => { + if (e.data === 'dropped' && !this.reservierungPending) this.$refs.calendar.resetEventLoader(); }); }, beforeUnmount() { - this.bcc.close(); + this.bcc?.close(); }, async created() { await this.$api @@ -1192,17 +937,14 @@ export default { `link[href="${data[rendertype].calendarEventStyles}"]`, ) ) { - var link = document.createElement("link"); - link.type = "text/css"; - link.rel = "stylesheet"; + var link = document.createElement('link'); + link.type = 'text/css'; + link.rel = 'stylesheet'; link.href = data[rendertype].calendarEventStyles; head.appendChild(link); } } - if (this.renderers === null) { - this.renderers = {}; - } if (!this.renderers[rendertype]) { this.renderers[rendertype] = {}; } @@ -1212,337 +954,96 @@ export default { } }); - this.$api.call(ApiTempusConfig.getHeader()).then((res) => { - this.visibleStatusArray = res.data.visible_status; - this.visibleStatus = ["all"]; - }); this.$api.call(ApiInfo.getStudiengaenge()).then((res) => { this.studiengaenge_all = res.data; }); }, - template: ` + template: /* html */ `
- +
- - - - - + + +
-
-
-
- Verband -
- -
- -
- - - - - - - - - - - - + + + + + + +
`, -}; \ No newline at end of file +}; diff --git a/public/js/components/Tempus/VerbandMenu.js b/public/js/components/Tempus/VerbandMenu.js new file mode 100644 index 000000000..ecf56e60b --- /dev/null +++ b/public/js/components/Tempus/VerbandMenu.js @@ -0,0 +1,25 @@ +import BaseTreemenu from '../Base/Treemenu.js'; + +export default { + name: 'TempusVerbandMenu', + components: { + BaseTreemenu, + }, + emits: ['select-verband-and-close'], + methods: { + onSelectVerbandAndClose(payload) { + this.$emit('select-verband-and-close', payload); + }, + }, + template: /* html */ ` +
+
+
+ Verband +
+ +
+ +
+ `, +}; diff --git a/public/js/composables/Tempus/ContextMenuActions.js b/public/js/composables/Tempus/ContextMenuActions.js index 1c6144902..5f6495ed1 100644 --- a/public/js/composables/Tempus/ContextMenuActions.js +++ b/public/js/composables/Tempus/ContextMenuActions.js @@ -18,6 +18,11 @@ export function useContextMenuActions(handlers) icon: 'fa-solid fa-table-list', action: handlers.openResourcesAssignmentModal }, + { + label: 'Tags', + icon: 'fa-solid fa-tags', + action: handlers.openTagsModal + }, { label: 'Freischalten für Voransicht', icon: 'fa-solid fa-chalkboard-user', diff --git a/system/dbupdate_3.4.php b/system/dbupdate_3.4.php index 66fe6efc9..4b1468906 100644 --- a/system/dbupdate_3.4.php +++ b/system/dbupdate_3.4.php @@ -99,6 +99,8 @@ require_once('dbupdate_3.4/76150_perm_other_lv_plan.php'); require_once('dbupdate_3.4/68957_dashboard_bookmark_neue_Spalte_sort.php'); require_once('dbupdate_3.4/68530_Dashboard_Cleanup.php'); require_once('dbupdate_3.4/77375_ressourcen.php'); +require_once('dbupdate_3.4/78052_tempus_tag_events.php'); +require_once('dbupdate_3.4/78318_rename_column_of_eindeutige_gruppen_id_to_eindeutige_kalender_gruppen_id_in_tbl_kalender.php'); require_once('dbupdate_3.4/78318_rename_column_of_eindeutige_gruppen_id_to_eindeutige_kalender_gruppen_id_in_tbl_kalender.php'); require_once('dbupdate_3.4/75959_StudVw_Automatische_Tags.php'); require_once('dbupdate_3.4/76160_lvv_favorites.php'); diff --git a/system/dbupdate_3.4/46975_tempus.php b/system/dbupdate_3.4/46975_tempus.php index 5dc9f730b..921bbb4e6 100644 --- a/system/dbupdate_3.4/46975_tempus.php +++ b/system/dbupdate_3.4/46975_tempus.php @@ -257,7 +257,7 @@ if(!$result = @$db->db_query("SELECT status_kurzbz_max FROM lehre.tbl_kalender L UPDATE lehre.tbl_kalender_status SET status_kurzbz = 'to_delete', - bezeichnung = 'to_delete', + bezeichnung = 'to_delete' WHERE status_kurzbz = 'todelete'; UPDATE lehre.tbl_kalender_status SET sort = 9 WHERE status_kurzbz = 'deleted'; UPDATE lehre.tbl_kalender_status SET sort = 10 WHERE status_kurzbz = 'archived'; diff --git a/system/dbupdate_3.4/78052_tempus_tag_events.php b/system/dbupdate_3.4/78052_tempus_tag_events.php new file mode 100644 index 000000000..64f47d57b --- /dev/null +++ b/system/dbupdate_3.4/78052_tempus_tag_events.php @@ -0,0 +1,19 @@ +db_query("SELECT 1 + FROM information_schema.columns + WHERE table_schema = 'public' + AND table_name = 'tbl_notizzuordnung' + AND column_name = 'eindeutige_kalender_gruppen_id'"); + +if ($db->db_num_rows($result) === 0) +{ + $qry = "ALTER TABLE public.tbl_notizzuordnung + ADD COLUMN eindeutige_kalender_gruppen_id UUID NULL DEFAULT NULL"; + + if (!$db->db_query($qry)) + echo 'public.tbl_notizzuordnung: '.$db->db_last_error().'
'; + else + echo '
public.tbl_notizzuordnung: eindeutige_kalender_gruppen_id column added'; +} diff --git a/system/phrasesupdate.php b/system/phrasesupdate.php index 7431cbf61..97b0d475c 100644 --- a/system/phrasesupdate.php +++ b/system/phrasesupdate.php @@ -62398,6 +62398,86 @@ I have been informed that I am under no obligation to consent to the transmissio ) ) ), + array( + 'app' => 'core', + 'category' => 'ui', + 'phrase' => 'tags_assignment_modal_title', + 'insertvon' => 'system', + 'phrases' => array( + array( + 'sprache' => 'German', + 'text' => 'Tag-Zuweisung', + 'description' => '', + 'insertvon' => 'system' + ), + array( + 'sprache' => 'English', + 'text' => 'Tag Assignment', + 'description' => '', + 'insertvon' => 'system' + ) + ) + ), + array( + 'app' => 'core', + 'category' => 'ui', + 'phrase' => 'assigned_tags_subtitle', + 'insertvon' => 'system', + 'phrases' => array( + array( + 'sprache' => 'German', + 'text' => 'Zugewiesene Tags', + 'description' => '', + 'insertvon' => 'system' + ), + array( + 'sprache' => 'English', + 'text' => 'Assigned Tags', + 'description' => '', + 'insertvon' => 'system' + ) + ) + ), + array( + 'app' => 'core', + 'category' => 'ui', + 'phrase' => 'no_assigned_tags', + 'insertvon' => 'system', + 'phrases' => array( + array( + 'sprache' => 'German', + 'text' => 'Keine zugewiesenen Tags', + 'description' => '', + 'insertvon' => 'system' + ), + array( + 'sprache' => 'English', + 'text' => 'No Assigned Tags', + 'description' => '', + 'insertvon' => 'system' + ) + ) + ), + array( + 'app' => 'core', + 'category' => 'ui', + 'phrase' => 'tags', + 'insertvon' => 'system', + 'phrases' => array( + array( + 'sprache' => 'German', + 'text' => 'Tags', + 'description' => '', + 'insertvon' => 'system' + ), + array( + 'sprache' => 'English', + 'text' => 'Tags', + 'description' => '', + 'insertvon' => 'system' + ) + ) + ), // generic tabulator phrases END array( 'app' => 'core', diff --git a/system/seeders/015_kalender.sql b/system/seeders/015_kalender.sql index 2ba23c754..a4bbc6a41 100644 --- a/system/seeders/015_kalender.sql +++ b/system/seeders/015_kalender.sql @@ -177,6 +177,17 @@ VALUES (51006, 31), (51004, 32); +-- Calendar setup for tags tests +INSERT INTO lehre.tbl_kalender +(von, bis, typ, status_kurzbz, vorgaenger_kalender_id, eindeutige_kalender_gruppen_id, insertamum, insertvon, updateamum, updatevon) +VALUES +(date_trunc('week', CURRENT_DATE) + INTERVAL '6 day' + TIME '16:55:00', date_trunc('week', CURRENT_DATE) + INTERVAL '6 day' + TIME '17:40:00', 'lehreinheit', 'live', NULL, 'ec1bd001-facf-4b21-a671-bcb2d4f037fd', now(), 'demoadmin', NULL, 'demoadmin'); + +INSERT INTO lehre.tbl_kalender_lehreinheit +(lehreinheit_id, kalender_id) +VALUES +(51004, 33); + -- INSERT INTO public.tbl_variablenname -- (name, defaultwert) -- VALUES('ignore_resources_collisions', 'false'); diff --git a/system/seeders/016_tag.sql b/system/seeders/016_tag.sql new file mode 100644 index 000000000..9fef2cf1c --- /dev/null +++ b/system/seeders/016_tag.sql @@ -0,0 +1,5 @@ +INSERT INTO public.tbl_notiz_typ +(typ_kurzbz, bezeichnung_mehrsprachig, beschreibung, automatisiert, aktiv, zuordnung, tag, style, vorrueckung, prioritaet/*, taglib*/) +VALUES +('prioone', '{Prio 1,Prio 1}', '', false, true, NULL, false, 'tag_rot', false, 1/*, NULL*/), +('priotwo', '{Prio 2,Prio 2}', '', false, true, NULL, false, 'tag_gelb', false, 2/*, NULL*/); \ No newline at end of file diff --git a/tests/cypress/cypress/e2e/specs/ui/tempus/tempus.smoke.cy.js b/tests/cypress/cypress/e2e/specs/ui/tempus/tempus.smoke.cy.js index 454c8686a..45fde5243 100644 --- a/tests/cypress/cypress/e2e/specs/ui/tempus/tempus.smoke.cy.js +++ b/tests/cypress/cypress/e2e/specs/ui/tempus/tempus.smoke.cy.js @@ -71,6 +71,20 @@ context("Tempus smoke tests", () => { tempusPage.getResourcesModal().should("be.visible"); }); + it("shows tags modal when selecting Tags from event context menu", () => { + tempusPage.waitForCalendarToFinishLoading(); + tempusPage + .getCalendarEventsByWeekdayAndStartTime("Sunday", "16:55:00") + .should("have.length.greaterThan", 0); + + tempusPage.getCalendarEventsByWeekdayAndStartTime("Sunday", "16:55:00").first().rightclick(); + tempusPage.getEventContextMenuOption("Tags").click({ force: true }); + waitForOk("@fetchTags"); + waitForOk("@fetchTagsByCalendar"); + + tempusPage.getTagsModal().should("be.visible"); + }); + it("shows history modal when selecting History from event context menu", () => { tempusPage.waitForCalendarToFinishLoading(); tempusPage.getCalendarEvents().should("have.length.greaterThan", 0); diff --git a/tests/cypress/cypress/support/pages/tempus.po.js b/tests/cypress/cypress/support/pages/tempus.po.js index 7eeeab146..3f8dd1e42 100644 --- a/tests/cypress/cypress/support/pages/tempus.po.js +++ b/tests/cypress/cypress/support/pages/tempus.po.js @@ -72,6 +72,14 @@ class TempusPage { method: "GET", url: "**/tempus/OperationalResourceToCalenderAPI/getAssignedResourcesByCalender/**", }).as("fetchAssignedResources"); + cy.intercept({ + method: "GET", + url: "**/tempus/Tags/getTags**", + }).as("fetchTags"); + cy.intercept({ + method: "GET", + url: "**/tempus/Tags/getTagsByCalendar/**", + }).as("fetchTagsByCalendar"); cy.intercept({ method: "POST", url: "**/tempus/coursepicker/getByStg**", @@ -345,6 +353,8 @@ class TempusPage { cy .get("[data-cy='resourcesAssignmentModal']") .closest(".bootstrap-modal.show"); + getTagsModal = () => + cy.get("[data-cy='tagsAssignmentModal']").closest(".bootstrap-modal.show") getCalendarEventRoom = (id) => this.getCalendarEventById(id).find("[data-cy='calendar-event-room']"); getStundenrasterToggle = () =>