From 8f9f447acf707a422a0cb341ecbc2e260b230a33 Mon Sep 17 00:00:00 2001 From: ma0048 Date: Thu, 18 Dec 2025 11:08:40 +0100 Subject: [PATCH] cis4 raumreservierung beta version --- application/config/Events.php | 9 + .../api/frontend/v1/calendar/RoomPlan.php | 232 +++++++++++++++++ application/libraries/StundenplanLib.php | 239 +++++++++++++++++ .../models/ressource/Reservierung_model.php | 7 +- public/css/Cis4/CoreCalendarEvents.css | 14 + public/js/api/factory/calendar/roomPlan.js | 44 ++++ public/js/components/Calendar/Base.js | 17 +- public/js/components/Calendar/Base/Grid.js | 34 ++- public/js/components/Calendar/LvPlan.js | 48 +++- .../js/components/Calendar/Mode/Day/View.js | 35 ++- public/js/components/Calendar/Mode/Week.js | 43 ++- .../js/components/Cis/Mylv/RoomInformation.js | 108 ++++++++ .../Renderer/Reservierungen/calendarEvent.js | 25 +- .../Cis/Renderer/Slot/roomModalContent.js | 246 ++++++++++++++++++ .../Cis/Renderer/Slot/roomModalTitle.js | 3 + public/js/composables/EventLoader.js | 24 +- system/phrasesupdate.php | 40 +++ 17 files changed, 1142 insertions(+), 26 deletions(-) create mode 100644 application/controllers/api/frontend/v1/calendar/RoomPlan.php create mode 100644 public/js/api/factory/calendar/roomPlan.js create mode 100644 public/js/components/Cis/Renderer/Slot/roomModalContent.js create mode 100644 public/js/components/Cis/Renderer/Slot/roomModalTitle.js diff --git a/application/config/Events.php b/application/config/Events.php index 3e0a5248f..bf9f317e3 100644 --- a/application/config/Events.php +++ b/application/config/Events.php @@ -23,6 +23,15 @@ Events::on('loadRenderers', function ($renderers) { ); }); +Events::on('loadRenderers', function ($renderers) { + $fhc_core_renderers =& $renderers(); + $fhc_core_renderers["slot_room"] = array( + 'modalTitle' => APP_ROOT.'public/js/components/Cis/Renderer/Slot/roomModalTitle.js', + 'modalContent' => APP_ROOT.'public/js/components/Cis/Renderer/Slot/roomModalContent.js', + 'calendarEventStyles' => APP_ROOT.'public/css/Cis4/CoreCalendarEvents.css' + ); +}); + Events::on('loadRenderers', function ($renderers) { $fhc_core_renderers =& $renderers(); $fhc_core_renderers["ferien"] = array( diff --git a/application/controllers/api/frontend/v1/calendar/RoomPlan.php b/application/controllers/api/frontend/v1/calendar/RoomPlan.php new file mode 100644 index 000000000..489d83b53 --- /dev/null +++ b/application/controllers/api/frontend/v1/calendar/RoomPlan.php @@ -0,0 +1,232 @@ +. + */ + +if (! defined('BASEPATH')) exit('No direct script access allowed'); + +class RoomPlan extends FHCAPI_Controller +{ + + /** + * Object initialization + */ + public function __construct() + { + parent::__construct([ + 'addRoomReservation' => self::PERM_LOGGED, + 'deleteRoomReservation' => self::PERM_LOGGED, + 'getRoomCreationInfo' => self::PERM_LOGGED, + 'getGruppen' => self::PERM_LOGGED, + 'getLektor' => self::PERM_LOGGED, + 'getReservableMap' => self::PERM_LOGGED, + ]); + + $this->load->library('LogLib'); + $this->loglib->setConfigs(array( + 'classIndex' => 5, + 'functionIndex' => 5, + 'lineIndex' => 4, + 'dbLogType' => 'API', + 'dbExecuteUser' => 'RESTful API' + )); + + $this->load->library('form_validation'); + $this->load->library('PermissionLib'); + $this->load->library('StundenplanLib'); + + $this->loadPhrases(['ui']); + } + + //------------------------------------------------------------------------------------------------------------------ + // Public methods + + + + public function addRoomReservation() + { + $this->form_validation->set_rules('selectedStart', "Start", "required"); + $this->form_validation->set_rules('selectedEnd', "End", "required"); + $this->form_validation->set_rules('title', "Title", "required|max_length[10]"); + $this->form_validation->set_rules('beschreibung', "Beschreibung", "required|max_length[32]"); + $this->form_validation->set_rules('ort_kurzbz', "Ort", "required|max_length[16]"); + $this->form_validation->set_rules('studiengang', 'Studiengang', 'numeric'); + $this->form_validation->set_rules('semester', 'Semester', 'integer|greater_than_equal_to[0]'); + $this->form_validation->set_rules('verband', 'Verband', 'trim'); + $this->form_validation->set_rules('gruppe', 'Gruppe', 'trim'); + $this->form_validation->set_rules('spezialgruppe', 'Spezialgruppe', 'max_length[32]'); + $this->form_validation->set_rules('lektoren', 'Lektoren'); + + if (!$this->form_validation->run()) + $this->terminateWithValidationErrors($this->form_validation->error_array()); + + $start = $this->input->post('selectedStart'); + $end = $this->input->post('selectedEnd'); + $title = $this->input->post('title'); + $beschreibung = $this->input->post('beschreibung'); + $ort_kurzbz = $this->input->post('ort_kurzbz'); + + $studiengang_kz = $this->input->post('studiengang'); + $semester = $this->input->post('semester'); + $verband = $this->input->post('verband'); + $gruppe = $this->input->post('gruppe'); + $spezialgruppe = $this->input->post('spezialgruppe'); + $lektoren = $this->input->post('lektoren'); + + + $result = $this->stundenplanlib->addReservation($start, $end, $title, $beschreibung, $ort_kurzbz, $lektoren, $studiengang_kz, $semester, $verband, $gruppe, $spezialgruppe); + + if (isError($result)) + $this->terminateWithError($result); + + $this->terminateWithSuccess($result); + } + + public function deleteRoomReservation() + { + $reservierung_id = $this->input->post('reservierung_id'); + + $result = $this->stundenplanlib->deleteReservation($reservierung_id); + + if (isError($result)) + $this->terminateWithError($result); + + $this->terminateWithSuccess($result); + } + + public function getRoomCreationInfo() + { + $return_array = array('berechtigt' => false, 'studiengaenge' => []); + if (!$this->permissionlib->isBerechtigt('lehre/reservierung')) + $this->terminateWithSuccess($return_array); + + $stg_berechtigungen = $this->permissionlib->getSTG_isEntitledFor('lehre/reservierung'); + if (isEmptyArray($stg_berechtigungen)) + $this->terminateWithSuccess($return_array); + + $this->load->model('organisation/Studiengang_model', 'StudiengangModel'); + $this->StudiengangModel->addSelect('studiengang_kz, UPPER(CONCAT(typ, kurzbz)) as kuerzel, kurzbzlang'); + $this->StudiengangModel->addOrder('typ, kurzbz'); + $this->StudiengangModel->db->where_in('studiengang_kz', $stg_berechtigungen); + $studiengaenge = $this->StudiengangModel->loadWhere(array('aktiv' => true)); + + if (isError($studiengaenge)) + $this->terminateWithError($studiengaenge); + + $return_array['studiengaenge'] = hasData($studiengaenge) ? getData($studiengaenge) : []; + $return_array['berechtigt'] = true; + + $this->terminateWithSuccess($return_array); + } + + public function getGruppen() + { + $query = $this->input->get('query'); + if (is_null($query)) + $this->terminateWithError($this->p->t('ui', 'ungueltigeParameter'), self::ERROR_TYPE_GENERAL); + + $stg_berechtigungen = $this->permissionlib->getSTG_isEntitledFor('lehre/reservierung'); + + if (isEmptyArray($stg_berechtigungen)) + $this->terminateWithSuccess([]); + + $this->load->model('organisation/gruppe_model', 'GruppeModel'); + + $query_words = explode(' ', urldecode($query)); + + $this->GruppeModel->addOrder('gruppe_kurzbz'); + $this->GruppeModel->db->group_start(); + foreach ($query_words as $word) + { + $this->GruppeModel->db->group_start(); + $this->GruppeModel->db->where('gruppe_kurzbz ILIKE', "%" . $word . "%"); + $this->GruppeModel->db->or_where('bezeichnung ILIKE', "%" . $word . "%"); + $this->GruppeModel->db->or_where('beschreibung ILIKE', "%" . $word . "%"); + $this->GruppeModel->db->or_where('orgform_kurzbz ILIKE', "%" . $word . "%"); + + if (is_numeric($word)) + { + $this->GruppeModel->db->or_where('studiengang_kz', $word); + } + $this->GruppeModel->db->group_end(); + } + $this->GruppeModel->db->group_end(); + $this->GruppeModel->db->where_in('studiengang_kz', $stg_berechtigungen); + $gruppen = $this->GruppeModel->loadWhere(array('sichtbar' => true, 'lehre' => true)); + if (isError($gruppen)) + $this->terminateWithError($gruppen); + + $this->terminateWithSuccess(hasData($gruppen) ? getData($gruppen) : []); + } + + public function getLektor() + { + + $query = $this->input->get('query'); + if (is_null($query)) + $this->terminateWithError($this->p->t('ui', 'ungueltigeParameter'), self::ERROR_TYPE_GENERAL); + + $stg_berechtigungen = $this->permissionlib->getSTG_isEntitledFor('lehre/reservierung'); + + if (isEmptyArray($stg_berechtigungen)) + $this->terminateWithSuccess([]); + + $this->load->model('ressource/Mitarbeiter_model', 'MitarbeiterModel'); + + $query_words = explode(' ', urldecode($query)); + + $this->MitarbeiterModel->addSelect('uid, person_id, vorname, nachname'); + $this->MitarbeiterModel->addJoin('public.tbl_benutzer', 'uid = mitarbeiter_uid'); + $this->MitarbeiterModel->addJoin('public.tbl_person', 'person_id'); + $this->MitarbeiterModel->db->where('public.tbl_benutzer.aktiv', true); + $this->MitarbeiterModel->db->group_start(); + foreach ($query_words as $word) + { + $this->MitarbeiterModel->db->group_start(); + $this->MitarbeiterModel->db->where('tbl_person.vorname ILIKE', "%" . $word . "%"); + $this->MitarbeiterModel->db->or_where('tbl_person.nachname ILIKE', "%" . $word . "%"); + $this->MitarbeiterModel->db->or_where('uid ILIKE', "%" . $word . "%"); + $this->MitarbeiterModel->db->group_end(); + } + $this->MitarbeiterModel->db->group_end(); + + $this->MitarbeiterModel->addOrder('nachname'); + $this->MitarbeiterModel->addOrder('vorname'); + $mitarbeiter = $this->MitarbeiterModel->load(); + if (isError($mitarbeiter)) + $this->terminateWithError($mitarbeiter); + + $this->terminateWithSuccess(hasData($mitarbeiter) ? getData($mitarbeiter) : []); + } + + public function getReservableMap($ort_kurzbz = null) + { + $this->form_validation->set_rules('start_date', "StartDate", "required"); + $this->form_validation->set_rules('end_date', "EndDate", "required"); + + if (!$this->form_validation->run()) + $this->terminateWithValidationErrors($this->form_validation->error_array()); + + // storing the post parameter in local variables + $start_date = $this->input->post('start_date', true); + $end_date = $this->input->post('end_date', true); + + $result = $this->stundenplanlib->getReservableMap($ort_kurzbz, $start_date, $end_date); + + $this->terminateWithSuccess(array('reservierbarMap' => hasData($result) ? getData($result) : [])); + } + +} diff --git a/application/libraries/StundenplanLib.php b/application/libraries/StundenplanLib.php index 7ed64da2c..7ece4fa65 100644 --- a/application/libraries/StundenplanLib.php +++ b/application/libraries/StundenplanLib.php @@ -368,6 +368,32 @@ class StundenplanLib $item->gruppe = $gruppe_obj_array; $item->lektor = $lektor_obj_array; + $this->_ci->load->library('PermissionLib'); + $berechtigt_begrenzt = $this->_ci->permissionlib->isBerechtigt('lehre/reservierung:begrenzt', 'sui'); + + $now = time(); + $res_lektor_start = $this->jump_day($now, RES_TAGE_LEKTOR_MIN - 1); + $res_lektor_ende = mktime(0, 0, 0, date('m', $now), date('d', $now) + RES_TAGE_LEKTOR_BIS, date('Y', $now)); + + $start_date = is_numeric($item->beginn) ? $item->beginn : strtotime($item->beginn); + if (!date('w', $start_date)) { + $start_date = $this->jump_day($start_date, 1); + } + + $start_date_str = date('Y-m-d', $start_date); + $res_lektor_start_str = date('Y-m-d', $res_lektor_start); + $res_lektor_ende_str = date('Y-m-d', $res_lektor_ende); + + $show_delete = (($berechtigt_begrenzt && ($item->insertvon == getAuthUID() || in_array(getAuthUID(), $item->uids))) && + $start_date_str >= $res_lektor_start_str && + $start_date_str <= $res_lektor_ende_str + ); + + if ($show_delete) + $item->deletable = true; + else + $item->deletable = false; + } } @@ -445,6 +471,219 @@ class StundenplanLib return success($ferienEventsFlattened); } + public function addReservation($start, $end, $title, $beschreibung, $ort_kurzbz, $lektoren = null, $studiengang = null, $semester = null, $verband = null, $gruppe = null, $spezialgruppe = null) + { + $this->_ci =& get_instance(); + $this->_ci->load->model('ressource/Stunde_model', 'StundeModel'); + $this->_ci->load->model('ressource/Reservierung_model', 'ReservierungModel'); + $this->_ci->load->model('ressource/stundenplandev_model', 'StundenplandevModel'); + $this->_ci->load->model('ressource/stundenplan_model', 'StundenplanModel'); + $this->_ci->load->library('PermissionLib'); + + $startTime = new DateTime($start); + $endTime = new DateTime($end); + + $stunden = $this->_ci->StundeModel->loadWhere(array( + 'beginn <' => $endTime->format('H:i:s'), + 'ende >' => $startTime->format('H:i:s') + )); + + if (!hasData($stunden)) + { + return error("Keine Stunden vorhanden"); + } + + $stunden = array_column(getData($stunden), 'stunde'); + + $this->_ci->StundenplandevModel->db->select('1'); + $this->_ci->StundenplandevModel->db->where('datum', $startTime->format('Y-m-d')); + $this->_ci->StundenplandevModel->db->where('ort_kurzbz', $ort_kurzbz); + $this->_ci->StundenplandevModel->db->where_in('stunde', $stunden); + $stundenplandev_belegung = $this->_ci->StundenplandevModel->load(); + + $this->_ci->StundenplanModel->db->select('1'); + $this->_ci->StundenplanModel->db->where('ort_kurzbz', $ort_kurzbz); + $this->_ci->StundenplanModel->db->where('datum', $startTime->format('Y-m-d')); + $this->_ci->StundenplanModel->db->where_in('stunde', $stunden); + $stundenplan_belegung = $this->_ci->StundenplanModel->load(); + + if ((hasData($stundenplandev_belegung) || hasData($stundenplan_belegung)) + && !$this->_ci->permissionlib->isBerechtigt('lehre/reservierungAdvanced')) + return error ('lvplan/bereitsReserviert'); + + $this->_ci->ReservierungModel->addSelect('stunde'); + $reservation_hours = $this->_ci->ReservierungModel->loadWhere(array('datum' => $startTime->format('Y-m-d'), 'ort_kurzbz' => $ort_kurzbz)); + + + if (isError($reservation_hours)) + return $reservation_hours; + + $reservation_hours = hasData($reservation_hours) ? array_column(getData($reservation_hours), 'stunde') : array(); + + if (!empty(array_intersect($stunden, $reservation_hours)) + && !$this->_ci->permissionlib->isBerechtigt('lehre/reservierungAdvanced')) + return error("lvplan/bereitsReserviert"); + + + if (!empty($lektoren)) + { + foreach ($lektoren as $lektor) + { + $insert = array('ort_kurzbz' => $ort_kurzbz, + 'datum' => $startTime->format('Y-m-d'), + 'titel' => $title, + 'studiengang_kz' => is_null($studiengang) ? 0 : $studiengang, + 'beschreibung' => $beschreibung, + 'insertvon' => getAuthUID(), + 'uid' => $lektor, + 'semester' => is_null($semester) ? null : $semester, + 'verband' => is_null($verband) ? null : $verband, + 'gruppe' => is_null($gruppe) ? null : $gruppe, + 'gruppe_kurzbz' => is_null($spezialgruppe) ? null : $spezialgruppe, + ); + + foreach ($stunden as $stunde) + { + $insert['stunde'] = $stunde; + $check_insert = $this->_ci->ReservierungModel->insert($insert); + if (isError($check_insert)) + return $check_insert; + } + } + } + else + { + foreach ($stunden as $stunde) + { + $check_insert = $this->_ci->ReservierungModel->insert(array( + 'ort_kurzbz' => $ort_kurzbz, + 'uid' => getAuthUID(), + 'stunde' => $stunde, + 'datum' => $startTime->format('Y-m-d'), + 'titel' => $title, + 'studiengang_kz' => is_null($studiengang) ? 0 : $studiengang, + 'beschreibung' => $beschreibung, + 'insertvon' => getAuthUID() + )); + if (isError($check_insert)) + return $check_insert; + } + } + + return success("Erfolgreich"); + } + + public function deleteReservation($reservierung_id) + { + $this->_ci =& get_instance(); + $this->_ci->load->model('ressource/Reservierung_model', 'ReservierungModel'); + $this->_ci->load->model('ressource/Stunde_model', 'StundeModel'); + $this->_ci->load->library('PermissionLib'); + + + $this->_ci->ReservierungModel->db->where_in('reservierung_id', $reservierung_id); + $reservation = $this->_ci->ReservierungModel->load(); + if (isError($reservation)) + return $reservation; + + if (!hasData($reservation)) + return error("Reservierungen nicht gefunden"); + + $reservations = getData($reservation); + + $today = new DateTime(); + foreach ($reservations as $reservierung) + { + if ($today->format('Y-m-d') > $reservierung->datum) + return error("Vergangene Reservierungen können nicht gelöscht werden"); + + if (($this->_ci->permissionlib->isBerechtigt('lehre/reservierung:begrenzt')) && ($reservierung->insertvon == getAuthUID() || $reservierung->uid === getAuthUID())) + { + $delete_result = $this->_ci->ReservierungModel->delete($reservierung->reservierung_id); + + if (isError($delete_result)) + return $delete_result; + } + } + return success("Erfolgreich"); + } + + + public function getReservableMap($ort_kurzbz, $start_date, $end_date) + { + $this->_ci =& get_instance(); + $this->_ci->load->model('ressource/Ort_model', 'OrtModel'); + $this->_ci->load->library('PermissionLib'); + + $berechtigt_begrenzt = $this->_ci->permissionlib->isBerechtigt('lehre/reservierung:begrenzt', 'suid'); + $berechtigt_erweitert = $this->_ci->permissionlib->isBerechtigt('lehre/reservierung', 'suid'); + + $ort_data = $this->_ci->OrtModel->load($ort_kurzbz); + if (isError($ort_data) || !hasData($ort_data)) + return []; + + $ort_data = getData($ort_data)[0]; + + if (!$ort_data->reservieren) + return []; + + if (!$berechtigt_begrenzt && !$berechtigt_erweitert) + return []; + + $start_ts = is_numeric($start_date) ? (int)$start_date : strtotime($start_date); + $end_ts = is_numeric($end_date) ? (int)$end_date : strtotime($end_date); + + if (!$start_ts || !$end_ts) + return []; + + if ($end_ts < $start_ts) + { + $tmp = $start_ts; + $start_ts = $end_ts; + $end_ts = $tmp; + } + + $now = time(); + $tage_min = defined('RES_TAGE_LEKTOR_MIN') ? (int)RES_TAGE_LEKTOR_MIN : 0; + $tage_bis = defined('RES_TAGE_LEKTOR_BIS') ? (int)RES_TAGE_LEKTOR_BIS : 0; + + $datum_res_lektor_start = $this->jump_day($now, $tage_min - 1); + $datum_res_lektor_ende = $this->jump_day($now, $tage_bis); + + $start_ymd_allowed = date('Y-m-d', $datum_res_lektor_start); + $end_ymd_allowed = date('Y-m-d', $datum_res_lektor_ende); + + $result = []; + + $current = strtotime(date('Y-m-d', $start_ts) . ' 00:00:00'); + $end_day = strtotime(date('Y-m-d', $end_ts) . ' 00:00:00'); + + while ($current <= $end_day) + { + $ymd = date('Y-m-d', $current); + + if ((int)date('w', $current) === 0) + { + $result[$ymd] = false; + $current = $this->jump_day($current, 1); + continue; + } + + $result[$ymd] = ($ymd >= $start_ymd_allowed && $ymd <= $end_ymd_allowed) ? true : false; + + $current = $this->jump_day($current, 1); + } + + return success($result); + } + + private function jump_day($timestamp, $days) + { + $days = (int)$days; + $prefix = ($days >= 0 ? '+' : ''); + return strtotime($prefix . $days . ' days', $timestamp); + } + // start of the private functions ######################################################################################################## // function used to sort an array of studiensemester strings diff --git a/application/models/ressource/Reservierung_model.php b/application/models/ressource/Reservierung_model.php index 0c391ea20..6d9bed9b4 100644 --- a/application/models/ressource/Reservierung_model.php +++ b/application/models/ressource/Reservierung_model.php @@ -50,11 +50,12 @@ class Reservierung_model extends DB_Model $query_result= $this->execReadOnlyQuery(" SELECT - 'reservierung' as type, beginn, ende, datum, + DISTINCT(insertvon), + 'reservierung' as type, beginn, ende, datum, array_agg(DISTINCT reservierung_id) AS reservierung_id, COALESCE(titel, beschreibung) as topic, array_agg(DISTINCT mitarbeiter_kurzbz) as lektor, array_agg(DISTINCT (gruppe,verband,semester,studiengang_kz,gruppen_kuerzel)) as gruppe, - + array_agg(DISTINCT(uid)) as uids, ort_kurzbz, 'FFFFFF' as farbe FROM @@ -62,7 +63,7 @@ class Reservierung_model extends DB_Model ". $subquery ." ) AS subquery - GROUP BY datum, beginn, ende, ort_kurzbz, titel, beschreibung + GROUP BY datum, beginn, ende, ort_kurzbz, titel, beschreibung, insertvon ORDER BY datum, beginn ", is_null($ort_kurzbz) ?[getAuthUID(), getAuthUID(),$start_date,$end_date]: [$ort_kurzbz, $start_date, $end_date]); diff --git a/public/css/Cis4/CoreCalendarEvents.css b/public/css/Cis4/CoreCalendarEvents.css index 3941872ef..da2b87deb 100644 --- a/public/css/Cis4/CoreCalendarEvents.css +++ b/public/css/Cis4/CoreCalendarEvents.css @@ -97,3 +97,17 @@ display: none; } +.fhc-calendar-empty-slot-plus { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%,-50%); + font-size: 18px; + opacity: 0; + pointer-events: none; + user-select: none; +} + +.part-body:hover .fhc-calendar-empty-slot-plus { + opacity: 1; +} diff --git a/public/js/api/factory/calendar/roomPlan.js b/public/js/api/factory/calendar/roomPlan.js new file mode 100644 index 000000000..d27843b37 --- /dev/null +++ b/public/js/api/factory/calendar/roomPlan.js @@ -0,0 +1,44 @@ +export default { + getReservableMap(ort_kurzbz, start_date, end_date) { + return { + method: 'post', + url: `/api/frontend/v1/calendar/RoomPlan/getReservableMap/${ort_kurzbz}`, + params: { start_date, end_date } + }; + }, + + getRoomCreationInfo() { + return { + method: 'get', + url: '/api/frontend/v1/calendar/RoomPlan/getRoomCreationInfo' + }; + }, + getGruppen(query) { + return { + method: 'get', + url: `/api/frontend/v1/calendar/RoomPlan/getGruppen?query=${encodeURIComponent(query)}` + }; + }, + getLektor(query) { + return { + method: 'get', + url: `/api/frontend/v1/calendar/RoomPlan/getLektor?query=${encodeURIComponent(query)}` + }; + }, + addRoomReservation(formData) { + return { + method: 'post', + url: '/api/frontend/v1/calendar/RoomPlan/addRoomReservation', + params: formData + }; + }, + deleteRoomReservation(reservierung_id) { + return { + method: 'post', + url: '/api/frontend/v1/calendar/RoomPlan/deleteRoomReservation', + params: { + reservierung_id: reservierung_id + } + }; + } +} \ No newline at end of file diff --git a/public/js/components/Calendar/Base.js b/public/js/components/Calendar/Base.js index 36d9877da..aeb374cde 100644 --- a/public/js/components/Calendar/Base.js +++ b/public/js/components/Calendar/Base.js @@ -44,7 +44,10 @@ export default { return () => true; }), hasDragoverFunc: Vue.computed(() => this.onDragover), - mode: Vue.computed(() => this.mode) + mode: Vue.computed(() => this.mode), + reservierbarMap: Vue.computed(() => this.reservierbarMap), + isReservierbar: Vue.computed(() => this.isReservierbar), + createContext: Vue.computed(() => this.createContext) }; }, props: { @@ -97,7 +100,13 @@ export default { draggableEvents: [Boolean, Array, Function], dropableEvents: [Boolean, Array, Function], onDragover: Function, - onDrop: Function + onDrop: Function, + isReservierbar: Boolean, + createContext: Object, + reservierbarMap: { + type: Object, + default: () => ({}) + }, }, emits: [ "click:next", @@ -105,11 +114,13 @@ export default { "click:mode", "click:event", "click:day", + "click:slot", "click:week", "update:date", "update:mode", "update:range", - "drop" + "drop", + "create-event" ], data() { return { diff --git a/public/js/components/Calendar/Base/Grid.js b/public/js/components/Calendar/Base/Grid.js index 3418a9151..1a10533b3 100644 --- a/public/js/components/Calendar/Base/Grid.js +++ b/public/js/components/Calendar/Base/Grid.js @@ -2,6 +2,7 @@ import GridLine from './Grid/Line.js'; import GridLineEvent from './Grid/Line/Event.js'; import CalDnd from '../../../directives/Calendar/DragAndDrop.js'; +import CalClick from '../../../directives/Calendar/Click.js'; export default { name: "CalendarGrid", @@ -10,12 +11,15 @@ export default { GridLineEvent }, directives: { - CalDnd + CalDnd, + CalClick }, inject: { originalEvents: "events", originalBackgrounds: "backgrounds", - dropAllowed: "dropAllowed" + dropAllowed: "dropAllowed", + timezone: "timezone", + reservierbar: "isReservierbar" }, provide() { return { @@ -308,6 +312,20 @@ export default { } else { this.$refs.scroller.scrollTo(0, 0); } + }, + isFreeSlot(date, part, dayEvents) { + const pastEnd = luxon.DateTime.now().setZone(this.timezone); + + const start = date.plus(part.start || part); + const end = date.plus(part.end || part.plus({ hours: 1 })); + + if (start < pastEnd) + return false; + + if (!dayEvents || !dayEvents.length) + return true; + + return !dayEvents.some(ev => ev.start < end && ev.end > start); } }, beforeUnmount() { @@ -400,6 +418,18 @@ export default { :style="'grid-' + axisCol + ':' + (1+index) + ';grid-' + axisRow + ':ps_' + i + '/pe_' + i" > + +
+
+ +
+
+
({}) + }, }, emits: [ "update:date", "update:mode", - "update:range" + "update:range", + "create-event", + "delete-event", + 'update:reservierbarMap' ], data() { return { + isReservierbar: Vue.computed(() => { + if (!this.reservierbar) + return false; + + if (!this.reservierbarMap) + return false; + + if (typeof this.reservierbarMap === 'object') + return Object.keys(this.reservierbarMap).length > 0; + + return false; + }), modes: { day: Vue.markRaw(ModeDay), week: Vue.markRaw(ModeWeek), @@ -88,21 +111,33 @@ export default { updateRange(rangeInterval) { this.rangeInterval = rangeInterval; this.$emit('update:range', rangeInterval); + }, + closeModal() { + this.$refs.calendar.hideEventModal(); + }, + resetEventLoader() { + this.reset(); } }, setup(props, context) { const rangeInterval = Vue.ref(null); - const { events, lv } = useEventLoader(rangeInterval, props.getPromiseFunc); + const { events, lv, reservierbarMap, reset } = useEventLoader(rangeInterval, props.getPromiseFunc); Vue.watch(lv, newValue => { context.emit('update:lv', newValue); }); + Vue.watch(reservierbarMap, newVal => { + context.emit('update:reservierbarMap', newVal); + }); + return { rangeInterval, events, - lv + lv, + reservierbarMap, + reset, }; }, created() { @@ -129,6 +164,9 @@ export default { :events="events || []" :backgrounds="backgrounds" :time-grid="teachingunits" + :reservierbar-map="reservierbarMap" + :isReservierbar="isReservierbar" + :create-context="createContext" show-btns @update:date="(newDate, newMode) => $emit('update:date', newDate, newMode)" @update:mode="(newMode, newDate) => $emit('update:mode', newMode, newDate)" @@ -144,6 +182,7 @@ export default { v-if="mode == 'event'" :is="renderers[event.type]?.modalContent" :event="event" + @create-event="(event) => $emit('create-event', event)" >
diff --git a/public/js/components/Calendar/Mode/Day/View.js b/public/js/components/Calendar/Mode/Day/View.js index cd953f33a..18de8207c 100644 --- a/public/js/components/Calendar/Mode/Day/View.js +++ b/public/js/components/Calendar/Mode/Day/View.js @@ -16,7 +16,19 @@ export default { inject: { timeGrid: "timeGrid", originalEvents: "events", - timezone: "timezone" + timezone: "timezone", + reservierbar: { + from: "isReservierbar", + default: false + }, + reservierbarMap: { + type: Object, + default: () => ({}) + }, + createContext: { + from: 'createContext', + default: () => {} + }, }, props: { day: { @@ -103,6 +115,27 @@ export default { }); } } + else if (evt.detail.source == 'slot') + { + if (!this.reservierbar) + return; + + const { date, part } = evt.detail.value || {}; + if (!date) + return; + let reservierbar = this.reservierbarMap?.[date.toISODate()] === true; + if (!reservierbar) + return; + + this.$emit('requestModalOpen', { + event: { + type: this.createContext?.scope ?? 'slot', + start: date.plus(part.start || part), + end: date.plus(part.end || part.plus({ hours: 1 })), + createContext: this.createContext + } + }); + } } }, setup() { diff --git a/public/js/components/Calendar/Mode/Week.js b/public/js/components/Calendar/Mode/Week.js index f8653b9e9..21458ed8d 100644 --- a/public/js/components/Calendar/Mode/Week.js +++ b/public/js/components/Calendar/Mode/Week.js @@ -7,6 +7,14 @@ export default { BaseSlider, WeekView }, + inject: { + reservierbar: "isReservierbar", + reservierbarMap: "reservierbarMap", + createContext: { + from: 'createContext', + default: () => {} + }, + }, props: { currentDate: { type: luxon.DateTime, @@ -83,14 +91,33 @@ export default { }, handleClickDefaults(evt) { switch (evt.detail.source) { - case 'day': - // default: Set current-date - this.$emit('update:currentDate', evt.detail.value); - break; - case 'event': - // default: Request Modal - this.$emit('requestModalOpen', { event: evt.detail.value }); - break; + case 'day': + // default: Set current-date + this.$emit('update:currentDate', evt.detail.value); + break; + case 'event': + // default: Request Modal + this.$emit('requestModalOpen', { event: evt.detail.value }); + break; + case 'slot': + { + const { date, part } = evt.detail.value || {}; + if (!date) + return; + let reservierbar = this.reservierbarMap?.[date.toISODate()] === true; + if (!reservierbar) + return; + + this.$emit('requestModalOpen', { + event: { + type: this.createContext?.scope ?? 'slot', + start: date.plus(part.start || part), + end: date.plus(part.end || part.plus({ hours: 1 })), + createContext: this.createContext + } + }); + break; + } } } }, diff --git a/public/js/components/Cis/Mylv/RoomInformation.js b/public/js/components/Cis/Mylv/RoomInformation.js index 2dd3d518d..a74d5233d 100644 --- a/public/js/components/Cis/Mylv/RoomInformation.js +++ b/public/js/components/Cis/Mylv/RoomInformation.js @@ -1,6 +1,7 @@ import FhcCalendar from "../../Calendar/LvPlan.js"; import ApiLvPlan from '../../../api/factory/lvPlan.js'; +import ApiRoomPlan from '../../../api/factory/calendar/roomPlan.js'; export const DEFAULT_MODE_RAUMINFO = 'Week' @@ -21,6 +22,36 @@ export default { return this.propsViewData?.mode || DEFAULT_MODE_RAUMINFO; } }, + data() { + return { + filteredGroups: [], + abortController: null, + createContext: { + scope: 'slot_room', + show_all_fields: false, + room_create_information: { + semester: [1, 2, 3, 4, 5, 6, 7, 8], + verband: ['A', 'B', 'C', 'D', 'E', 'F', 'V'], + gruppe: [1, 2, 3, 4], + studiengaenge: [], + searchGroup: this.searchGroup, + searchLektor: this.searchLektor, + }, + } + } + }, + created() { + + this.$api.call(ApiRoomPlan.getRoomCreationInfo()) + .then(result => result.data) + .then(result => { + if (result.berechtigt) + { + this.createContext.room_create_information.studiengaenge = result.studiengaenge + } + this.createContext.show_all_fields = result.berechtigt; + }); + }, methods:{ handleChangeDate(day, newMode) { return this.handleChangeMode(newMode, day); @@ -38,8 +69,81 @@ export default { } }); }, + async handleCreateEvent(event) + { + event.ort_kurzbz = this.propsViewData.ort_kurzbz; + this.$api.call(ApiRoomPlan.addRoomReservation(event)); + this.$refs.calendar.resetEventLoader(); + this.$refs.calendar.closeModal(); + }, + async handleDeleteEvent(event) + { + if (event.type !== 'reservierung') + return; + + if (luxon.DateTime.fromISO(`${event.datum}T${event.beginn}`) < luxon.DateTime.now()) + return; + + this.$api.call(ApiRoomPlan.deleteRoomReservation(event.reservierung_id)); + + this.$refs.calendar.reset(); + + }, + async searchGroup(event) + { + const query = event.query.trim(); + + if (query.length < 2) + return []; + + if (this.abortController) + this.abortController.abort(); + + this.abortController = new AbortController(); + const signal = this.abortController.signal; + + return this.$api.call(ApiRoomPlan.getGruppen(query), { signal }) + .then(result => { + return result.data.map(gruppe => ({ + label: gruppe.bezeichnung + ? `${gruppe.gruppe_kurzbz.trim()} (${gruppe.bezeichnung})` + : gruppe.gruppe_kurzbz.trim(), + gid: gruppe.gid, + gruppe_kurzbz: gruppe.gruppe_kurzbz.trim(), + lehrverband: gruppe.lehrverband, + }) + ); + }) + .catch((e)=> { + this.$fhcAlert.handleSystemError(e) + return [] + }) + }, + async searchLektor(event) + { + const query = event.query.trim(); + + if (query.length < 2) + return []; + + if (this.abortController) + this.abortController.abort(); + + this.abortController = new AbortController(); + const signal = this.abortController.signal; + + return this.$api.call(ApiRoomPlan.getLektor(query), { signal }) + .then(result => { + return result.data.map(lektor => ({ + label: `${lektor.nachname} ${lektor.vorname} (${lektor.uid})`, + uid: lektor.uid + }) + )}) + .catch(this.$fhcAlert.handleSystemError) + }, getPromiseFunc(start, end) { return [ + this.$api.call(ApiRoomPlan.getReservableMap(this.propsViewData.ort_kurzbz, start.toISODate(), end.toISODate())), this.$api.call(ApiLvPlan.getRoomInfo(this.propsViewData.ort_kurzbz, start.toISODate(), end.toISODate())), this.$api.call(ApiLvPlan.getOrtReservierungen(this.propsViewData.ort_kurzbz, start.toISODate(), end.toISODate())) ]; @@ -55,8 +159,12 @@ export default { :get-promise-func="getPromiseFunc" :date="currentDay" :mode="currentMode" + :reservierbar="true" + :create-context="createContext" @update:date="handleChangeDate" @update:mode="handleChangeMode" + @create-event="handleCreateEvent" + @delete-event="handleDeleteEvent" class="responsive-calendar" > ` diff --git a/public/js/components/Cis/Renderer/Reservierungen/calendarEvent.js b/public/js/components/Cis/Renderer/Reservierungen/calendarEvent.js index 2447c4f61..a26788990 100644 --- a/public/js/components/Cis/Renderer/Reservierungen/calendarEvent.js +++ b/public/js/components/Cis/Renderer/Reservierungen/calendarEvent.js @@ -5,6 +5,10 @@ export default { required: true } }, + inject: { + mode: "mode", + }, + emits: ['delete-event'], computed: { tooltipString() { const tooltipArray = []; @@ -50,12 +54,20 @@ export default { return luxon.Duration .fromISOTime(this.event.ende) .toISOTime({ suppressSeconds: true }); + }, + isFutureEvent() { + const eventStart = luxon.DateTime.fromISO(`${this.event.datum}T${this.event.beginn}`); + return eventStart > luxon.DateTime.now(); + } + }, + methods: { + handleDelete() { + this.$emit('delete-event', this.event); } }, template: /* html */`
+ class="cis-renderer-reservierungen-calendar-event calendar-event-default h-100 w-100 p-1 position-relative">
{{ end }}
+ + {{ event.topic }} t.start === startTime)?.start || this.timeGrid[0]?.start; + this.selectedEnd = this.timeGrid.find(t => t.end === endTime)?.end || this.timeGrid.at(-1)?.end; + }, + saveEvent() { + const [startHour, startMinute] = this.selectedStart.split(':').map(Number); + const [endHour, endMinute] = this.selectedEnd.split(':').map(Number); + const selectedStart = this.event.start.startOf('day').set({ hour: startHour, minute: startMinute }); + const selectedEnd = this.event.start.startOf('day').set({ hour: endHour, minute: endMinute }); + + const lektoren_uid = this.selectedLektoren.map(m => m.uid) + + const spezialgruppe = this.selectedGruppe?.gruppe_kurzbz; + + const event = { + selectedStart: selectedStart, + selectedEnd: selectedEnd, + title: this.title, + beschreibung: this.beschreibung, + studiengang: this.studiengang, + semester: this.semester, + verband: this.verband, + gruppe: this.gruppe, + spezialgruppe: spezialgruppe, + lektoren: lektoren_uid + } + + this.$emit('create-event', event); + }, + async searchGroup(event) { + this.filteredGroups = await this.event.createContext.room_create_information.searchGroup(event) + }, + async searchLektor(event) { + this.filteredLektoren = await this.event.createContext.room_create_information.searchLektor(event) + }, + capitalize(text) + { + if (!text) return '' + return text.charAt(0).toUpperCase() + text.slice(1) + } + }, + + template: ` +
+
Neue Reservierung
+
+ + + + + + + +
+
+ + + + + +
+ +
+
+ + + + + + + + + + + + + + + + + +
+
+ + + +
+ ` +}; diff --git a/public/js/components/Cis/Renderer/Slot/roomModalTitle.js b/public/js/components/Cis/Renderer/Slot/roomModalTitle.js new file mode 100644 index 000000000..1f477a997 --- /dev/null +++ b/public/js/components/Cis/Renderer/Slot/roomModalTitle.js @@ -0,0 +1,3 @@ +export default { + template:`
{{ $p.t('lehre','new_reservierung') }}
` +} \ No newline at end of file diff --git a/public/js/composables/EventLoader.js b/public/js/composables/EventLoader.js index 7360d0286..6c9bfa907 100644 --- a/public/js/composables/EventLoader.js +++ b/public/js/composables/EventLoader.js @@ -7,6 +7,12 @@ export function useEventLoader(rangeInterval, getPromiseFunc) { const allEvents = Vue.computed(() => events.value.concat(loadingEvents.value)); const lv = Vue.ref(null); const eventsLoaded = []; + const reservierbarMap = Vue.ref({}); + + const mergeReservierbarMap = (incoming) => { + if (!incoming) return; + reservierbarMap.value = { ...reservierbarMap.value, ...incoming }; + }; const mergePromiseArr = (n, o) => { if (Array.isArray(n)) @@ -111,7 +117,7 @@ export function useEventLoader(rangeInterval, getPromiseFunc) { return mergePromiseArr(getPromiseFunc(start, end), result); }; - Vue.watchEffect(() => { + const reload = () => { const range = Vue.toValue(rangeInterval); if (!(range instanceof luxon.Interval)) return; @@ -128,11 +134,23 @@ export function useEventLoader(rangeInterval, getPromiseFunc) { lv.value = res.value.meta.lv; events.value = events.value.concat(res.value.data); + mergeReservierbarMap(res.value.data?.reservierbarMap); loadingEvents.value = []; } }) }); - }) + }; - return { events: allEvents, lv } + Vue.watchEffect(reload); + + const reset = () => { + loading_id = 0; + events.value = []; + loadingEvents.value = []; + reservierbarMap.value = {}; + eventsLoaded.splice(0, eventsLoaded.length); + reload(); + } + + return { events: allEvents, lv, reservierbarMap, reset } } \ No newline at end of file diff --git a/system/phrasesupdate.php b/system/phrasesupdate.php index 228fa5f74..dd0026bca 100644 --- a/system/phrasesupdate.php +++ b/system/phrasesupdate.php @@ -46226,6 +46226,46 @@ array( ) ) ), + array( + 'app' => 'core', + 'category' => 'lehre', + 'phrase' => 'special_group', + 'insertvon' => 'system', + 'phrases' => array( + array( + 'sprache' => 'German', + 'text' => 'Spezialgruppe', + 'description' => '', + 'insertvon' => 'system' + ), + array( + 'sprache' => 'English', + 'text' => 'Special group', + 'description' => '', + 'insertvon' => 'system' + ) + ) + ), + array( + 'app' => 'core', + 'category' => 'lehre', + 'phrase' => 'new_reservierung', + 'insertvon' => 'system', + 'phrases' => array( + array( + 'sprache' => 'German', + 'text' => 'Neue Reservierung', + 'description' => '', + 'insertvon' => 'system' + ), + array( + 'sprache' => 'English', + 'text' => 'New reservation', + 'description' => '', + 'insertvon' => 'system' + ) + ) + ), // feature-55614 end array( 'app' => 'softwarebereitstellung',