diff --git a/application/controllers/api/frontend/v1/tempus/Config.php b/application/controllers/api/frontend/v1/tempus/Config.php index e20716226..30be2cc6f 100644 --- a/application/controllers/api/frontend/v1/tempus/Config.php +++ b/application/controllers/api/frontend/v1/tempus/Config.php @@ -32,7 +32,7 @@ class Config extends FHCAPI_Controller { $config = []; - $result = $this->_ci->VariableModel->getVariables(getAuthUID(), ['ignore_kollision', 'kollision_student', 'ignore_reservierung', 'ignore_zeitsperre']); + $result = $this->_ci->VariableModel->getVariables(getAuthUID(), ['ignore_kollision', 'kollision_student', 'ignore_reservierung', 'ignore_zeitsperre', 'ignore_resources_collisions']); $data = $this->getDataOrTerminateWithError($result); $config['ignore_kollision'] = [ @@ -61,7 +61,11 @@ class Config extends FHCAPI_Controller "value" => ($data['ignore_zeitsperre'] ?? 'false') === 'true' ]; - + $config['ignore_resources_collisions'] = [ + "type" => "checkbox", + "label" => $this->p->t('ui', 'ignore_resources_collisions'), + "value" => ($data['ignore_resources_collisions'] ?? 'false') === 'true' + ]; $this->terminateWithSuccess($config); } @@ -108,6 +112,11 @@ class Config extends FHCAPI_Controller 'ignore_zeitsperre', $this->input->post('ignore_zeitsperre') === true ? 'true' : 'false' ); + $this->_ci->VariableModel->setVariable( + getAuthUID(), + 'ignore_resources_collisions', + $this->input->post('ignore_resources_collisions') === true ? 'true' : 'false' + ); $this->terminateWithSuccess(); } diff --git a/application/controllers/api/frontend/v1/tempus/Kalender.php b/application/controllers/api/frontend/v1/tempus/Kalender.php index 75b8b02f7..7a65a5211 100644 --- a/application/controllers/api/frontend/v1/tempus/Kalender.php +++ b/application/controllers/api/frontend/v1/tempus/Kalender.php @@ -224,7 +224,7 @@ class Kalender extends FHCAPI_Controller if (isError($result)) $this->terminateWithError(getError($result), $result->code); - $this->terminateWithSuccess(); + $this->terminateWithSuccess(getData($result)); } public function getRaumvorschlag() diff --git a/application/controllers/api/frontend/v1/tempus/OperationalResourceToCalenderAPI.php b/application/controllers/api/frontend/v1/tempus/OperationalResourceToCalenderAPI.php new file mode 100644 index 000000000..376a0fedc --- /dev/null +++ b/application/controllers/api/frontend/v1/tempus/OperationalResourceToCalenderAPI.php @@ -0,0 +1,84 @@ + ['admin:r', 'assistenz:r'], + 'storeResourcesToCalendarRelationship' => ['admin:w', 'assistenz:w'], + 'getSchedulableResourcesByCalendar' => ['admin:r', 'assistenz:r'], + ]); + + $this->load->model('ressource/BetriebsmittelKalender_model', 'BetriebsmittelKalenderModel'); + $this->load->model('ressource/Betriebsmittel_model', 'BetriebsmittelModel'); + $this->load->model('ressource/Kalender_model', 'KalenderModel'); + + $this->load->library('CollisionChecker'); + $this->load->library('KalenderLib'); + } + + //------------------------------------------------------------------------------------------------------------------ + // Public methods + public function getSchedulableResourcesByCalendar($calenderID) + { + if (!isset($calenderID)) $this->terminateWithError("Missing required parameter 'kalender_id'"); + + $result = $this->KalenderModel->loadWhere(['kalender_id' => $calenderID]); + if (isError($result)) $this->terminateWithError("Calendar with id '$calenderID' does not have a valid group id"); + + $calender = $this->getDataOrTerminateWithError($result)[0]; + if (!isset($calender)) $this->terminateWithError("Calendar with id '$calenderID' not found"); + + $result = $this->BetriebsmittelModel->getSchedulableEntriesByDatetimeInterval($calender->von, $calender->bis); + $this->terminateWithSuccess($this->getDataOrTerminateWithError($result)); + } + + public function getAssignedResourcesByCalender($calenderID) + { + if (!isset($calenderID)) $this->terminateWithError("Missing required parameter 'kalender_id'"); + + $result = $this->KalenderModel->loadWhere(['kalender_id' => $calenderID]); + if (empty($result)) $this->terminateWithError("Calendar with id '$calenderID' not found"); + + $calenderGroupID = $this->getDataOrTerminateWithError($result)[0]->eindeutige_gruppen_id; + if (!isset($calenderGroupID)) $this->terminateWithError("Calendar with id '$calenderID' does not have a valid group id"); + + + $this->BetriebsmittelKalenderModel->addSelect(['tbl_betriebsmittel_kalender.*', 'tbl_betriebsmittel.beschreibung', 'tbl_betriebsmittel.verplanen']); + $this->BetriebsmittelKalenderModel->addJoin('wawi.tbl_betriebsmittel', 'betriebsmittel_id'); + $result = $this->BetriebsmittelKalenderModel->loadWhere([ + 'eindeutige_kalender_gruppen_id' => $calenderGroupID, + 'tbl_betriebsmittel.verplanen' => true, + ]); + + $this->terminateWithSuccess($this->getDataOrTerminateWithError($result)); + } + + public function storeResourcesToCalendarRelationship() + { + $calenderID = $this->input->post('kalender_id'); + $assignedResources = $this->input->post('assignedResources'); + + if (!isset($calenderID)) $this->terminateWithError("Missing required parameter 'kalender_id'", 'general'); + if (!isset($assignedResources)) $this->terminateWithError("Missing required parameter 'assignedResources'", 'general'); + + $result = $this->KalenderModel->loadWhere(['kalender_id' => $calenderID]); + if (empty($result)) $this->terminateWithError("Calendar with id '$calenderID' not found"); + + $calendar = $this->getDataOrTerminateWithError($result)[0]; + + $calenderGroupID = $calendar->eindeutige_gruppen_id; + if (!isset($calenderGroupID)) $this->terminateWithError("Calendar with id '$calenderID' does not have a valid group id"); + + $result = $this->kalenderlib->addOperationalResourcesToKalenderEvent($calendar, $assignedResources); + if (isError($result)) + $this->terminateWithError(getError($result)); + + $this->terminateWithSuccess(['message' => 'Resources assigned successfully']); + } +} diff --git a/application/controllers/system/MigrateKalender.php b/application/controllers/system/MigrateKalender.php index f9d7416e5..49e895dbc 100644 --- a/application/controllers/system/MigrateKalender.php +++ b/application/controllers/system/MigrateKalender.php @@ -264,7 +264,108 @@ class MigrateKalender extends CLI_Controller } } + public function migrateStundenplanBetriebsmittelEntries() { + $this->setKalendarEntriesGroupIDs(); + $this->setKalendarEntriesGroupIDsForChildren(); + $dbModel = new DB_Model(); + + $deleteOldImportedTempusEntriesQuery = "DELETE from lehre.tbl_betriebsmittel_kalender WHERE quelle != 'tempus_neu' OR quelle IS NULL;"; + $createHelperTypeQuery = "DO $$ + BEGIN + IF NOT EXISTS ( + SELECT 1 + FROM pg_type + WHERE typname = 'betriebsmittel_info' + ) THEN + CREATE TYPE betriebsmittel_info AS ( + betriebsmittel_id bigint, + insertamum timestamp, + insertvon text + ); + END IF; + END $$;"; + + $query = $deleteOldImportedTempusEntriesQuery . + $createHelperTypeQuery . + "WITH test AS ( + SELECT + tk.eindeutige_gruppen_id AS eindeutige_gruppen_id, + array_agg(tk.kalender_id), + array_agg( + ROW(tsb.betriebsmittel_id, tsb.insertamum, tsb.insertvon)::betriebsmittel_info + ) AS betriebsmittel_data + FROM sync.tbl_stundenplandev_kalender AS sk + JOIN lehre.tbl_kalender tk ON tk.kalender_id = sk.kalender_id + JOIN lehre.tbl_stundenplan_betriebsmittel tsb ON tsb.stundenplandev_id = sk.stundenplandev_id + GROUP BY tk.eindeutige_gruppen_id + ) + INSERT INTO lehre.tbl_betriebsmittel_kalender ( + eindeutige_kalender_gruppen_id, + betriebsmittel_id, + insertamum, + insertvon + ) + SELECT + t.eindeutige_gruppen_id, + bm.betriebsmittel_id, + bm.insertamum, + bm.insertvon + FROM test t + CROSS JOIN LATERAL unnest(t.betriebsmittel_data) AS bm + ON CONFLICT (eindeutige_kalender_gruppen_id, betriebsmittel_id) DO NOTHING; + ;"; + + $dbModel->db->query($query); + } + + private function setKalendarEntriesGroupIDs() { + $dbModel = new DB_Model(); + + $query = "UPDATE lehre.tbl_kalender + SET eindeutige_gruppen_id = gen_random_uuid() + WHERE vorgaenger_kalender_id IS NULL + AND eindeutige_gruppen_id IS NULL;"; + + $dbModel->db->query($query); + + } + + private function setKalendarEntriesGroupIDsForChildren() { + $dbModel = new DB_Model(); + + $query = "WITH RECURSIVE tree AS + ( + SELECT + kalender_id, + vorgaenger_kalender_id, + kalender_id AS root_id, + eindeutige_gruppen_id as root_eindeutige_gruppen_id + FROM lehre.tbl_kalender + WHERE vorgaenger_kalender_id IS NULL + + UNION ALL + + SELECT + i.kalender_id, + i.vorgaenger_kalender_id, + t.root_id, + t.root_eindeutige_gruppen_id + FROM lehre.tbl_kalender i + JOIN tree t + ON i.vorgaenger_kalender_id = t.kalender_id + where i.eindeutige_gruppen_id is NULL + + ) + UPDATE lehre.tbl_kalender k + SET eindeutige_gruppen_id = t.root_eindeutige_gruppen_id + FROM tree t + WHERE k.kalender_id = t.kalender_id + AND k.eindeutige_gruppen_id IS NULL;"; + + $dbModel->db->query($query); + } + private function _insertKalender($block, $typ) { $result = $this->KalenderModel->insert( diff --git a/application/helpers/hlp_common_helper.php b/application/helpers/hlp_common_helper.php index 06cfa1cfd..b9ce6b200 100644 --- a/application/helpers/hlp_common_helper.php +++ b/application/helpers/hlp_common_helper.php @@ -570,3 +570,13 @@ function buildDropdownEntryPrintArray($id, $name, $parameterurl, $uid=null, $ord ]; } + +function generateUUID() +{ + $data = openssl_random_pseudo_bytes(16); + + $data[6] = chr(ord($data[6]) & 0x0f | 0x40); + $data[8] = chr(ord($data[8]) & 0x3f | 0x80); + + return vsprintf('%s%s-%s-%s-%s-%s%s%s', str_split(bin2hex($data), 4)); +} \ No newline at end of file diff --git a/application/libraries/CollisionChecker.php b/application/libraries/CollisionChecker.php index b7921a6d9..ac9152d2b 100644 --- a/application/libraries/CollisionChecker.php +++ b/application/libraries/CollisionChecker.php @@ -18,10 +18,12 @@ class CollisionChecker $this->_ci->load->library('collision/checks/LectureCollisionCheck'); $this->_ci->load->library('collision/checks/VerbandCollisionCheck'); $this->_ci->load->library('collision/checks/StudentCollisionCheck'); + $this->_ci->load->library('collision/checks/ResourcesCollisionCheck'); $this->register($this->_ci->roomcollisioncheck); $this->register($this->_ci->lecturecollisioncheck); $this->register($this->_ci->verbandcollisioncheck); $this->register($this->_ci->studentcollisioncheck); + $this->register($this->_ci->resourcescollisioncheck); Events::trigger('collision_register', $this); } diff --git a/application/libraries/KalenderLib.php b/application/libraries/KalenderLib.php index 19c800e3f..3a531102f 100644 --- a/application/libraries/KalenderLib.php +++ b/application/libraries/KalenderLib.php @@ -14,6 +14,7 @@ class KalenderLib $this->_ci->load->model('ressource/Kalender_Event_model', 'KalenderEventModel'); $this->_ci->load->model('ressource/Kalender_Event_Teilnehmer_model', 'KalenderEventTeilnehmerModel'); $this->_ci->load->model('ressource/Kalender_Ort_model', 'KalenderOrtModel'); + $this->_ci->load->model('ressource/BetriebsmittelKalender_model', 'BetriebsmittelKalenderModel'); $this->_ci->load->model('education/Lehreinheit_model', 'LehreinheitModel'); $this->_ci->load->model('education/Lehrveranstaltung_model', 'LehrveranstaltungModel'); $this->_ci->load->model('education/LehreinheitMitarbeiter_model', 'LehreinheitMitarbeiterModel'); @@ -38,6 +39,7 @@ class KalenderLib $end_date = date('Y-m-d', strtotime($end_date . ' +1 day')); $this->_ci->KalenderModel->addSelect('tbl_kalender.kalender_id, + tbl_kalender.eindeutige_gruppen_id, tbl_kalender.status_kurzbz, tbl_kalender.typ, tbl_kalender.von, @@ -134,6 +136,8 @@ class KalenderLib $this->_ci->KalenderModel->db->where('tbl_kalender.von >=', $start_date); $this->_ci->KalenderModel->db->where('tbl_kalender.bis <', $end_date); + + $this->_ci->KalenderModel->addOrder('tbl_kalender.eindeutige_gruppen_id', 'DESC'); } private function _mapEvents($data, $collisionCheck = true) @@ -155,6 +159,7 @@ class KalenderLib $bis = new DateTime($row->bis); $events[$id] = (object) [ + 'eindeutige_gruppen_id' => $row->eindeutige_gruppen_id, 'type' => $row->typ, 'beginn' => $von->format('H:i:s'), 'ende' => $bis->format('H:i:s'), @@ -179,7 +184,8 @@ class KalenderLib 'titel' => isset($row->titel) ? $row->titel : '', 'beschreibung' => isset($row->beschreibung) ? $row->beschreibung : '', 'topic' => [], - 'collisions' => false + 'collisions' => false, + 'has_assigned_resources' => isset($row->has_assigned_resources) ? $row->has_assigned_resources : false, ]; } @@ -436,6 +442,14 @@ class KalenderLib WHERE nachfolger.vorgaenger_kalender_id = tbl_kalender.kalender_id)', null, false); $this->_ci->KalenderModel->db->where_not_in('status_kurzbz', array('deleted')); + + $this->_ci->KalenderModel->addSelect([ + "( + SELECT COUNT(*) FROM lehre.tbl_betriebsmittel_kalender + WHERE tbl_betriebsmittel_kalender.eindeutige_kalender_gruppen_id = tbl_kalender.eindeutige_gruppen_id + ) AS has_assigned_resources" + ]); + $data = $this->_ci->KalenderModel->load(); return $this->_mapEvents($data); @@ -633,6 +647,7 @@ class KalenderLib 'bis' => $end_date, 'typ' => 'lehreinheit', 'status_kurzbz' => 'planning', + 'eindeutige_gruppen_id' => $this->_ci->KalenderModel->generateUniqueGroupId(), 'insertvon' => getAuthUID(), 'insertamum' => date('Y-m-d H:i:s') ) @@ -990,6 +1005,81 @@ class KalenderLib return $kalenderresult; } + public function addOperationalResourcesToKalenderEvent($calendar, $assignedResources) + { + $checkerData = new stdClass(); + $checkerData->kalender_id = $calendar->kalender_id; + $checkerData->betriebsmittel_ids = []; + $checkerData->von = $calendar->von; + $checkerData->bis = $calendar->bis; + + foreach ($assignedResources as $assignedResource) { + array_push($checkerData->betriebsmittel_ids, $assignedResource['betriebsmittel_id']); + } + + $errors = []; + $result = $this->_ci->collisionchecker->run($checkerData); + if (!empty($result)) + { + $errors = $result; + } + + if (!empty($errors)) { + return error($errors); + } + + $this->_ci->db->trans_start(); + + $allowedResourceIDs = array_filter(array_map( + function ($resource) { + return $resource['betriebsmittel_id'] ?? null; + }, + $assignedResources + )); + + if (!empty($allowedResourceIDs)) { + $placeholders = implode(',', array_fill(0, count($allowedResourceIDs), '?')); + + $query = " + DELETE FROM lehre.tbl_betriebsmittel_kalender + WHERE eindeutige_kalender_gruppen_id = ? + AND betriebsmittel_id NOT IN ($placeholders) + "; + + $params = array_merge( + [$calendar->eindeutige_gruppen_id], + $allowedResourceIDs + ); + + $this->_ci->db->query($query, $params); + } else { + $this->_ci->db->where('eindeutige_kalender_gruppen_id', $calendar->eindeutige_gruppen_id); + $this->_ci->db->delete('lehre.tbl_betriebsmittel_kalender'); + } + + foreach ($assignedResources as $assignedResource) { + $data = [ + 'eindeutige_kalender_gruppen_id' => $calendar->eindeutige_gruppen_id, + 'betriebsmittel_id' => $assignedResource['betriebsmittel_id'], + 'anmerkung' => $assignedResource['anmerkung'] ?? null, + 'quelle' => 'tempus_neu', + 'updateamum' => date('c'), + 'updatevon' => getAuthUid(), + ]; + if (isset($assignedResource['betriebsmittel_kalender_id'])) { + $result = $this->_ci->BetriebsmittelKalenderModel->update($assignedResource['betriebsmittel_kalender_id'], $data); + } else { + $result = $this->_ci->BetriebsmittelKalenderModel->insert(array_merge($data, [ + 'insertvon' => getAuthUid(), + 'updateamum' => date('c'), + ])); + } + } + $this->_ci->db->trans_complete(); + + return success('Operational resources added to calendar event successfully.'); + } + private function _addTeilnehmerToEvent($kalender_id, $uid, $rolle) { return $this->_ci->KalenderEventTeilnehmerModel->insert( @@ -1090,6 +1180,18 @@ class KalenderLib $kalender_entry = getData($entryResult); + $calendarResources = $this->_ci->BetriebsmittelKalenderModel->loadWhere(['eindeutige_kalender_gruppen_id' => $kalender_entry->eindeutige_gruppen_id]); + if (isError($calendarResources)) return $calendarResources; + + $calendarResourcesItems = getData($calendarResources); + $calendarResourcesIDs = []; + if (is_array($calendarResourcesItems)) { + $calendarResourcesIDs = array_map(function($resource) { + return $resource->betriebsmittel_id; + }, getData($calendarResources)); + } + + $kalender_entry->betriebsmittel_ids = $calendarResourcesIDs; $kalender_entry->ort_kurzbz = !is_null($ort_kurzbz) ? (array) $ort_kurzbz : $kalender_entry->ort_kurzbz; $kalender_entry->von = $start_time ?? $kalender_entry->von; $kalender_entry->bis = $end_time ?? $kalender_entry->bis; @@ -1115,7 +1217,7 @@ class KalenderLib if (isError($result)) return $result; } - return success('Erfolgreich'); + return success($result); } public function updateZeit($kalender_id, $start_date, $end_date) @@ -1237,6 +1339,7 @@ class KalenderLib 'typ' => 'lehreinheit', 'status_kurzbz' => 'planning', 'vorgaenger_kalender_id' => $old_id, + 'eindeutige_gruppen_id' => $kalender_entry->eindeutige_gruppen_id, 'insertvon' => getAuthUID(), 'insertamum' => date('Y-m-d H:i:s') ) diff --git a/application/libraries/collision/checks/ResourcesCollisionCheck.php b/application/libraries/collision/checks/ResourcesCollisionCheck.php new file mode 100644 index 000000000..0be789b7f --- /dev/null +++ b/application/libraries/collision/checks/ResourcesCollisionCheck.php @@ -0,0 +1,85 @@ +_ci =& get_instance(); + $this->_ci->load->model('ressource/Kalender_model', 'KalenderModel'); + $this->_ci->load->library('VariableLib', array('uid' => getAuthUID())); + $this->_ci->load->library('PhrasesLib', array('ui')); + } + + public function getName() + { + return 'resources'; + } + + public function check($data) + { + if (!isset($data->betriebsmittel_ids, $data->von, $data->bis, $data->kalender_id)) return []; + + if (empty($data->betriebsmittel_ids)) return []; + + if ($this->_ci->variablelib->getVar('ignore_kollision') === 'true') return []; + if ($this->_ci->variablelib->getVar('ignore_resources_collisions') === 'true') return []; + + $this->_ci->KalenderModel->addSelect('kalender_id, von, bis, bm.beschreibung AS betriebsmittel_beschreibung'); + $this->_ci->KalenderModel->addJoin('lehre.tbl_betriebsmittel_kalender bk', 'bk.eindeutige_kalender_gruppen_id = tbl_kalender.eindeutige_gruppen_id'); + $this->_ci->KalenderModel->addJoin('wawi.tbl_betriebsmittel bm', 'bm.betriebsmittel_id = bk.betriebsmittel_id'); + + $this->_ci->KalenderModel->db->where('tbl_kalender.kalender_id !=', $data->kalender_id); + $this->_ci->KalenderModel->db->where('tbl_kalender.kalender_id NOT IN (SELECT vorgaenger_kalender_id FROM lehre.tbl_kalender WHERE vorgaenger_kalender_id IS NOT NULL)', null, false); + $this->_ci->KalenderModel->db->where_not_in('tbl_kalender.status_kurzbz', ['archived', 'deleted', 'to_delete']); + $this->_ci->KalenderModel->db->where_in('bk.betriebsmittel_id', $data->betriebsmittel_ids); + + + $result = $this->_ci->KalenderModel->loadWhere(array( + 'von <' => $data->bis, + 'bis >' => $data->von, + )); + + if (isError($result)) return []; + if (!hasData($result)) return []; + + return array_map(function($row) + { + return [ + 'errorCode' => 'resource_collision', + 'message' => $this->_ci->phraseslib->t('ui', 'resource_collision') . ': ' . $row->betriebsmittel_beschreibung . ' (' . date('d.m.Y H:i', strtotime($row->von)) . ' - ' . date('d.m.Y H:i', strtotime($row->bis)) . ')' + ]; + }, getData($result)); + } + + public function checkAll($kalender_ids) + { + if (empty($kalender_ids)) return []; + + $this->_ci->KalenderModel->addSelect('DISTINCT ON (tbl_kalender.kalender_id) tbl_kalender.kalender_id'); + $this->_ci->KalenderModel->addJoin('lehre.tbl_betriebsmittel_kalender current_bk', 'current_bk.eindeutige_kalender_gruppen_id = tbl_kalender.eindeutige_gruppen_id'); + $this->_ci->KalenderModel->addJoin('lehre.tbl_betriebsmittel_kalender other_bk', 'other_bk.betriebsmittel_id = current_bk.betriebsmittel_id'); + $this->_ci->KalenderModel->addJoin('lehre.tbl_kalender other_kalender', 'other_kalender.eindeutige_gruppen_id = other_bk.eindeutige_kalender_gruppen_id'); + + $this->_ci->KalenderModel->db->where('other_kalender.kalender_id != tbl_kalender.kalender_id', null, false); + $this->_ci->KalenderModel->db->where('other_kalender.von < tbl_kalender.bis', null, false); + $this->_ci->KalenderModel->db->where('other_kalender.bis > tbl_kalender.von', null, false); + $this->_ci->KalenderModel->db->where_not_in('other_kalender.status_kurzbz', ['archived', 'deleted', 'to_delete']); + $this->_ci->KalenderModel->db->where_in('tbl_kalender.kalender_id', $kalender_ids); + $this->_ci->KalenderModel->db->where('other_kalender.kalender_id NOT IN (SELECT vorgaenger_kalender_id FROM lehre.tbl_kalender WHERE vorgaenger_kalender_id IS NOT NULL)', null, false); + + $result = $this->_ci->KalenderModel->load(); + + if (isError($result) || !hasData($result)) return []; + + $grouped = []; + foreach (getData($result) as $row) + { + $grouped[$row->kalender_id][] = true; + } + + return $grouped; + } +} \ No newline at end of file diff --git a/application/models/ressource/BetriebsmittelKalender_model.php b/application/models/ressource/BetriebsmittelKalender_model.php new file mode 100644 index 000000000..f9a519bbf --- /dev/null +++ b/application/models/ressource/BetriebsmittelKalender_model.php @@ -0,0 +1,14 @@ +dbTable = 'lehre.tbl_betriebsmittel_kalender'; + $this->pk = 'betriebsmittel_kalender_id'; + } +} diff --git a/application/models/ressource/Betriebsmittel_model.php b/application/models/ressource/Betriebsmittel_model.php index 290c3491d..682961cee 100644 --- a/application/models/ressource/Betriebsmittel_model.php +++ b/application/models/ressource/Betriebsmittel_model.php @@ -31,4 +31,26 @@ class Betriebsmittel_model extends DB_Model return $this->execQuery($qry); } + + public function getSchedulableEntriesByDatetimeInterval($from, $to) + { + $qry = "SELECT + * + FROM + wawi.tbl_betriebsmittel + WHERE + verplanen=true + AND + NOT EXISTS( + SELECT 1 FROM lehre.tbl_betriebsmittel_kalender + JOIN lehre.tbl_kalender ON tbl_kalender.eindeutige_gruppen_id = tbl_betriebsmittel_kalender.eindeutige_kalender_gruppen_id + WHERE + betriebsmittel_id=tbl_betriebsmittel.betriebsmittel_id + AND tbl_kalender.von <= ? + AND tbl_kalender.bis >= ? + )"; + + + return $this->execQuery($qry, array($from, $to)); + } } diff --git a/application/models/ressource/Kalender_model.php b/application/models/ressource/Kalender_model.php index 2ca8e6ac1..d538b7313 100644 --- a/application/models/ressource/Kalender_model.php +++ b/application/models/ressource/Kalender_model.php @@ -11,4 +11,15 @@ class Kalender_model extends DB_Model $this->dbTable = 'lehre.tbl_kalender'; $this->pk = 'kalender_id'; } + + public function generateUniqueGroupId() + { + while (true) { + $uniqueGroupId = generateUUID(); + $result = $this->loadWhere(['eindeutige_gruppen_id' => $uniqueGroupId]); + if (!hasData($result)) { + return $uniqueGroupId; + } + } + } } diff --git a/public/js/api/factory/operationalResourceToCalender.js b/public/js/api/factory/operationalResourceToCalender.js new file mode 100644 index 000000000..618ab9e68 --- /dev/null +++ b/public/js/api/factory/operationalResourceToCalender.js @@ -0,0 +1,38 @@ +/** + * Copyright (C) 2025 fhcomplete.org + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +export default { + getSchedulableResourcesByCalendar(calendarID) { + return { + method: "get", + url: "/api/frontend/v1/tempus/OperationalResourceToCalenderAPI/getSchedulableResourcesByCalendar/" + calendarID, + }; + }, + getAssignedResourcesByCalender(calenderID) { + return { + method: "get", + url: "/api/frontend/v1/tempus/OperationalResourceToCalenderAPI/getAssignedResourcesByCalender/" + calenderID, + }; + }, + storeResourcesToCalendarRelationship(calenderID, assignedResources) { + return { + method: "post", + url: "/api/frontend/v1/tempus/OperationalResourceToCalenderAPI/storeResourcesToCalendarRelationship", + params: { kalender_id: calenderID, assignedResources: assignedResources }, + }; + } +}; diff --git a/public/js/components/Tempus/Renderer/Lehreinheit/calendarEvent.js b/public/js/components/Tempus/Renderer/Lehreinheit/calendarEvent.js index 1367d29e0..ae375da8d 100644 --- a/public/js/components/Tempus/Renderer/Lehreinheit/calendarEvent.js +++ b/public/js/components/Tempus/Renderer/Lehreinheit/calendarEvent.js @@ -82,8 +82,14 @@ export default { template: /*html*/`
+
+ +
{{ topicString }} - {{ ortString }} + {{ ortString }} {{ gruppeString }} this.renderers), - appConfig: Vue.computed(() => this.appconfig), - contextMenuActions: useContextMenuActions({ - openRaumauswahl: (orig) => this.openRaumauswahl(orig), - openHistory: (orig) => this.openHistory(orig), - deleteEntry: (orig) => this.deleteEntry(orig), - syncToLecturer: (orig) => this.syncToLecturer(orig), - syncToStudent: (orig) => this.syncToStudent(orig), - }), - tableActions: { - deleteEntries: (origList) => this.deleteEntries(origList), - openRaumauswahl: (orig) => this.openRaumauswahl(orig), - }, - } - }, - data() { - return { - appconfig: {}, - 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, - ort_kurzbz: null, - view: 'room', - parkedKeys: new Set(), - lecturers: [], - studiengaenge: [], - rooms: [], - overlayCache: [], - extraBackgrounds: [], - lastRange: null, - stg: null, - semester: null, - studiensemester_kurzbz: null, - raumModal: { - show: false, - loading: false, - vorschlaege: [], - event: null - }, - visibleStatusArray: {}, - visibleStatus: ['all'], - selectedStudiensemester: this.studiensemester_kurzbz ?? this.defaultSemester, - calendarDate: luxon.DateTime.now().setZone(this.config.timezone).toISODate(), - historyEntries: [], - previewRole: 'planer', - multiWeekModal: { - show: false, - lehreinheitId: null, - ortKurzbz: null, - startTime: null, - endTime: null, - }, - studiengaenge_all: [] - } - }, - computed: { - currentDay() { - return luxon.DateTime.now().setZone(this.config.timezone).toISODate(); - }, - visibleLecturerUids() { - if (!this.lecturers.length) - return null; - return this.lecturers.filter(lecture => lecture.showEvents).map(lecture => lecture.uid); - }, - visibleStatusOptions() { - return Object.entries(this.visibleStatusArray).map(([key, label]) => ({ key, label })); - }, - visibleStatusValue() { - if (this.visibleStatus.includes('all')) - return this.visibleStatusOptions.filter(visibleStatus => visibleStatus.key === 'all'); - return this.visibleStatus.map(status => ({ key: status, label: this.visibleStatusArray[status] })); - }, - keyboardShortcuts() { - return getTempusShortcuts(this); - }, - searchbaroptions() { - return getTempusSearchbarOptions(this); - }, - }, - methods: { - async openRaumauswahl(orig) { - if (!orig?.kalender_id) - return; - this.raumModal = orig; - - await this.$api.call(ApiKalender.getRaumvorschlag( - orig.kalender_id - )).then(result => { - - this.raumVorschlaege = result.data ?? []; - this.$refs.raumModal.show(); - }); - }, - async deleteEntry(orig) - { - if (!orig?.kalender_id) - return; - - await this.deleteEntryCall(orig); - this.$refs.calendar.resetEventLoader(); - this.$refs.coursepicker.reload(); - }, - async deleteEntries(origList) - { - let validList = (origList ?? []).filter(orig => orig?.kalender_id); - if (!validList.length) - return; - - await Promise.allSettled(validList.map(orig => this.deleteEntryCall(orig))); - this.$refs.calendar.resetEventLoader(); - this.$refs.coursepicker.reload(); - }, - async deleteEntryCall(orig) - { - await this.$api.call(ApiKalender.deleteEntry( - orig.kalender_id - )).then(() => { - this.$refs.parking.unpark({ type: orig.type, id: orig.kalender_id }); - }); - }, - async openHistory(orig) - { - if (!orig?.kalender_id) - return; - await this.$api.call(ApiKalender.getHistory( - orig.kalender_id - )).then(result => { - this.historyEntries = result.data ?? []; - this.$refs.historyModel.show(); - }); - }, - syncToLecturer(orig) { - if (!orig?.kalender_id) - return; - return this.$api.call(ApiKalender.syncToLecturer(orig.kalender_id)) - .then(() => this.$refs.calendar.resetEventLoader()); - }, - syncToStudent(orig) { - if (!orig?.kalender_id) - return; - return this.$api.call(ApiKalender.syncToStudent(orig.kalender_id)) - .then(() => this.$refs.calendar.resetEventLoader()); - }, - async selectRaum(ort_kurzbz) { - const orig = this.raumModal; - await this.$api.call( - ApiKalender.updateKalenderEvent(orig.kalender_id, { - ort_kurzbz, - start_time: orig.von, - end_time: orig.bis - })).then(() => this.$refs.raumModal.hide()); - this.$refs.calendar.resetEventLoader(); - }, - setOrt: function(data) - { - this.ort_kurzbz = data.ort_kurzbz; - this.rooms = [{ ort_kurzbz: data.ort_kurzbz }]; - }, - onSelectVerbandAndClose(payload) { - console.log(payload); - this.onSelectVerband(payload); - bootstrap.Offcanvas.getOrCreateInstance(this.$refs.verbandMenu).hide(); - }, - - onSelectVerband({link, studiengang_kz, semester, orgform_kurzbz, name}) - { - if (orgform_kurzbz) - { - semester = null; - } - else if (typeof link === 'string') - { - [studiengang_kz, semester] = link.split('/'); - } - - let exists = this.studiengaenge.some(stg => - stg.studiengang_kz == studiengang_kz - && stg.semester == semester - && stg.orgform_kurzbz === orgform_kurzbz - ); - - if (!exists) - { - this.studiengaenge = [ - ...this.studiengaenge, - { studiengang_kz, semester, orgform_kurzbz, name } - ]; - } - }, - setEmp: function(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 }, - }]; - - this.$refs.calendar.resetEventLoader(); - if (this.lastRange) - this.handleRange(this.lastRange); - }, - addToFilter: function(filter, type) - { - if (type === 'ort') - { - const ort_kurzbz = filter.ort_kurzbz; - if (!this.rooms.some(room => room.ort_kurzbz === ort_kurzbz)) - { - this.rooms.push({ ort_kurzbz }); - } - } - else if (type === 'mitarbeiter') - { - const uid = filter.uid; - const label = filter.name; - if (!this.lecturers.some(l => l.uid === uid)) - { - this.lecturers.push({ - uid, - label, - showEvents: true, - overlays: { blocks: true, wishes: true }, - }); - } - } - - this.$refs.calendar.resetEventLoader(); - if (this.lastRange) - this.handleRange(this.lastRange); - }, - jumpToKw(kw) { - const num = parseInt(kw); - if (!num) - return; - - const date = luxon.DateTime.fromObject({ - weekYear: luxon.DateTime.now().setZone(this.config.timezone).weekYear, - weekNumber: num, - weekday: 1, - }, { zone: this.config.timezone }); - this.calendarDate = date.toISODate(); - }, - handleChangeDate(newDate) { - if (newDate && luxon.DateTime.isDateTime(newDate) && newDate.isValid) - this.calendarDate = newDate.toISODate(); - }, - handleChangeMode(newMode, newDate) { - if (!newMode) - return; - this.currentMode = newMode; - if (newDate && luxon.DateTime.isDateTime(newDate) && newDate.isValid) - this.calendarDate = newDate.toISODate(); - }, - updateCollision() - { - this.$api.call(ApiTempusConfig.updateCollision()) - .then(() => { - this.$refs.config.reload(); - 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)); - }, - getPromiseFunc(start, end) { - const hasRooms = this.rooms.length > 0; - const hasLektoren = this.lecturers.length > 0; - const hasStg = this.studiengaenge.length > 0; - - const filter = {}; - - if (hasRooms) - filter.ort = this.rooms.map(room => room.ort_kurzbz); - if (hasStg) - { - filter.stg = this.studiengaenge.map(({ studiengang_kz, semester, orgform_kurzbz }) => ({ - studiengang_kz, - semester, - orgform_kurzbz - })); - } - if (hasLektoren) - filter.uid = this.lecturers.map(lecture => lecture.uid); - - if (this.previewRole === 'lektor') - return [this.$api.call(ApiKalender.getPlanLecturer(start.toISODate(), end.toISODate()))]; - - if (this.previewRole === 'student') - return [this.$api.call(ApiKalender.getPlanStudent(start.toISODate(), end.toISODate()))]; - - return [this.$api.call(ApiKalender.getPlan(filter, start.toISODate(), end.toISODate()))]; - }, - 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"); - return null; - } - - return { - startDT, - endDT, - start_time: startDT.toFormat('yyyy-MM-dd HH:mm'), - end_time: endDT.toFormat('yyyy-MM-dd HH:mm'), - }; - }, - - _updateKalenderEvent(obj, startDT, endDT, start_time, end_time, onSuccess) - { - const origStart = luxon.DateTime.fromISO(obj.orig.isostart); - const origEnd = luxon.DateTime.fromISO(obj.orig.isoend); - - if (origStart.toMillis() === startDT.toMillis() && origEnd.toMillis() === endDT.toMillis()) - return; - - const updatedInfos = { - ort_kurzbz: this.ort_kurzbz ? this.ort_kurzbz : obj.orig.ort_kurzbz, - start_time, - end_time, - }; - - return this.$api.call(ApiKalender.updateKalenderEvent(obj.orig.kalender_id, updatedInfos)) - .then(() => { - if (onSuccess) - onSuccess(); - }); - }, - - resizeHandler(payload) { - 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"); - - const dates = this._parseDates(start, end); - - if (!dates) - return; - - return this._updateKalenderEvent(obj, dates.startDT, dates.endDT, dates.start_time, dates.end_time, () => { - this.$refs.calendar.resetEventLoader(); - this.$refs.coursepicker.reload(); - }); - }, - - dropHandler(payload) { - if (this.previewRole !== 'planer') //TODO (david) testzweck - return; - const { item, start, end } = payload; - if (!item?.length) - return alert("Keine Daten gedroppt"); - - const obj = item[0]; - if (!obj?.type) - return alert("Unbekannter Drop-Typ"); - - if (payload.targetKalenderId) - { - if (obj.type !== 'lehreinheit' || !obj.orig?.lehreinheit_id) - return alert("Nur Lehreinheiten können einem Termin hinzugefügt werden"); - - return this.$api.call( - ApiKalender.addToKalenderEvent(payload.targetKalenderId, obj.orig.lehreinheit_id) - ).then(() => { - this.$refs.calendar.resetEventLoader(); - this.bcc.postMessage('dropped'); - }); - } - - const dates = this._parseDates(start, end); - if (!dates) return; - - const { startDT, endDT, start_time, end_time } = dates; - - if (obj.type === 'reservierung') - { - this.reservierungPending = true; - this.$refs.reservierung.show(start_time, end_time); - } - else if (obj.type === 'lehreinheit') - { - if (obj.multiweek) - { - this.openMultiWeekPreview( - obj.orig.lehreinheit_id, - this.ort_kurzbz ? this.ort_kurzbz : obj.orig.ort_kurzbz, - start_time, - end_time - ); - return; - } - - 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 => { - this.$refs.calendar.resetEventLoader(); - this.$refs.coursepicker.reload(); - this.bcc.postMessage('dropped'); - }); - } - else if (obj.type === 'kalender') - { - return this._updateKalenderEvent(obj, startDT, endDT, start_time, end_time, () => - { - this.$refs.parking.unpark({ type: obj.type, id: obj.orig.kalender_id }); - this.$refs.calendar.resetEventLoader(); - this.bcc.postMessage('dropped'); - - }); - } - else - { - alert("Unbekannter Drop-Typ: " + obj.type); - } - }, - openMultiWeekPreview(lehreinheit_id, ort_kurzbz, start_time, end_time) { - this.multiWeekModal.lehreinheitId = lehreinheit_id; - this.multiWeekModal.ortKurzbz = ort_kurzbz; - this.multiWeekModal.startTime = start_time; - this.multiWeekModal.endTime = end_time; - this.multiWeekModal.show = true; - }, - closeMultiWeekModal() { - this.multiWeekModal.show = false; - }, - onMultiWeekConfirmed() { - this.$refs.calendar.resetEventLoader(); - this.$refs.coursepicker.reload(); - this.bcc.postMessage('dropped'); - }, - handleRange(range) { - if (!range?.start || !range?.end) - return; - - if (this.currentMode === 'week') - { - //Workaround because, updateRange is emitting 2 times - const startDay = range.start.startOf('day'); - const endDay = range.end.startOf('day'); - - const days = Math.round(endDay.diff(startDay, 'days').days) + 1; - if (days > 8) - return; - } - - this.lastRange = range; - - const key = `${range.start.toISODate()}_${range.end.toISODate()}_${this.currentMode}`; - - for (const lect of this.lecturers) - { - this.getOverlays(lect.uid, range, key); - } - - this.rebuildExtraBackgrounds(); - }, - - getOverlays(uid, range, rangeKey) - { - if (!this.overlayCache[uid]) - this.overlayCache[uid] = {}; - - let entry = this.overlayCache[uid][rangeKey]; - - if (entry?.loaded || entry?.loading) - return; - - entry = this.overlayCache[uid][rangeKey] = { - blocks: [], - wishes: [], - loading: true, - loaded: false - }; - - const promises = []; - const lect = this.lecturers.find(lecture => lecture.uid === uid); - - if (lect.overlays.wishes) - { - promises.push( - this.$api.call(ApiKalender.getLektorZeitwuensche(uid, range.start.toISODate(), range.end.toISODate())) - .then(result => { - entry.wishes = (result.data || []).map(zeitwunsch => ({ - class: `bg-lecturer-wish bg-uid-${uid} wish-w-${zeitwunsch.gewicht}`, - start: zeitwunsch.isostart, - end: zeitwunsch.isoend, - label: zeitwunsch.label - })); - }) - ); - } - - if (lect.overlays.blocks) - { - promises.push( - this.$api.call(ApiKalender.getLektorZeitsperren(uid, range.start.toISODate(), range.end.toISODate())) - .then(result => { - entry.blocks = (result.data || []).map(zeitsperre => ({ - class: `bg-lecturer-block bg-uid-${uid}`, - start: zeitsperre.isostart, - end: zeitsperre.isoend, - label: zeitsperre.label - })); - }) - ); - } - - Promise.allSettled(promises).then(() => { - entry.loading = false; - entry.loaded = true; - this.rebuildExtraBackgrounds(); - }); - }, - - rebuildExtraBackgrounds() { - if (!this.lastRange) - return; - - const key = `${this.lastRange.start.toISODate()}_` + `${this.lastRange.end.toISODate()}_` + `${this.currentMode}`; - let res = []; - - for (let lect of this.lecturers) - { - const entry = this.overlayCache[lect.uid]?.[key]; - if (!entry) - continue; - - if (lect.overlays.blocks) - res.push(...(entry.blocks || [])); - - if (lect.overlays.wishes) - res.push(...(entry.wishes || [])); - } - - this.extraBackgrounds = res; - }, - - removeLecturer(uid) - { - if (uid == null) - { - for (const lect of this.lecturers) - delete this.overlayCache[lect.uid]; - - this.lecturers = []; - this.$refs.calendar.resetEventLoader(); - } - else - { - this.lecturers = this.lecturers.filter(lecture => lecture.uid !== uid); - delete this.overlayCache[uid]; - this.$refs.calendar.resetEventLoader(); - } - }, - triggerSync() - { - this.$api.call(ApiKalender.sync()).then(this.$refs.calendar.resetEventLoader()) - }, - onEventHover(event) { - this.hoveredEvent = event; - }, - onEventUnhover(event) - { - if (this.hoveredEvent?.kalender_id === event?.kalender_id) { - this.hoveredEvent = null; - } - }, - parkHoveredEvent() - { - 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 }); - } - else - { - this.$refs.parking.park(null, { - type: 'kalender', - id: event.kalender_id, - orig: event - }); - } - }, - deleteHoveredEvent() { - const event = this.hoveredEvent; - if (!event?.kalender_id) - return; - - this.deleteEntry(event); - }, - clearHoveredEvent() { - this.hoveredEvent = null; - }, - focusSearchbar() { - this.$refs.searchbar?.$refs?.input.focus() - }, - }, - watch: { - lecturers: { - deep: true, - handler() { - this.rebuildExtraBackgrounds(); - } - }, - rooms() { - this.$refs.calendar.resetEventLoader(); - }, - studiengaenge: { - deep: true, - handler() { - this.$refs.calendar.resetEventLoader(); - } - }, - }, - mounted() { - this.reservierungPending = false; - 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(); - }, - async created() - { - await this.$api - .call(ApiRenderers.loadTempusRenderers()) - .then(res => res.data) - .then(data => { - for (let rendertype of Object.keys(data)) { - let modalTitle = null; - let modalContent = null; - let calendarEvent = null; - if (data[rendertype].modalTitle) - modalTitle = Vue.markRaw(Vue.defineAsyncComponent(() => import(data[rendertype].modalTitle))); - if (data[rendertype].modalContent) - modalContent = Vue.markRaw(Vue.defineAsyncComponent(() => import(data[rendertype].modalContent))); - if (data[rendertype].calendarEvent) - calendarEvent = Vue.markRaw(Vue.defineAsyncComponent(() => import(data[rendertype].calendarEvent))); - - if (data[rendertype].calendarEventStyles){ - var head = document.head; - if(!head.querySelector(`link[href="${data[rendertype].calendarEventStyles}"]`)){ - 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] = {} - } - this.renderers[rendertype].modalTitle = modalTitle; - this.renderers[rendertype].modalContent = modalContent; - this.renderers[rendertype].calendarEvent = calendarEvent; - } - }); - - 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: ` -
+ name: "Tempus", + components: { + CoreSearchbar, + VerticalSplit, + FhcCalendar, + FhcCoursepicker, + LectureSelection, + VerbandSelection, + RoomSelection, + ParkingSlot, + AppConfig, + AppMenu, + NavLanguage, + BsModal, + StvVerband, + StvStudiensemester, + Multiselect: primevue.multiselect, + FormInput, + Reservierung, + KeyboardShortcuts, + MultiWeekPlanModal, + }, + props: { + defaultSemester: String, + config: Object, + permissions: Object, + tempusRoot: String, + cisRoot: String, + activeAddons: String, // semicolon separated list of active addons + viewData: Object, + logoutUrl: String, + avatarUrl: String, + }, + directives: { + drop, + }, + provide() { + return { + cisRoot: this.cisRoot, + defaultSemester: this.defaultSemester, + currentSemester: this.defaultSemester, + renderers: Vue.computed(() => this.renderers), + appConfig: Vue.computed(() => this.appconfig), + contextMenuActions: useContextMenuActions({ + openRaumauswahl: (orig) => this.openRaumauswahl(orig), + openResourcesAssignmentModal: (orig) => + this.openResourcesAssignmentModal(orig), + openHistory: (orig) => this.openHistory(orig), + deleteEntry: (orig) => this.deleteEntry(orig), + syncToLecturer: (orig) => this.syncToLecturer(orig), + syncToStudent: (orig) => this.syncToStudent(orig), + }), + tableActions: { + deleteEntries: (origList) => this.deleteEntries(origList), + openRaumauswahl: (orig) => this.openRaumauswahl(orig), + }, + }; + }, + data() { + return { + appconfig: {}, + 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, + ort_kurzbz: null, + view: "room", + parkedKeys: new Set(), + lecturers: [], + studiengaenge: [], + rooms: [], + overlayCache: [], + extraBackgrounds: [], + lastRange: null, + stg: null, + semester: null, + studiensemester_kurzbz: null, + raumModal: { + show: false, + loading: false, + vorschlaege: [], + event: null, + }, + visibleStatusArray: {}, + visibleStatus: ["all"], + selectedStudiensemester: + this.studiensemester_kurzbz ?? this.defaultSemester, + calendarDate: luxon.DateTime.now() + .setZone(this.config.timezone) + .toISODate(), + historyEntries: [], + previewRole: "planer", + multiWeekModal: { + show: false, + lehreinheitId: null, + ortKurzbz: null, + startTime: null, + endTime: null, + }, + studiengaenge_all: [], + resourcesAssignmentModal: { + calendar: null, + availableResources: [], + filteredAvailableResources: [], + selectedAvailableResource: null, + assignedResources: [], + areFormButtonsDisplayed: false, + }, + }; + }, + computed: { + currentDay() { + return luxon.DateTime.now().setZone(this.config.timezone).toISODate(); + }, + visibleLecturerUids() { + if (!this.lecturers.length) return null; + return this.lecturers + .filter((lecture) => lecture.showEvents) + .map((lecture) => lecture.uid); + }, + visibleStatusOptions() { + return Object.entries(this.visibleStatusArray).map(([key, label]) => ({ + key, + label, + })); + }, + visibleStatusValue() { + if (this.visibleStatus.includes("all")) + return this.visibleStatusOptions.filter( + (visibleStatus) => visibleStatus.key === "all", + ); + return this.visibleStatus.map((status) => ({ + key: status, + label: this.visibleStatusArray[status], + })); + }, + 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 openRaumauswahl(orig) { + if (!orig?.kalender_id) return; + this.raumModal = orig; + + await this.$api + .call(ApiKalender.getRaumvorschlag(orig.kalender_id)) + .then((result) => { + this.raumVorschlaege = result.data ?? []; + this.$refs.raumModal.show(); + }); + }, + 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(); + }, + async deleteEntries(origList) { + let validList = (origList ?? []).filter((orig) => orig?.kalender_id); + if (!validList.length) return; + + await Promise.allSettled( + validList.map((orig) => this.deleteEntryCall(orig)), + ); + this.$refs.calendar.resetEventLoader(); + this.$refs.coursepicker.reload(); + }, + async deleteEntryCall(orig) { + await this.$api + .call(ApiKalender.deleteEntry(orig.kalender_id)) + .then(() => { + this.$refs.parking.unpark({ type: orig.type, id: orig.kalender_id }); + }); + }, + async openHistory(orig) { + if (!orig?.kalender_id) return; + await this.$api + .call(ApiKalender.getHistory(orig.kalender_id)) + .then((result) => { + this.historyEntries = result.data ?? []; + this.$refs.historyModel.show(); + }); + }, + syncToLecturer(orig) { + if (!orig?.kalender_id) return; + return this.$api + .call(ApiKalender.syncToLecturer(orig.kalender_id)) + .then(() => this.$refs.calendar.resetEventLoader()); + }, + syncToStudent(orig) { + if (!orig?.kalender_id) return; + return this.$api + .call(ApiKalender.syncToStudent(orig.kalender_id)) + .then(() => this.$refs.calendar.resetEventLoader()); + }, + async selectRaum(ort_kurzbz) { + const orig = this.raumModal; + await this.$api + .call( + ApiKalender.updateKalenderEvent(orig.kalender_id, { + ort_kurzbz, + start_time: orig.von, + end_time: orig.bis, + }), + ) + .then(() => this.$refs.raumModal.hide()); + this.$refs.calendar.resetEventLoader(); + }, + setOrt: function (data) { + this.ort_kurzbz = data.ort_kurzbz; + this.rooms = [{ ort_kurzbz: data.ort_kurzbz }]; + }, + onSelectVerbandAndClose(payload) { + console.log(payload); + this.onSelectVerband(payload); + bootstrap.Offcanvas.getOrCreateInstance(this.$refs.verbandMenu).hide(); + }, + + onSelectVerband({ link, studiengang_kz, semester, orgform_kurzbz, name }) { + if (orgform_kurzbz) { + semester = null; + } else if (typeof link === "string") { + [studiengang_kz, semester] = link.split("/"); + } + + let exists = this.studiengaenge.some( + (stg) => + stg.studiengang_kz == studiengang_kz && + stg.semester == semester && + stg.orgform_kurzbz === orgform_kurzbz, + ); + + if (!exists) { + this.studiengaenge = [ + ...this.studiengaenge, + { studiengang_kz, semester, orgform_kurzbz, name }, + ]; + } + }, + setEmp: function (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 }, + }, + ]; + + this.$refs.calendar.resetEventLoader(); + if (this.lastRange) this.handleRange(this.lastRange); + }, + addToFilter: function (filter, type) { + if (type === "ort") { + const ort_kurzbz = filter.ort_kurzbz; + if (!this.rooms.some((room) => room.ort_kurzbz === ort_kurzbz)) { + this.rooms.push({ ort_kurzbz }); + } + } else if (type === "mitarbeiter") { + const uid = filter.uid; + const label = filter.name; + if (!this.lecturers.some((l) => l.uid === uid)) { + this.lecturers.push({ + uid, + label, + showEvents: true, + overlays: { blocks: true, wishes: true }, + }); + } + } + + this.$refs.calendar.resetEventLoader(); + if (this.lastRange) this.handleRange(this.lastRange); + }, + jumpToKw(kw) { + const num = parseInt(kw); + if (!num) return; + + const date = luxon.DateTime.fromObject( + { + weekYear: luxon.DateTime.now().setZone(this.config.timezone).weekYear, + weekNumber: num, + weekday: 1, + }, + { zone: this.config.timezone }, + ); + this.calendarDate = date.toISODate(); + }, + handleChangeDate(newDate) { + if (newDate && luxon.DateTime.isDateTime(newDate) && newDate.isValid) + this.calendarDate = newDate.toISODate(); + }, + handleChangeMode(newMode, newDate) { + if (!newMode) return; + this.currentMode = newMode; + if (newDate && luxon.DateTime.isDateTime(newDate) && newDate.isValid) + this.calendarDate = newDate.toISODate(); + }, + updateCollision() { + this.$api + .call(ApiTempusConfig.updateCollision()) + .then(() => { + this.$refs.config.reload(); + 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)); + }, + getPromiseFunc(start, end) { + const hasRooms = this.rooms.length > 0; + const hasLektoren = this.lecturers.length > 0; + const hasStg = this.studiengaenge.length > 0; + + const filter = {}; + + if (hasRooms) filter.ort = this.rooms.map((room) => room.ort_kurzbz); + if (hasStg) { + filter.stg = this.studiengaenge.map( + ({ studiengang_kz, semester, orgform_kurzbz }) => ({ + studiengang_kz, + semester, + orgform_kurzbz, + }), + ); + } + if (hasLektoren) + filter.uid = this.lecturers.map((lecture) => lecture.uid); + + if (this.previewRole === "lektor") + return [ + this.$api.call( + ApiKalender.getPlanLecturer(start.toISODate(), end.toISODate()), + ), + ]; + + if (this.previewRole === "student") + return [ + this.$api.call( + ApiKalender.getPlanStudent(start.toISODate(), end.toISODate()), + ), + ]; + + return [ + this.$api.call( + ApiKalender.getPlan(filter, start.toISODate(), end.toISODate()), + ), + ]; + }, + 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"); + return null; + } + + return { + startDT, + endDT, + start_time: startDT.toFormat("yyyy-MM-dd HH:mm"), + end_time: endDT.toFormat("yyyy-MM-dd HH:mm"), + }; + }, + + _updateKalenderEvent(obj, startDT, endDT, start_time, end_time, onSuccess) { + const origStart = luxon.DateTime.fromISO(obj.orig.isostart); + const origEnd = luxon.DateTime.fromISO(obj.orig.isoend); + + if ( + origStart.toMillis() === startDT.toMillis() && + origEnd.toMillis() === endDT.toMillis() + ) + return; + + const updatedInfos = { + ort_kurzbz: this.ort_kurzbz ? this.ort_kurzbz : obj.orig.ort_kurzbz, + start_time, + end_time, + }; + + return this.$api + .call( + ApiKalender.updateKalenderEvent(obj.orig.kalender_id, updatedInfos), + ) + .then(() => { + if (onSuccess) onSuccess(); + }); + }, + + resizeHandler(payload) { + 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"); + + const dates = this._parseDates(start, end); + + if (!dates) return; + + return this._updateKalenderEvent( + obj, + dates.startDT, + dates.endDT, + dates.start_time, + dates.end_time, + () => { + this.$refs.calendar.resetEventLoader(); + this.$refs.coursepicker.reload(); + }, + ); + }, + + dropHandler(payload) { + if (this.previewRole !== "planer") + //TODO (david) testzweck + return; + const { item, start, end } = payload; + if (!item?.length) return alert("Keine Daten gedroppt"); + + const obj = item[0]; + if (!obj?.type) return alert("Unbekannter Drop-Typ"); + + if (payload.targetKalenderId) { + if (obj.type !== "lehreinheit" || !obj.orig?.lehreinheit_id) + return alert( + "Nur Lehreinheiten können einem Termin hinzugefügt werden", + ); + + return this.$api + .call( + ApiKalender.addToKalenderEvent( + payload.targetKalenderId, + obj.orig.lehreinheit_id, + ), + ) + .then(() => { + this.$refs.calendar.resetEventLoader(); + this.bcc.postMessage("dropped"); + }); + } + + const dates = this._parseDates(start, end); + if (!dates) return; + + const { startDT, endDT, start_time, end_time } = dates; + + if (obj.type === "reservierung") { + this.reservierungPending = true; + this.$refs.reservierung.show(start_time, end_time); + } else if (obj.type === "lehreinheit") { + if (obj.multiweek) { + this.openMultiWeekPreview( + obj.orig.lehreinheit_id, + this.ort_kurzbz ? this.ort_kurzbz : obj.orig.ort_kurzbz, + start_time, + end_time, + ); + return; + } + + 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) => { + this.$refs.calendar.resetEventLoader(); + this.$refs.coursepicker.reload(); + this.bcc.postMessage("dropped"); + }); + } else if (obj.type === "kalender") { + return this._updateKalenderEvent( + obj, + startDT, + endDT, + start_time, + end_time, + () => { + this.$refs.parking.unpark({ + type: obj.type, + id: obj.orig.kalender_id, + }); + this.$refs.calendar.resetEventLoader(); + this.bcc.postMessage("dropped"); + }, + ); + } else { + alert("Unbekannter Drop-Typ: " + obj.type); + } + }, + openMultiWeekPreview(lehreinheit_id, ort_kurzbz, start_time, end_time) { + this.multiWeekModal.lehreinheitId = lehreinheit_id; + this.multiWeekModal.ortKurzbz = ort_kurzbz; + this.multiWeekModal.startTime = start_time; + this.multiWeekModal.endTime = end_time; + this.multiWeekModal.show = true; + }, + closeMultiWeekModal() { + this.multiWeekModal.show = false; + }, + onMultiWeekConfirmed() { + this.$refs.calendar.resetEventLoader(); + this.$refs.coursepicker.reload(); + this.bcc.postMessage("dropped"); + }, + handleRange(range) { + if (!range?.start || !range?.end) return; + + if (this.currentMode === "week") { + //Workaround because, updateRange is emitting 2 times + const startDay = range.start.startOf("day"); + const endDay = range.end.startOf("day"); + + const days = Math.round(endDay.diff(startDay, "days").days) + 1; + if (days > 8) return; + } + + this.lastRange = range; + + const key = `${range.start.toISODate()}_${range.end.toISODate()}_${this.currentMode}`; + + for (const lect of this.lecturers) { + this.getOverlays(lect.uid, range, key); + } + + this.rebuildExtraBackgrounds(); + }, + + getOverlays(uid, range, rangeKey) { + if (!this.overlayCache[uid]) this.overlayCache[uid] = {}; + + let entry = this.overlayCache[uid][rangeKey]; + + if (entry?.loaded || entry?.loading) return; + + entry = this.overlayCache[uid][rangeKey] = { + blocks: [], + wishes: [], + loading: true, + loaded: false, + }; + + const promises = []; + const lect = this.lecturers.find((lecture) => lecture.uid === uid); + + if (lect.overlays.wishes) { + promises.push( + this.$api + .call( + ApiKalender.getLektorZeitwuensche( + uid, + range.start.toISODate(), + range.end.toISODate(), + ), + ) + .then((result) => { + entry.wishes = (result.data || []).map((zeitwunsch) => ({ + class: `bg-lecturer-wish bg-uid-${uid} wish-w-${zeitwunsch.gewicht}`, + start: zeitwunsch.isostart, + end: zeitwunsch.isoend, + label: zeitwunsch.label, + })); + }), + ); + } + + if (lect.overlays.blocks) { + promises.push( + this.$api + .call( + ApiKalender.getLektorZeitsperren( + uid, + range.start.toISODate(), + range.end.toISODate(), + ), + ) + .then((result) => { + entry.blocks = (result.data || []).map((zeitsperre) => ({ + class: `bg-lecturer-block bg-uid-${uid}`, + start: zeitsperre.isostart, + end: zeitsperre.isoend, + label: zeitsperre.label, + })); + }), + ); + } + + Promise.allSettled(promises).then(() => { + entry.loading = false; + entry.loaded = true; + this.rebuildExtraBackgrounds(); + }); + }, + + rebuildExtraBackgrounds() { + if (!this.lastRange) return; + + const key = + `${this.lastRange.start.toISODate()}_` + + `${this.lastRange.end.toISODate()}_` + + `${this.currentMode}`; + let res = []; + + for (let lect of this.lecturers) { + const entry = this.overlayCache[lect.uid]?.[key]; + if (!entry) continue; + + if (lect.overlays.blocks) res.push(...(entry.blocks || [])); + + if (lect.overlays.wishes) res.push(...(entry.wishes || [])); + } + + 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]; + + this.lecturers = []; + this.$refs.calendar.resetEventLoader(); + } else { + this.lecturers = this.lecturers.filter( + (lecture) => lecture.uid !== uid, + ); + delete this.overlayCache[uid]; + this.$refs.calendar.resetEventLoader(); + } + }, + triggerSync() { + this.$api + .call(ApiKalender.sync()) + .then(this.$refs.calendar.resetEventLoader()); + }, + onEventHover(event) { + this.hoveredEvent = event; + }, + onEventUnhover(event) { + if (this.hoveredEvent?.kalender_id === event?.kalender_id) { + this.hoveredEvent = null; + } + }, + parkHoveredEvent() { + 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 }); + } else { + this.$refs.parking.park(null, { + type: "kalender", + id: event.kalender_id, + orig: event, + }); + } + }, + deleteHoveredEvent() { + const event = this.hoveredEvent; + if (!event?.kalender_id) return; + + this.deleteEntry(event); + }, + clearHoveredEvent() { + this.hoveredEvent = null; + }, + focusSearchbar() { + this.$refs.searchbar?.$refs?.input.focus(); + }, + }, + watch: { + lecturers: { + deep: true, + handler() { + this.rebuildExtraBackgrounds(); + }, + }, + rooms() { + this.$refs.calendar.resetEventLoader(); + }, + studiengaenge: { + deep: true, + handler() { + 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; + }, + }, + mounted() { + this.reservierungPending = false; + 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(); + }, + async created() { + await this.$api + .call(ApiRenderers.loadTempusRenderers()) + .then((res) => res.data) + .then((data) => { + for (let rendertype of Object.keys(data)) { + let modalTitle = null; + let modalContent = null; + let calendarEvent = null; + if (data[rendertype].modalTitle) + modalTitle = Vue.markRaw( + Vue.defineAsyncComponent( + () => import(data[rendertype].modalTitle), + ), + ); + if (data[rendertype].modalContent) + modalContent = Vue.markRaw( + Vue.defineAsyncComponent( + () => import(data[rendertype].modalContent), + ), + ); + if (data[rendertype].calendarEvent) + calendarEvent = Vue.markRaw( + Vue.defineAsyncComponent( + () => import(data[rendertype].calendarEvent), + ), + ); + + if (data[rendertype].calendarEventStyles) { + var head = document.head; + if ( + !head.querySelector( + `link[href="${data[rendertype].calendarEventStyles}"]`, + ) + ) { + 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] = {}; + } + this.renderers[rendertype].modalTitle = modalTitle; + this.renderers[rendertype].modalContent = modalContent; + this.renderers[rendertype].calendarEvent = calendarEvent; + } + }); + + 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: ` +