diff --git a/application/controllers/api/frontend/v1/tempus/Coursepicker.php b/application/controllers/api/frontend/v1/tempus/Coursepicker.php index 73414c75e..18de39a4e 100644 --- a/application/controllers/api/frontend/v1/tempus/Coursepicker.php +++ b/application/controllers/api/frontend/v1/tempus/Coursepicker.php @@ -32,7 +32,6 @@ class Coursepicker extends FHCAPI_Controller //TODO Where weiter anpassen z.B. Fachbereich $this->_ci->LehreinheitModel->addSelect('tbl_lehreinheit.lehreinheit_id, - tbl_lehreinheit.unr, tbl_lehreinheit.lvnr, tbl_lehreinheit.lehrfach_id, lehrfach.kurzbz AS lehrfach, @@ -103,7 +102,6 @@ class Coursepicker extends FHCAPI_Controller $this->_ci->LehreinheitModel->addSelect(' tbl_lehreinheit.lehreinheit_id, - tbl_lehreinheit.unr, tbl_lehreinheit.lvnr, tbl_lehreinheit.lehrfach_id, lehrfach.kurzbz AS lehrfach, @@ -146,15 +144,30 @@ class Coursepicker extends FHCAPI_Controller )); $result = hasData($result) ? getData($result) : array(); + + $lehreinheit_ids = array_values(array_unique(array_column($result, 'lehreinheit_id'))); + + $offen_map = array(); + if (!empty($lehreinheit_ids)) + { + $offen_result = $this->_ci->LehreinheitModel->getOffeneStunden($lehreinheit_ids); + if (hasData($offen_result)) + { + foreach (getData($offen_result) as $row) + { + $offen_map[$row->lehreinheit_id . '_' . $row->mitarbeiter_uid] = $row; + } + } + } + $grouped = array(); foreach ($result as $row) { - $unr = $row->unr; - if (!isset($grouped[$unr])) + $lehreinheit_id = $row->lehreinheit_id; + if (!isset($grouped[$lehreinheit_id])) { - $grouped[$unr] = (object)array( - 'unr' => $row->unr, + $grouped[$lehreinheit_id] = (object)array( 'lehrfach_id' => $row->lehrfach_id, 'lehrfach_bez' => $row->lehrfach_bez, 'lehrfach_farbe' => $row->lehrfach_farbe, @@ -186,7 +199,7 @@ class Coursepicker extends FHCAPI_Controller ); } - $group = $grouped[$unr]; + $group = $grouped[$lehreinheit_id]; $group->lektoren[$row->lektor_uid] = (object)array( 'uid' => $row->lektor_uid, @@ -202,9 +215,15 @@ class Coursepicker extends FHCAPI_Controller $group->wochenrythmus[] = $row->wochenrythmus; $group->planstunden[] = $row->planstunden; $group->start_kw[] = $row->start_kw; - $group->verplant[] = isset($row->verplant) ? $row->verplant : 0; - $group->offenestunden[] = isset($row->offenestunden) ? $row->offenestunden : 0; - $group->verplant_gesamt += isset($row->verplant) ? $row->verplant : 0; + + $offen_key = $lehreinheit_id . '_' . $row->lektor_uid; + $offen = isset($offen_map[$offen_key]) ? $offen_map[$offen_key] : null; + $verplant_wert = $offen ? $offen->verplant : 0; + $offenestunden_wert = $offen ? $offen->offenestunden : 0; + + $group->verplant[] = $verplant_wert; + $group->offenestunden[] = $offenestunden_wert; + $group->verplant_gesamt += $verplant_wert; $lvb = $row->studiengang . '-' . $row->semester; @@ -244,7 +263,7 @@ class Coursepicker extends FHCAPI_Controller $values = array_values(array_unique($arr)); $formatted = implode(' ', $values); - if (count($formatted) > 1) + if (count($values) > 1) $formatted .= ' ?'; return $formatted; diff --git a/application/controllers/api/frontend/v1/tempus/Kalender.php b/application/controllers/api/frontend/v1/tempus/Kalender.php index d1aeaa576..ba7eec4d0 100644 --- a/application/controllers/api/frontend/v1/tempus/Kalender.php +++ b/application/controllers/api/frontend/v1/tempus/Kalender.php @@ -30,6 +30,9 @@ class Kalender extends FHCAPI_Controller 'getZeitsperren' => self::PERM_LOGGED, 'updateKalenderEvent' => 'lehre/lvplan:rw', 'addKalenderEvent' => 'lehre/lvplan:rw', + 'calculateMultiWeekPlan' => 'lehre/lvplan:rw', + 'confirmMultiWeekPlan' => 'lehre/lvplan:rw', + 'addToKalenderEvent' => 'lehre/lvplan:rw', 'addReservierung' => 'lehre/lvplan:rw', 'sync' => 'lehre/lvplan:rw', ]); @@ -322,8 +325,6 @@ class Kalender extends FHCAPI_Controller $ort_kurzbz = $this->_ci->input->post('ort_kurzbz', TRUE); $start_date = $this->_ci->input->post('start_date', TRUE); $end_date = $this->_ci->input->post('end_date', TRUE); - - $result = $this->_ci->kalenderlib->addKalenderEvent($start_date, $end_date, $lehreinheit_id, $ort_kurzbz); if (isError($result)) @@ -332,6 +333,69 @@ class Kalender extends FHCAPI_Controller $this->terminateWithSuccess(getData($result)); } + public function calculateMultiWeekPlan() + { + $this->_ci->form_validation->set_data($_POST); + $this->_ci->form_validation->set_rules('lehreinheit_id',"lehreinheit_id","required"); + $this->_ci->form_validation->set_rules('start_date',"start_date","required"); + $this->_ci->form_validation->set_rules('end_date',"end_date","required"); + + if($this->_ci->form_validation->run() === FALSE) + $this->terminateWithValidationErrors($this->_ci->form_validation->error_array()); + + $lehreinheit_id = $this->_ci->input->post('lehreinheit_id', TRUE); + $ort_kurzbz = $this->_ci->input->post('ort_kurzbz', TRUE); + $start_date = $this->_ci->input->post('start_date', TRUE); + $end_date = $this->_ci->input->post('end_date', TRUE); + + $result = $this->_ci->kalenderlib->calculateMultiWeekPlan($start_date, $end_date, $lehreinheit_id, $ort_kurzbz); + + if (isError($result)) + $this->terminateWithError(getError($result)); + + $this->terminateWithSuccess(getData($result)); + } + + public function confirmMultiWeekPlan() + { + $this->_ci->form_validation->set_data($_POST); + $this->_ci->form_validation->set_rules('plan[]',"plan","required"); + $this->_ci->form_validation->set_rules('lehreinheit_id',"lehreinheit_id","required"); + + if($this->_ci->form_validation->run() === FALSE) + $this->terminateWithValidationErrors($this->_ci->form_validation->error_array()); + + $plan = $this->_ci->input->post('plan', TRUE); + $lehreinheit_id = $this->_ci->input->post('lehreinheit_id', TRUE); + + $result = $this->_ci->kalenderlib->confirmMultiWeekPlan($plan, $lehreinheit_id); + + if (isError($result)) + $this->terminateWithError(getError($result)); + + $this->terminateWithSuccess(getData($result)); + } + + public function addToKalenderEvent() + { + $this->_ci->form_validation->set_data($_POST); + $this->_ci->form_validation->set_rules('target_kalender_id',"target_kalender_id","required"); + $this->_ci->form_validation->set_rules('lehreinheit_id',"lehreinheit_id","required"); + + if($this->_ci->form_validation->run() === FALSE) + $this->terminateWithValidationErrors($this->_ci->form_validation->error_array()); + + $target_kalender_id = $this->_ci->input->post('target_kalender_id', TRUE); + $lehreinheit_id = $this->_ci->input->post('lehreinheit_id', TRUE); + + $result = $this->_ci->kalenderlib->addToKalenderEvent($target_kalender_id, $lehreinheit_id); + + if (isError($result)) + $this->terminateWithError(getError($result)); + + $this->terminateWithSuccess(getData($result)); + } + public function addReservierung() { $this->_ci->form_validation->set_data($_POST); diff --git a/application/controllers/system/MigrateKalender.php b/application/controllers/system/MigrateKalender.php index eee368ff9..ec199ee08 100644 --- a/application/controllers/system/MigrateKalender.php +++ b/application/controllers/system/MigrateKalender.php @@ -8,7 +8,7 @@ if (! defined('BASEPATH')) exit('No direct script access allowed'); -class MigrateKalender extends Auth_Controller +class MigrateKalender extends CLI_Controller { /** @@ -35,8 +35,14 @@ class MigrateKalender extends Auth_Controller /** * Everything has a beginning */ - public function migrateStundenplan($von = null, $bis = null, $studiengang_kz = null) + public function migrateStundenplan($von, $bis = null, $studiengang_kz = null) { + if (is_null($von)) + return error('Parameter "von" ist erforderlich'); + + if (is_null($bis)) + $bis = '2100-12-31'; + $db = new DB_Model(); $stpldevsql = ' @@ -71,8 +77,8 @@ class MigrateKalender extends Auth_Controller bk.block_nr, MIN(bk.stunde) AS stunde_von, MAX(bk.stunde) AS stunde_bis, - MIN(sp.lehreinheit_id) AS lehreinheit_id, - MIN(sp.ort_kurzbz) AS ort_kurzbz, + array_agg(DISTINCT sp.lehreinheit_id) AS lehreinheit_id, + array_agg(DISTINCT sp.ort_kurzbz) AS ort_kurzbz, array_agg(sp.stundenplandev_id ORDER BY bk.stunde) AS stundenplandev_ids, MIN(sp.insertamum) AS insertamum, (array_agg(sp.insertvon ORDER BY sp.insertamum ASC))[1] AS insertvon, @@ -118,7 +124,7 @@ class MigrateKalender extends Auth_Controller $ids = is_array($block->stundenplandev_ids) ? $block->stundenplandev_ids : explode(',', $block->stundenplandev_ids); /*$ids = array_map('intval', $ids);*/ - $this->SyncModel->db->where('stundenplandev_id IN (' . implode(',', $ids) . ')'); + $this->SyncModel->db->where_in('stundenplandev_id', $ids); $sync_result = $this->SyncModel->load(); if (!hasData($sync_result)) @@ -152,8 +158,14 @@ class MigrateKalender extends Auth_Controller } } - public function migrateReservierung($von = null, $bis = null, $ort_kurzbz = null) + public function migrateReservierung($von, $bis = null, $ort_kurzbz = null) { + if (is_null($von)) + return error('Parameter "von" ist erforderlich'); + + if (is_null($bis)) + $bis = '2100-12-31'; + $db = new DB_Model(); $qry = "WITH eindeutige_stunden AS ( @@ -170,43 +182,42 @@ class MigrateKalender extends Auth_Controller } $qry .= "), - block_keys AS ( + block_keys AS ( + SELECT + titel, beschreibung, datum, stunde, + stunde - ROW_NUMBER() OVER (PARTITION BY titel, beschreibung, datum ORDER BY stunde) AS block_nr + FROM eindeutige_stunden + ), + blocks AS ( SELECT - titel, beschreibung, datum, stunde, - stunde - ROW_NUMBER() OVER (PARTITION BY titel, beschreibung, datum ORDER BY stunde) AS block_nr - FROM eindeutige_stunden - ), - blocks AS ( - SELECT - bk.titel, - bk.beschreibung, - bk.datum, - bk.block_nr, - MIN(bk.stunde) AS stunde_von, - MAX(bk.stunde) AS stunde_bis, - array_agg(DISTINCT r.reservierung_id::text) AS reservierung_ids, - array_agg(DISTINCT r.uid) AS uids, - array_agg(DISTINCT r.gruppe_kurzbz) AS gruppen_kurzbz, - array_agg(DISTINCT ROW(r.semester, r.verband, r.gruppe)::text) AS svg_kombis, - array_agg(DISTINCT r.ort_kurzbz) AS orte_kurzbz, - MIN(r.studiengang_kz) AS studiengang_kz, - MIN(r.veranstaltung_id) AS veranstaltung_id, - MIN(r.reservierung_id) AS reservierung_id, - MAX(r.insertamum) AS insertamum, - (array_agg(r.insertvon ORDER BY r.insertamum ASC))[1] AS insertvon - FROM block_keys bk - JOIN campus.tbl_reservierung r - ON r.titel = bk.titel AND r.beschreibung = bk.beschreibung AND r.datum = bk.datum AND r.stunde = bk.stunde - WHERE r.datum >= ? AND r.datum <= ? - GROUP BY bk.titel, bk.beschreibung, bk.datum, bk.block_nr - ) + bk.titel, + bk.beschreibung, + bk.datum, + bk.block_nr, + MIN(bk.stunde) AS stunde_von, + MAX(bk.stunde) AS stunde_bis, + array_agg(DISTINCT r.reservierung_id::text) AS reservierung_ids, + array_agg(DISTINCT r.uid) AS uids, + array_agg(DISTINCT r.gruppe_kurzbz) AS gruppen_kurzbz, + array_agg(DISTINCT ROW(r.semester, r.verband, r.gruppe, r.studiengang_kz)::text) AS svg_kombis, + array_agg(DISTINCT r.ort_kurzbz) AS ort_kurzbz, + MIN(r.studiengang_kz) AS studiengang_kz, + MIN(r.reservierung_id) AS reservierung_id, + MAX(r.insertamum) AS insertamum, + (array_agg(r.insertvon ORDER BY r.insertamum ASC))[1] AS insertvon + FROM block_keys bk + JOIN campus.tbl_reservierung r + ON r.titel = bk.titel AND r.beschreibung = bk.beschreibung AND r.datum = bk.datum AND r.stunde = bk.stunde + WHERE r.datum >= ? AND r.datum <= ? + GROUP BY bk.titel, bk.beschreibung, bk.datum, bk.block_nr + ) SELECT b.*, (b.datum + s_von.beginn) AS von, - (b.datum + s_bis.ende) AS bis + (b.datum + s_bis.ende) AS bis FROM blocks b - JOIN lehre.tbl_stunde s_von ON s_von.stunde = b.stunde_von - JOIN lehre.tbl_stunde s_bis ON s_bis.stunde = b.stunde_bis + JOIN lehre.tbl_stunde s_von ON s_von.stunde = b.stunde_von + JOIN lehre.tbl_stunde s_bis ON s_bis.stunde = b.stunde_bis ORDER BY b.reservierung_id DESC;"; array_push($params, $von, $bis); @@ -222,7 +233,7 @@ class MigrateKalender extends Auth_Controller $ids = is_array($block->reservierung_ids) ? $block->reservierung_ids : explode(',', $block->reservierung_ids); - $this->SyncReservierungModel->db->where('reservierung_id IN (' . implode(',', $ids) . ')'); + $this->SyncReservierungModel->db->where_in('reservierung_id', $ids); $sync_result = $this->SyncReservierungModel->load(); if (!hasData($sync_result)) @@ -379,21 +390,30 @@ class MigrateKalender extends Auth_Controller if ($typ === 'lehreinheit') { - $this->KalenderLehreinheitModel->insert( - array ( - 'kalender_id' => $kalender_id, - 'lehreinheit_id'=> $block->lehreinheit_id - ) - ); + $lehreinheit_ids = is_array($block->lehreinheit_id) ? $block->lehreinheit_id : explode(',', $block->lehreinheit_id); + foreach ($lehreinheit_ids as $lehreinheit_id) + { + $leResult = $this->KalenderLehreinheitModel->insert( + array ( + 'kalender_id' => $kalender_id, + 'lehreinheit_id'=> $lehreinheit_id + ) + ); + + if (!isSuccess($leResult)) + return null; + } } else if ($typ === 'reservierung') { - $this->KalenderEventModel->insert(array( + $eventResult = $this->KalenderEventModel->insert(array( 'kalender_id' => $kalender_id, 'titel' => $block->titel, 'beschreibung' => $block->beschreibung )); + if (!isSuccess($eventResult)) + return null; if ($block->insertvon) { @@ -420,7 +440,10 @@ class MigrateKalender extends Auth_Controller } $semester_range = $this->StudiensemesterModel->getByDateRange($block->von, $block->bis); - if (isError($semester_range)) return $semester_range; + + if (isError($semester_range)) + return null; + $studiensemester_kurzbz = getData($semester_range)[0]->studiensemester_kurzbz ?? null; $gruppen = is_array($block->gruppen_kurzbz) ? $block->gruppen_kurzbz : explode(',', $block->gruppen_kurzbz ?? ''); @@ -458,18 +481,19 @@ class MigrateKalender extends Auth_Controller foreach ($block->svg_kombis as $kombi_str) { $kombi_str = trim($kombi_str, '()'); - list($sem, $verb, $grp) = explode(',', $kombi_str); + list($sem, $verb, $grp, $stg_kz) = explode(',', $kombi_str); $sem = trim($sem) === '' ? null : trim($sem); $verb = trim($verb) === '' ? null : trim($verb); $grp = trim($grp) === '' ? null : trim($grp); + $stg_kz = trim($stg_kz) === '' ? null : trim($stg_kz); - if (is_null($sem) && is_null($verb) && is_null($grp)) + if (is_null($sem) && is_null($verb) && is_null($grp) && is_null($stg_kz)) continue; $this->KalenderEventTeilnehmerModel->insert(array( 'kalender_id' => $kalender_id, - 'studiengang_kz' => $block->studiengang_kz, + 'studiengang_kz' => $stg_kz, 'semester' => $sem, 'verband' => $verb, 'gruppe' => $grp, @@ -479,6 +503,22 @@ class MigrateKalender extends Auth_Controller } } + $orte = is_array($block->ort_kurzbz) ? $block->ort_kurzbz : explode(',', $block->ort_kurzbz ?? ''); + + foreach ($orte as $ort) + { + $ort = trim($ort); + if (!empty($ort)) + { + $this->KalenderOrtModel->insert( + array ( + 'kalender_id' => $kalender_id, + 'ort_kurzbz' => $ort + ) + ); + } + } + return $kalender_id; } @@ -523,6 +563,21 @@ class MigrateKalender extends Auth_Controller 'updatevon' => $block->updatevon ) ); + + $orte = is_array($block->ort_kurzbz) ? $block->ort_kurzbz : explode(',', $block->ort_kurzbz ?? ''); + $orte = array_filter(array_map('trim', $orte)); + + $this->KalenderOrtModel->delete(array('kalender_id' => $kalender_id)); + + foreach ($orte as $ort) + { + $this->KalenderOrtModel->insert( + array ( + 'kalender_id' => $kalender_id, + 'ort_kurzbz' => $ort + ) + ); + } } private function _updateSync($ids, $kalender_id) diff --git a/application/libraries/KalenderLib.php b/application/libraries/KalenderLib.php index b0b3ad0ad..95cff42ba 100644 --- a/application/libraries/KalenderLib.php +++ b/application/libraries/KalenderLib.php @@ -21,9 +21,16 @@ class KalenderLib $this->_ci->load->model('ressource/Ort_model', 'OrtModel'); $this->_ci->load->model('organisation/gruppe_model', 'GruppeModel'); $this->_ci->load->model('organisation/Lehrverband_model', 'LehrverbandModel'); + $this->_ci->load->model('ressource/Stunde_model', 'StundeModel'); + $this->_ci->load->model('organisation/Studiensemester_model', 'StudiensemesterModel'); + $this->_ci->load->model('system/Variable_model', 'VariableModel'); + $this->_ci->load->model('organisation/Ferien_model', 'FerienModel'); + $this->_ci->load->library('CollisionChecker'); $this->_ci->load->library('PhrasesLib', array('ui')); + $this->_ci->load->library('VariableLib', array('uid' => getAuthUID())); + } @@ -62,9 +69,7 @@ class KalenderLib teilmitarbeiter.kurzbz as teilnehmer_kurzbz, teilperson.vorname as teilnehmer_vorname, teilperson.nachname as teilnehmer_nachname, - teilbenutzer.uid as teilnehmer_uid, - CASE WHEN tbl_lehreinheitgruppe.gruppe_kurzbz IS NULL THEN COALESCE( @@ -164,7 +169,7 @@ class KalenderLib 'isoend' => $bis->format('c'), 'tooltip' => 'tip', 'status_kurzbz' => $row->status_kurzbz, - 'ort_kurzbz' => isset($row->ort_kurzbz) ? $row->ort_kurzbz : '', + 'ort_kurzbz' => [], 'lehrform' => isset($row->lehrform_kurzbz) ? $row->lehrform_kurzbz : '', 'lehrfach' => isset($row->lehrfach_kurzbz) ? $row->lehrfach_kurzbz : '', 'lehrfach_bez' => isset($row->lehrfach_bezeichnung) ? $row->lehrfach_bezeichnung : '', @@ -178,15 +183,22 @@ class KalenderLib 'gruppe' => [], 'titel' => isset($row->titel) ? $row->titel : '', 'beschreibung' => isset($row->beschreibung) ? $row->beschreibung : '', - 'topic' => (isset($row->lehrfach_kurzbz) ? $row->lehrfach_kurzbz : '').' '.(isset($row->lehrform_kurzbz) ? $row->lehrform_kurzbz : ''), + 'topic' => [], + 'has_assigned_resources' => isset($row->has_assigned_resources) ? $row->has_assigned_resources : false, 'collisions' => false, - 'has_assigned_resources' => isset($row->has_assigned_resources) ? $row->has_assigned_resources : false ]; } if ($row->lehreinheit_id && !in_array($row->lehreinheit_id, $events[$id]->lehreinheit_id)) $events[$id]->lehreinheit_id[] = $row->lehreinheit_id; + if (isset($row->ort_kurzbz) && $row->ort_kurzbz !== '' && !in_array($row->ort_kurzbz, $events[$id]->ort_kurzbz)) + $events[$id]->ort_kurzbz[] = $row->ort_kurzbz; + + $topic = trim((isset($row->lehrfach_kurzbz) ? $row->lehrfach_kurzbz : '').' '.(isset($row->lehrform_kurzbz) ? $row->lehrform_kurzbz : '')); + if ($topic !== '' && !in_array($topic, $events[$id]->topic)) + $events[$id]->topic[] = $topic; + if ($row->mitarbeiter_uid) { if (!in_array($row->mitarbeiter_uid, array_column($events[$id]->lektor, 'mitarbeiter_uid'))) @@ -341,7 +353,13 @@ class KalenderLib $this->_ci->KalenderModel->db->where('tbl_kalender_ort.ort_kurzbz', $ort); if (!is_null($uids)) + { + $this->_ci->KalenderModel->db->group_start(); $this->_ci->KalenderModel->db->where_in('tbl_lehreinheitmitarbeiter.mitarbeiter_uid', $uids); + $this->_ci->KalenderModel->db->or_where_in('orginasator.uid', $uids); + $this->_ci->KalenderModel->db->or_where_in('teilnehmer.uid', $uids); + $this->_ci->KalenderModel->db->group_end(); + } if (!is_null($stg)) $this->_ci->KalenderModel->db->where('tbl_lehrveranstaltung.studiengang_kz', $stg); @@ -500,10 +518,57 @@ class KalenderLib return $zeitwuensche_array; } - public function addKalenderEvent($start_date, $end_date, $lehreinheit_id, $ort_kurzbz) - { - $this->_ci->KalenderModel->db->trans_start(); + public function addToKalenderEvent($target_kalender_id, $lehreinheit_id) + { + $check = $this->_ci->KalenderLehreinheitModel->loadWhere(array('kalender_id' => $target_kalender_id, 'lehreinheit_id' => $lehreinheit_id)); + + if (hasData($check)) + { + return error([ + 'message' => $this->_ci->phraseslib->t('ui', 'already_present'), + 'errorCode' => 'already_present' + ]); + } + + $kalenderlehreinheitresult = $this->_ci->KalenderLehreinheitModel->insert( + array( + 'kalender_id' => $target_kalender_id, + 'lehreinheit_id' => $lehreinheit_id + ) + ); + + if (isError($kalenderlehreinheitresult)) + return $kalenderlehreinheitresult; + + return success($target_kalender_id); + } + + private function _addWeeks($date_str, $weeks) + { + $d = new DateTime($date_str); + $d->modify('+' . $weeks . ' week'); + return $d->format('Y-m-d'); + } + + //TODO (david) studiengang_kz übergeben + private function _isFerien($date_str) + { + $this->_ci->FerienModel->db->where('vondatum <=', $date_str); + $this->_ci->FerienModel->db->where('bisdatum >=', $date_str); + $this->_ci->FerienModel->db->where('studiengang_kz =', 0); + + $ferien_result = $this->_ci->FerienModel->load(); + + if (isError($ferien_result)) + return false; + + if (hasData($ferien_result)) + return true; + } + + private function _insertKalenderEventRaw($start_date, $end_date, $lehreinheit_id, $ort_kurzbz) + { $kalenderresult = $this->_ci->KalenderModel->insert( array ( 'von' => $start_date, @@ -516,50 +581,244 @@ class KalenderLib ) ); - if (isSuccess($kalenderresult) && hasData($kalenderresult)) + if (!isSuccess($kalenderresult) || !hasData($kalenderresult)) + return ['kalender_id' => null, 'result' => $kalenderresult]; + + $kalender_id = getData($kalenderresult); + + $kalenderlehreinheitresult = $this->_ci->KalenderLehreinheitModel->insert( + array ( + 'kalender_id' => $kalender_id, + 'lehreinheit_id' => $lehreinheit_id + ) + ); + + if (isSuccess($kalenderlehreinheitresult) && !is_null($ort_kurzbz)) { - $kalender_id = getData($kalenderresult); - - $kalenderlehreinheitresult = $this->_ci->KalenderLehreinheitModel->insert( - array ( - 'kalender_id' => $kalender_id, - 'lehreinheit_id' => $lehreinheit_id - ) - ); - - if (isSuccess($kalenderlehreinheitresult) && !is_null($ort_kurzbz)) - { - $ortresult = $this->_addKalenderOrt($kalender_id, $ort_kurzbz); - if (isError($ortresult)) - { - $this->_ci->KalenderModel->db->trans_rollback(); - return $ortresult; - } - } - - $entryResult = $this->_loadKalenderEntry($kalender_id); - - if (isError($entryResult)) - { - $this->_ci->KalenderModel->db->trans_rollback(); - return $entryResult; - } - - $kalender_entry = getData($entryResult); - - $errors = $this->_ci->collisionchecker->run($kalender_entry); - - if (!empty($errors)) - { - $this->_ci->KalenderModel->db->trans_rollback(); - return error($errors); - } - - $this->_ci->KalenderModel->db->trans_complete(); - return $kalenderlehreinheitresult; + $ortresult = $this->_addKalenderOrt($kalender_id, $ort_kurzbz); + if (isError($ortresult)) + return ['kalender_id' => $kalender_id, 'result' => $ortresult]; } + + $entryResult = $this->_loadKalenderEntry($kalender_id); + if (isError($entryResult)) + return ['kalender_id' => $kalender_id, 'result' => $entryResult]; + + $kalender_entry = getData($entryResult); + $errors = $this->_ci->collisionchecker->run($kalender_entry); + + if (!empty($errors)) + return ['kalender_id' => $kalender_id, 'result' => error($errors)]; + + return ['kalender_id' => $kalender_id, 'result' => $kalenderlehreinheitresult]; + } + + public function calculateMultiWeekPlan($start_date, $end_date, $lehreinheit_id, $ort_kurzbz) + { + $this->_ci->load->library('RaumvorschlagLib'); + $le_result = $this->_ci->LehreinheitModel->load($lehreinheit_id); + + if (!hasData($le_result)) + return error('Lehreinheit nicht gefunden'); + $le = getData($le_result)[0]; + + $block = $le->stundenblockung; + $wochenrythmus = $le->wochenrythmus; + $studiensemester_kurzbz = $le->studiensemester_kurzbz; + + if ($block <= 0) + return error('Stundenblockung ist ungültig'); + if ($wochenrythmus <= 0) + return error('Wochenrythmus ist ungültig'); + + $offen_result = $this->_ci->LehreinheitModel->getOffeneStunden([$lehreinheit_id]); + if (!hasData($offen_result)) + return error('Offene Stunden nicht gefunden'); + + $rows = getData($offen_result); + $offenestunden_werte = array_values(array_unique(array_column($rows, 'offenestunden'))); + + if (count($offenestunden_werte) > 1) + return error('Offene Stunden sind nicht eindeutig'); + + $rest = $offenestunden_werte[0]; + + if ($rest <= 0) + return error('Es sind bereits alle Stunden verplant'); + + $studiensemester = $this->_ci->StudiensemesterModel->loadWhere(array('studiensemester_kurzbz' => $studiensemester_kurzbz)); + if (!hasData($studiensemester)) + return error('Studiensemester nicht gefunden'); + $semesterende = getData($studiensemester)[0]->ende; + + $start_time = (new DateTime($start_date))->format('H:i:s'); + $start_stunde_result = $this->_ci->StundeModel->loadWhere(array('beginn' => $start_time)); + + if (!hasData($start_stunde_result)) + return error('Startzeit entspricht keinem gültigen Raster'); + + $start_stunde_nr = getData($start_stunde_result)[0]->stunde; + + $current_date = (new DateTime($start_date))->format('Y-m-d'); + $ende = new DateTime($semesterende); + + $plan = []; + $skipped_weeks = []; + $errors = []; + + $original_ignore = $this->_ci->variablelib->getVar('ignore_kollision'); + + $this->_ci->VariableModel->setVariable( + getAuthUID(), + 'ignore_kollision', + 'false' + ); + + $this->_ci->KalenderModel->db->trans_start(); + + while ($rest > 0 && new DateTime($current_date) < $ende) + { + $max_skip = 52; + $skip_count = 0; + while ($this->_isFerien($current_date) && $skip_count < $max_skip) + { + $skipped_weeks[] = $current_date; + $current_date = $this->_addWeeks($current_date, $wochenrythmus); + $skip_count++; + } + + if ($skip_count >= $max_skip) + { + $errors[] = ['message' => 'Keine ferienfreie Woche bis Semesterende gefunden.']; + break; + } + + if (new DateTime($current_date) >= $ende) + break; + + $current_block = ($rest < $block) ? $rest : $block; + + $end_stunde_nr = $start_stunde_nr + $current_block - 1; + $end_stunde_result = $this->_ci->StundeModel->loadWhere(array('stunde' => $end_stunde_nr)); + + if (!hasData($end_stunde_result)) + { + $errors[] = [ + 'datum' => $current_date, + 'message' => 'Endzeit entspricht keinem gültigen Raster', + ]; + $current_date = $this->_addWeeks($current_date, $wochenrythmus); + continue; + } + + $end_time = getData($end_stunde_result)[0]->ende; + + $start_str = $current_date . ' ' . $start_time; + $end_str = $current_date . ' ' . $end_time; + + $insert = $this->_insertKalenderEventRaw($start_str, $end_str, $lehreinheit_id, $ort_kurzbz); + $kalender_id = $insert['kalender_id']; + $insert_result = $insert['result']; + + $collisions = isError($insert_result) ? getError($insert_result) : []; + + $raum_vorschlaege = []; + if ($kalender_id) + { + $vorschlaege = $this->_ci->raumvorschlaglib->getVorschlaege($kalender_id); + $raum_vorschlaege = is_array($vorschlaege) ? $vorschlaege : []; + } + + $plan[] = [ + 'datum' => $current_date, + 'start' => $start_str, + 'ende' => $end_str, + 'block' => $current_block, + 'ort_kurzbz' => $ort_kurzbz, + 'collisions' => $collisions, + 'raum_vorschlaege' => $raum_vorschlaege, + ]; + + $rest -= $current_block; + $current_date = $this->_addWeeks($current_date, $wochenrythmus); + } + $this->_ci->KalenderModel->db->trans_rollback(); - return $kalenderresult; + + $this->_ci->VariableModel->setVariable( + getAuthUID(), + 'ignore_kollision', + $original_ignore + ); + + return success([ + 'plan' => $plan, + 'skipped_weeks' => $skipped_weeks, + 'errors' => $errors, + 'rest_offen' => max(0, $rest), + 'lehreinheit_id' => $lehreinheit_id, + 'ort_kurzbz' => $ort_kurzbz, + ]); + } + + public function confirmMultiWeekPlan($plan, $lehreinheit_id) + { + $created = []; + $errors = []; + + $original_ignore = $this->_ci->variablelib->getVar('ignore_kollision'); + $this->_ci->VariableModel->setVariable( + getAuthUID(), + 'ignore_kollision', + 'true' + ); + foreach ($plan as $termin) + { + $ort_kurzbz = $termin['selected_ort_kurzbz'] ?? $termin['ort_kurzbz']; + + $result = $this->addKalenderEvent($termin['start'], $termin['ende'], $lehreinheit_id, $ort_kurzbz); + + if (isError($result)) + { + $errors[] = [ + 'datum' => $termin['datum'], + 'message' => getError($result), + ]; + } + else + { + $created[] = getData($result); + } + } + + $this->_ci->VariableModel->setVariable( + getAuthUID(), + 'ignore_kollision', + $original_ignore + ); + + return success([ + 'created' => $created, + 'errors' => $errors, + ]); + } + + + public function addKalenderEvent($start_date, $end_date, $lehreinheit_id, $ort_kurzbz) + { + $this->_ci->KalenderModel->db->trans_start(); + + $insert = $this->_insertKalenderEventRaw($start_date, $end_date, $lehreinheit_id, $ort_kurzbz); + $result = $insert['result']; + + if (isError($result)) + { + $this->_ci->KalenderModel->db->trans_rollback(); + return $result; + } + + $this->_ci->KalenderModel->db->trans_complete(); + return $result; } public function addReservierung($titel, $beschreibung, $ort_kurzbz, $start_date, $end_date, $teilnehmer, $specialFinalGroups, $specialGroups, $groups) @@ -853,8 +1112,8 @@ class KalenderLib }, getData($calendarResources)); } - $kalender_entry->ort_kurzbz = $ort_kurzbz ?? $kalender_entry->ort_kurzbz; $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; @@ -862,7 +1121,7 @@ class KalenderLib if (!empty($errors)) return error($errors); - if (!is_null($ort_kurzbz) && !isEmptyString($ort_kurzbz)) + if (!is_null($ort_kurzbz) && !empty($ort_kurzbz)) { $result = $this->updateOrt($kalender_id, $ort_kurzbz); if (isError($result)) return $result; @@ -914,8 +1173,9 @@ class KalenderLib ); } - public function updateOrt($kalender_id, $ort_kurzbz) + public function updateOrt($kalender_id, $ort_kurzbz_array) { + $ort_kurzbz_array = (array)$ort_kurzbz_array; $entryResult = $this->_loadKalenderEntry($kalender_id); if (isError($entryResult)) return $entryResult; @@ -932,21 +1192,29 @@ class KalenderLib } if (in_array($kalender_entry->status_kurzbz, array('live', 'preview'))) - return $this->_createHistoryEntry($kalender_entry, null, null, $ort_kurzbz); + return $this->_createHistoryEntry($kalender_entry, null, null, $ort_kurzbz_array); } - $exist = $this->_ci->KalenderOrtModel->load(array('kalender_id' => $kalender_id)); - if (hasData($exist)) + $bestehende = $this->_ci->KalenderOrtModel->load(array('kalender_id' => $kalender_id)); + $alte_orte = hasData($bestehende) ? array_column(getData($bestehende), 'ort_kurzbz') : []; + + $zu_loeschen = array_diff($alte_orte, $ort_kurzbz_array); + $neu_anzulegen = array_diff($ort_kurzbz_array, $alte_orte); + + if (!empty($zu_loeschen)) { - return $this->_ci->KalenderOrtModel->update( - array('kalender_id' => $kalender_id), - array('ort_kurzbz' => $ort_kurzbz) - ); + $this->_ci->KalenderOrtModel->db->where('kalender_id', $kalender_id); + $this->_ci->KalenderOrtModel->db->where_in('ort_kurzbz', $zu_loeschen); + $this->_ci->KalenderOrtModel->db->delete('lehre.tbl_kalender_ort'); } - else + + foreach ($neu_anzulegen as $ort_kurzbz) { - return $this->_addKalenderOrt($kalender_id, $ort_kurzbz); + $result = $this->_addKalenderOrt($kalender_id, $ort_kurzbz); + if (isError($result)) return $result; } + + return success(); } public function updateStatus($kalender_id, $status_kurzbz) @@ -981,7 +1249,7 @@ class KalenderLib return success(); } - private function _createHistoryEntry($kalender_entry, $start_date = null, $end_date = null, $ort_kurzbz = null) + private function _createHistoryEntry($kalender_entry, $start_date = null, $end_date = null, $ort_kurzbz_array = null) { $old_id = $kalender_entry->kalender_id; @@ -1003,24 +1271,39 @@ class KalenderLib $new_kalender_id = getData($kalenderresult); - $kalenderlehreinheitresult = $this->_ci->KalenderLehreinheitModel->insert( - array( - 'kalender_id' => $new_kalender_id, - 'lehreinheit_id' => $kalender_entry->lehreinheit_id - ) - ); + $lehreinheit_ids = (array) $kalender_entry->lehreinheit_id; - if (!isSuccess($kalenderlehreinheitresult)) - return $kalenderlehreinheitresult; + foreach ($lehreinheit_ids as $lehreinheit_id) + { + $kalenderlehreinheitresult = $this->_ci->KalenderLehreinheitModel->insert( + array( + 'kalender_id' => $new_kalender_id, + 'lehreinheit_id' => $lehreinheit_id + ) + ); - $new_ort = $ort_kurzbz ? $ort_kurzbz : $kalender_entry->ort_kurzbz; - if (!is_null($new_ort) || !is_null($kalender_entry->location)) + if (!isSuccess($kalenderlehreinheitresult)) + return $kalenderlehreinheitresult; + } + + if (!empty($ort_kurzbz_array)) + { + $new_orte = (array) $ort_kurzbz_array; + $new_locations = array_fill(0, count($new_orte), null); + } + else + { + $new_orte = (array) $kalender_entry->ort_kurzbz; + $new_locations = (array) $kalender_entry->location; + } + + foreach ($new_orte as $index => $ort) { $ortresult = $this->_ci->KalenderOrtModel->insert( array( 'kalender_id' => $new_kalender_id, - 'ort_kurzbz' => $new_ort, - 'location' => $kalender_entry->location + 'ort_kurzbz' => $ort, + 'location' => $new_locations[$index] ?? null ) ); @@ -1033,9 +1316,31 @@ class KalenderLib private function _loadKalenderEntry($kalender_id) { - $this->_ci->KalenderModel->addSelect('tbl_kalender.*, tbl_kalender_lehreinheit.lehreinheit_id, tbl_kalender_ort.ort_kurzbz, tbl_kalender_ort.location'); - $this->_ci->KalenderModel->addJoin('lehre.tbl_kalender_lehreinheit', 'tbl_kalender.kalender_id = tbl_kalender_lehreinheit.kalender_id', 'LEFT'); - $this->_ci->KalenderModel->addJoin('lehre.tbl_kalender_ort', 'tbl_kalender.kalender_id = tbl_kalender_ort.kalender_id', 'LEFT'); + $this->_ci->KalenderModel->addSelect(' + tbl_kalender.*, + le.lehreinheit_id, + orte.ort_kurzbz, + orte.location + '); + + $this->_ci->KalenderModel->addJoin( + '(SELECT kalender_id, + array_agg(lehreinheit_id) AS lehreinheit_id + FROM lehre.tbl_kalender_lehreinheit + GROUP BY kalender_id) le', + 'tbl_kalender.kalender_id = le.kalender_id', + 'LEFT' + ); + + $this->_ci->KalenderModel->addJoin( + '(SELECT kalender_id, + array_agg(ort_kurzbz ORDER BY ort_kurzbz) AS ort_kurzbz, + array_agg(location ORDER BY ort_kurzbz) AS location + FROM lehre.tbl_kalender_ort + GROUP BY kalender_id) orte', + 'tbl_kalender.kalender_id = orte.kalender_id', + 'LEFT' + ); $result = $this->_ci->KalenderModel->load(array('tbl_kalender.kalender_id' => $kalender_id)); @@ -1043,7 +1348,12 @@ class KalenderLib if (!hasData($result)) return error(['message' => 'Not found', 'errorCode' => 'not_found']); - return success(getData($result)[0]); + $entry = getData($result)[0]; + $entry->lehreinheit_id = $entry->lehreinheit_id ?? []; + $entry->ort_kurzbz = $entry->ort_kurzbz ?? []; + $entry->location = $entry->location ?? []; + + return success($entry); } diff --git a/application/libraries/RaumvorschlagLib.php b/application/libraries/RaumvorschlagLib.php index 3ff37e34b..f36ef2cfa 100644 --- a/application/libraries/RaumvorschlagLib.php +++ b/application/libraries/RaumvorschlagLib.php @@ -89,62 +89,95 @@ class RaumvorschlagLib } - private function _getOrtDetails($ort_kurzbz) + private function _getOrtDetails($ort_kurzbz_array) { + $ort_kurzbz_array = (array) $ort_kurzbz_array; + if (empty($ort_kurzbz_array)) return []; $this->_ci->OrtModel->addSelect('ort_kurzbz, stockwerk, standort_id'); - $this->_ci->OrtModel->db->where('ort_kurzbz', $ort_kurzbz); + $this->_ci->OrtModel->db->where_in('ort_kurzbz', $ort_kurzbz_array); $result = $this->_ci->OrtModel->load(); - return hasData($result) ? getData($result)[0] : null; + + return hasData($result) ? getData($result) : []; } - private function _rateLektor(&$rating, $raum, $lektor_davor_ort) + private function _rateLektor(&$rating, $raum, $lektor_davor_orte) { - if (!$lektor_davor_ort) return; + if (empty($lektor_davor_orte)) return; - if ($lektor_davor_ort->ort_kurzbz === $raum->ort_kurzbz) + foreach ($lektor_davor_orte as $ort) { - $rating['score'] += 20; - $rating['details'][] = '+20 ' . $this->_ci->phraseslib->t('ui', 'lecturer_already_here'); - return; + if ($ort->ort_kurzbz === $raum->ort_kurzbz) + { + $rating['score'] += 20; + $rating['details'][] = '+20 ' . $this->_ci->phraseslib->t('ui', 'lecturer_already_here'); + return; + } } - if ($lektor_davor_ort->standort_id !== $raum->standort_id) + $gleiches_gebaeude = array_filter($lektor_davor_orte, function($ort) use ($raum) + { + return $ort->standort_id === $raum->standort_id; + }); + + if (empty($gleiches_gebaeude)) { $rating['score'] -= 20; $rating['details'][] = '-20 '. $this->_ci->phraseslib->t('ui', 'lecturer_building_change'); + return; } - elseif ($lektor_davor_ort->stockwerk !== $raum->stockwerk) + + $diffs = array_map(function($ort) use ($raum) + { + return abs($ort->stockwerk - $raum->stockwerk); + }, $gleiches_gebaeude); + + $diff = min($diffs); + + if ($diff > 0) { - $diff = abs($lektor_davor_ort->stockwerk - $raum->stockwerk); $rating['score'] -= $diff * 5; $rating['details'][] = '-' . ($diff * 5) . ' ' . $this->_ci->phraseslib->t('ui', 'lecturer_floor_change'); } } - private function _rateGruppen(&$rating, $raum, $gruppen_davor_ort) + private function _rateGruppen(&$rating, $raum, $gruppen_davor_orte) { - if (!$gruppen_davor_ort) return; + if (empty($gruppen_davor_orte)) return; - if ($gruppen_davor_ort->ort_kurzbz === $raum->ort_kurzbz) + foreach ($gruppen_davor_orte as $ort) { - $rating['score'] += 10; - $rating['details'][] = '+10 ' . $this->_ci->phraseslib->t('ui', 'student_already_here'); - return; + if ($ort->ort_kurzbz === $raum->ort_kurzbz) + { + $rating['score'] += 10; + $rating['details'][] = '+10 ' . $this->_ci->phraseslib->t('ui', 'student_already_here'); + return; + } } - if ($gruppen_davor_ort->standort_id !== $raum->standort_id) + $gleiches_gebaeude = array_filter($gruppen_davor_orte, function($ort) use ($raum) + { + return $ort->standort_id === $raum->standort_id; + }); + + if (empty($gleiches_gebaeude)) { $rating['score'] -= 20; $rating['details'][] = '-20 '. $this->_ci->phraseslib->t('ui', 'student_building_change'); + return; } - elseif ($gruppen_davor_ort->stockwerk !== $raum->stockwerk) + + $diffs = array_map(function($ort) use ($raum) + { + return abs($ort->stockwerk - $raum->stockwerk); + }, $gleiches_gebaeude); + + $diff = min($diffs); + + if ($diff > 0) { - $diff = abs($gruppen_davor_ort->stockwerk - $raum->stockwerk); $rating['score'] -= $diff * 5; $rating['details'][] = '-' . ($diff * 5) . ' '. $this->_ci->phraseslib->t('ui', 'student_floor_change'); } - - } private function _getEventDavor($events, $von, $uids, $type) diff --git a/application/libraries/collision/checks/RoomCollisionCheck.php b/application/libraries/collision/checks/RoomCollisionCheck.php index 4d72ce0cd..b59d79028 100644 --- a/application/libraries/collision/checks/RoomCollisionCheck.php +++ b/application/libraries/collision/checks/RoomCollisionCheck.php @@ -20,7 +20,7 @@ class RoomCollisionCheck implements ICollisionCheck public function check($data) { - if (!isset($data->ort_kurzbz, $data->von, $data->bis, $data->kalender_id)) return []; + if (!isset($data->ort_kurzbz, $data->von, $data->bis, $data->kalender_id) || isEmptyArray($data->ort_kurzbz)) return []; if ($this->_ci->variablelib->getVar('ignore_kollision') === 'true') return []; @@ -31,10 +31,10 @@ class RoomCollisionCheck implements ICollisionCheck $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('ort_kurzbz', $data->ort_kurzbz); $result = $this->_ci->KalenderModel->loadWhere(array( 'von <' => $data->bis, 'bis >' => $data->von, - 'ort_kurzbz' => $data->ort_kurzbz, )); if (isError($result)) return []; @@ -67,6 +67,7 @@ class RoomCollisionCheck implements ICollisionCheck $result = $this->_ci->KalenderModel->load(); + if (isError($result) || !hasData($result)) return []; $grouped = []; diff --git a/application/models/education/Lehreinheit_model.php b/application/models/education/Lehreinheit_model.php index 2f955c295..4c17b581e 100644 --- a/application/models/education/Lehreinheit_model.php +++ b/application/models/education/Lehreinheit_model.php @@ -395,6 +395,77 @@ EOSQL; return $this->execReadOnlyQuery($qry, $params); } + public function getOffeneStunden($lehreinheit_ids) + { + if (!is_array($lehreinheit_ids)) + $lehreinheit_ids = [$lehreinheit_ids]; + + if (empty($lehreinheit_ids)) + return success([]); + + $db = new DB_Model(); + $placeholders = implode(',', array_fill(0, count($lehreinheit_ids), '?')); + + $qry = "WITH eventdauer AS ( + SELECT + tbl_kalender_lehreinheit.lehreinheit_id, + tbl_kalender.von, + tbl_kalender.bis, + tbl_kalender.kalender_id + FROM lehre.tbl_lehreinheit + JOIN lehre.tbl_kalender_lehreinheit USING(lehreinheit_id) + JOIN lehre.tbl_kalender USING(kalender_id) + WHERE tbl_lehreinheit.lehreinheit_id IN ($placeholders) + GROUP BY tbl_kalender_lehreinheit.lehreinheit_id, tbl_kalender.von, tbl_kalender.bis, tbl_kalender.kalender_id + ), + raster AS ( + SELECT + eventdauer.lehreinheit_id, + eventdauer.kalender_id, + eventdauer.von, + eventdauer.bis, + vonstunde.stunde AS vonstunde_nr, + bisstunde.stunde AS bisstunde_nr + FROM eventdauer + LEFT JOIN lehre.tbl_stunde vonstunde ON eventdauer.von::time = vonstunde.beginn + LEFT JOIN lehre.tbl_stunde bisstunde ON eventdauer.bis::time = bisstunde.ende + ), + unterrichtsdauer AS ( + SELECT + raster.lehreinheit_id, + raster.kalender_id, + sum(EXTRACT(EPOCH FROM (LEAST(stunde.ende, raster.bis::time) - GREATEST(stunde.beginn, raster.von::time))) / 60) AS minuten + FROM raster JOIN lehre.tbl_stunde stunde ON stunde.beginn < raster.bis::time AND stunde.ende > raster.von::time + WHERE raster.vonstunde_nr IS NULL OR raster.bisstunde_nr IS NULL + GROUP BY raster.lehreinheit_id, raster.kalender_id + ), + verplantestunden AS ( + SELECT raster.lehreinheit_id, raster.kalender_id, + CASE + WHEN raster.vonstunde_nr IS NOT NULL AND raster.bisstunde_nr IS NOT NULL + THEN (raster.bisstunde_nr - raster.vonstunde_nr + 1) + ELSE coalesce(unterrichtsdauer.minuten, 0) / 45 + END AS verplantestunde + FROM raster LEFT JOIN unterrichtsdauer ON unterrichtsdauer.lehreinheit_id = raster.lehreinheit_id AND unterrichtsdauer.kalender_id = raster.kalender_id + ), + verplant_gesamt AS ( + SELECT lehreinheit_id, SUM(verplantestunde) AS verplant + FROM verplantestunden + GROUP BY lehreinheit_id + ) + SELECT + lema.lehreinheit_id, + lema.mitarbeiter_uid, + lema.planstunden, + ROUND(coalesce(verplant_gesamt.verplant, 0), 2) AS verplant, + ROUND((lema.planstunden - coalesce(verplant_gesamt.verplant, 0)), 2) AS offenestunden + FROM lehre.tbl_lehreinheitmitarbeiter lema + LEFT JOIN verplant_gesamt ON verplant_gesamt.lehreinheit_id = lema.lehreinheit_id + WHERE lema.lehreinheit_id IN ($placeholders)"; + + return $db->execReadOnlyQuery($qry, array_merge($lehreinheit_ids, $lehreinheit_ids)); + } + private function getLVTmp($stg_kz = null) { $qry = "SELECT DISTINCT ON(lehrveranstaltung_id) *, diff --git a/public/js/api/factory/tempus/kalender.js b/public/js/api/factory/tempus/kalender.js index 434063457..5c0cda753 100644 --- a/public/js/api/factory/tempus/kalender.js +++ b/public/js/api/factory/tempus/kalender.js @@ -88,6 +88,28 @@ export default { params: { lehreinheit_id, ort_kurzbz, start_date, end_date} }; }, + calculateMultiWeekPlan(lehreinheit_id, ort_kurzbz, start_date, end_date) { + return { + method: 'post', + url: '/api/frontend/v1/tempus/Kalender/calculateMultiWeekPlan', + params: { lehreinheit_id, ort_kurzbz, start_date, end_date } + }; + }, + confirmMultiWeekPlan(plan, lehreinheit_id ) { + return { + method: 'post', + url: '/api/frontend/v1/tempus/Kalender/confirmMultiWeekPlan', + params: { plan, lehreinheit_id } + }; + }, + + addToKalenderEvent(target_kalender_id, lehreinheit_id ) { + return { + method: 'post', + url: '/api/frontend/v1/tempus/Kalender/addToKalenderEvent', + params: { target_kalender_id, lehreinheit_id } + }; + }, getRaumvorschlag(kalender_id) { return { method: 'get', diff --git a/public/js/components/Calendar/Base.js b/public/js/components/Calendar/Base.js index 0833bee0d..85c64ac18 100644 --- a/public/js/components/Calendar/Base.js +++ b/public/js/components/Calendar/Base.js @@ -164,7 +164,7 @@ export default { res.start = luxon.DateTime.fromMillis(res.start, { zone: this.timezone, locale: this.locale }); else if (res.start instanceof Date) res.start = luxon.DateTime.fromJSDate(res.start, { zone: this.timezone, locale: this.locale }); - else if (typeof res.start === + else if (typeof res.start === 'string' || res.start instanceof String) res.start = luxon.DateTime.fromISO(res.start, { zone: this.timezone, locale: this.locale }); } @@ -173,7 +173,7 @@ export default { res.end = luxon.DateTime.fromMillis(res.end, { zone: this.timezone, locale: this.locale }); else if (res.end instanceof Date) res.end = luxon.DateTime.fromJSDate(res.end, { zone: this.timezone, locale: this.locale }); - else if (typeof res.end === + else if (typeof res.end === 'string' || res.end instanceof String) res.end = luxon.DateTime.fromISO(res.end, { zone: this.timezone, locale: this.locale }); } @@ -199,7 +199,10 @@ export default { // choose default mode let mode = this.mode; if (mode) - mode = mode.toLowerCase(); + { + const lower = mode.toLowerCase(); + mode = Object.keys(this.modes).find(key => key.toLowerCase() === lower) || null; + } if (!mode || !this.modes[mode]) mode = Object.keys(this.modes).find(Boolean); // start with first entry as active mode return mode || ''; @@ -332,4 +335,4 @@ export default { ` -} +} \ No newline at end of file diff --git a/public/js/components/Calendar/Base/Grid.js b/public/js/components/Calendar/Base/Grid.js index 58db5108b..bd21c053f 100644 --- a/public/js/components/Calendar/Base/Grid.js +++ b/public/js/components/Calendar/Base/Grid.js @@ -398,7 +398,8 @@ export default { this.onDrop?.({ item: [obj], start: dropStart.toISO(), - end: dropEnd.toISO() + end: dropEnd.toISO(), + ctrlKey: !!(evt?.ctrlKey || evt?.metaKey) }); }, @@ -437,7 +438,8 @@ export default { this.onDrop?.({ item: [obj], start: dropStart.toISO(), - end: dropEnd.toISO() + end: dropEnd.toISO(), + ctrlKey: !!(evt?.ctrlKey || evt?.metaKey) }); }, handleResizeStart({ edge, evt, el, event }) diff --git a/public/js/components/Calendar/Base/Grid/Line/Event.js b/public/js/components/Calendar/Base/Grid/Line/Event.js index 9b9b40bac..ba4fb57dd 100644 --- a/public/js/components/Calendar/Base/Grid/Line/Event.js +++ b/public/js/components/Calendar/Base/Grid/Line/Event.js @@ -1,10 +1,12 @@ import draggable from '../../../../../directives/draggable.js'; +import drop from '../../../../../directives/drop.js'; import CalClick from '../../../../../directives/Calendar/Click.js'; export default { name: "GridLineEvent", directives: { draggable, + drop, CalClick }, emits: [ @@ -29,6 +31,10 @@ export default { contextMenuActions: { from: "contextMenuActions", default: () => ({}) + }, + onDrop: { + from: "onDrop", + default: () => null } }, props: { @@ -60,6 +66,8 @@ export default { if (this.event.endsHere) classes.push('event-end'); } + + classes.push(`calender_id-${this.event.orig.kalender_id}`); return classes; }, dragKalenderCollection() { @@ -102,18 +110,45 @@ export default { evt.dataTransfer.setData('fhc-grab-offset-y', evt.clientY - rect.top); evt.dataTransfer.setData('fhc-grab-offset-x', evt.clientX - rect.left); }, + onDropOnCard(evt, items) { + if (this.isHeaderOrFooter || !this.onDrop) + return; + + const list = Array.isArray(items) ? items : [items]; + const obj = list[0]; + if (!obj) + return; + + if ((evt.ctrlKey || evt.metaKey) && obj.type === 'lehreinheit') + { + return this.onDrop({ + item: [obj], + ctrlKey: true, + targetKalenderId: this.event.orig?.kalender_id ?? null + }); + } + + return this.onDrop({ + item: [obj], + start: this.event.start.toISO(), + end: this.event.end.toISO(), + ctrlKey: false, + targetKalenderId: null + }); + }, }, template:`
- {{ event.orig.topic || event.orig.titel || event.orig.lehrfach }} + {{ topicString || event.orig.titel || event.orig.lehrfach }}
{{ event.orig.datum }} {{ event.orig.beginn }}–{{ event.orig.ende }} diff --git a/public/js/components/Calendar/Mode/Table/View.js b/public/js/components/Calendar/Mode/Table/View.js index 52875f23b..cee1bfe7d 100644 --- a/public/js/components/Calendar/Mode/Table/View.js +++ b/public/js/components/Calendar/Mode/Table/View.js @@ -8,7 +8,8 @@ export default { name: "TableView", inject: { events: "events", - timezone: "timezone" + timezone: "timezone", + tableActions: "tableActions", }, components: { CoreFilterCmpt, @@ -91,6 +92,19 @@ export default { { openModal() { this.$refs.raumModal.show(); + }, + async deleteSelected() { + let selected = this.$refs.tableViewTable?.tabulator?.getSelectedData() ?? []; + + if (!selected.length) return; + + let isConfirmed = await this.$fhcAlert.confirmDelete(); + + if (!isConfirmed) return; + + await this.tableActions?.deleteEntries(selected) + + this.$refs.tableViewTable.tabulator.deselectRow(); } }, watch: { @@ -98,7 +112,7 @@ export default { this.$refs.tableViewTable?.tabulator?.setData(newData); } }, - mounted() { +/* mounted() { this.$api.call(ApiDetails.getRaumtyp()) .then(result => { @@ -106,7 +120,7 @@ export default { }) .catch(this.$fhcAlert.handleSystemError); - }, + },*/ template: /* html */`
- +
` } \ No newline at end of file diff --git a/public/js/components/Calendar/Tempus.js b/public/js/components/Calendar/Tempus.js index 4a4587313..b9447ba7e 100644 --- a/public/js/components/Calendar/Tempus.js +++ b/public/js/components/Calendar/Tempus.js @@ -9,57 +9,65 @@ import ApiKalender from "../../api/factory/tempus/kalender.js"; import draggable from "../../directives/draggable.js"; export default { - name: "CalendarTempus", - components: { - FhcCalendar, - }, - inject: { - renderers: { from: "renderers" }, - appConfig: { - from: "appConfig", - default: { - visible_status: "all", - }, - }, - }, - directives: { - draggable, - }, - props: { - timezone: { - type: String, - required: true, - }, - date: { - type: [Date, String, Number, luxon.DateTime], - default: luxon.DateTime.local(), - }, - mode: { - type: String, - default: "Week", - }, - getPromiseFunc: { - type: Function, - required: true, - }, - parkedEvents: { - type: Object, - default: () => new Set(), - }, - visibleLecturers: { - type: Array, - default: null, - }, - extraBackgrounds: { - type: Array, - default: () => [], - }, - visibleStatus: { - type: Array, - default: () => ["all"], - }, - }, - emits: ["update:date", "update:mode", "update:range", "drop", "resize"], + name: "CalendarTempus", + components: { + FhcCalendar + }, + inject: { + renderers: {from: 'renderers'}, + appConfig: { + from: 'appConfig', + default: { + visible_status: 'all' + } + } + }, + directives: { + draggable, + }, + props: { + timezone: { + type: String, + required: true + }, + date: { + type: [Date, String, Number, luxon.DateTime], + default: luxon.DateTime.local() + }, + mode: { + type: String, + default: 'Week' + }, + getPromiseFunc: { + type: Function, + required: true + }, + parkedEvents: { + type: Object, + default: () => new Set() + }, + visibleLecturers: { + type: Array, + default: null + }, + extraBackgrounds: { + type: Array, + default: () => [] + }, + visibleStatus: { + type: Array, + default: () => ['all'] + }, + }, + emits: [ + "update:date", + "update:mode", + "update:range", + "drop", + "resize", + "event-hover", + "event-unhover" + ], data() { return { @@ -149,6 +157,12 @@ export default { resetEventLoader() { this.reset(); }, + navigatePrev() { + this.$refs.calendar.clickPrev(); + }, + navigateNext() { + this.$refs.calendar.clickNext(); + }, clearOutCalendarEventEmphasis() { this.$refs.calendar.$el .querySelectorAll( @@ -234,6 +248,8 @@ export default { :class="['event-type-' + event.type + ' ' + mode + 'PageContainer', { 'event--parked': parkedEvents.has(String(event.kalender_id)) }]" :type="mode == 'day' ? 'button' : undefined" :style="eventStyle(event)" + @mouseenter="$emit('event-hover', event)" + @mouseleave="$emit('event-unhover', event)" >
- `, -}; + ` +} diff --git a/public/js/components/Tempus/Coursepicker.js b/public/js/components/Tempus/Coursepicker.js index 5d13235b2..1bd43b569 100644 --- a/public/js/components/Tempus/Coursepicker.js +++ b/public/js/components/Tempus/Coursepicker.js @@ -24,21 +24,56 @@ export default { return { searchparam: '', allCourses: [], + sortBy: null, + multiWeekIds: new Set() } }, computed: { courses() { const query = (this.searchparam ?? '').trim().toLowerCase(); - if (!query) - return this.allCourses; - return this.allCourses.filter(course => - course.showname.toLowerCase().includes(query) || - course.lektoren?.some(l => - l.name.toLowerCase().includes(query) || - l.kurzbz.toLowerCase().includes(query) - ) - ); + let result; + if (query) + { + result = this.allCourses.filter(course => + course.showname.toLowerCase().includes(query) || + course.lektoren?.some(lektor => + lektor.name.toLowerCase().includes(query) || + lektor.kurzbz.toLowerCase().includes(query) + ) + ); + } + else + result = this.allCourses; + + if (this.sortBy === 'lektor-asc' || this.sortBy === 'lektor-desc') + { + let dir = this.sortBy === 'lektor-asc' ? 1 : -1; + + return result.sort((a, b) => { + let an = a.lektoren?.[0]?.kurzbz ?? ''; + let bn = b.lektoren?.[0]?.kurzbz ?? ''; + return an.localeCompare(bn) * dir; + }); + } + else if (this.sortBy === 'kw-asc') + { + return result.sort((a, b) => (a.start_kw ?? 0) - (b.start_kw ?? 0)); + } + else if (this.sortBy === 'kw-desc') + { + return result.sort((a, b) => (b.start_kw ?? 0) - (a.start_kw ?? 0)); + } + else if (this.sortBy === 'stunden-asc') + { + return result.sort((a, b) => (a.offenestunden ?? 0) - (b.offenestunden ?? 0)); + } + else if (this.sortBy === 'stunden-desc') + { + return result.sort((a, b) => (b.offenestunden ?? 0) - (a.offenestunden ?? 0)); + } + + return result; } }, watch: { @@ -94,6 +129,7 @@ export default { id: orig.lehreinheit_id, orig: orig, stundenblockung: course.stundenblockung, + multiweek: this.isMultiWeek(course), }; }, courseStyle(course) { @@ -104,7 +140,44 @@ export default { }, selectLecturer(lektor) { this.$emit('select-lecturer', lektor); - } + }, + setSort(field) { + if (this.sortBy === `${field}-asc`) + this.sortBy = `${field}-desc`; + else if (this.sortBy === `${field}-desc`) + this.sortBy = null; + else + this.sortBy = `${field}-asc`; + }, + sortIcon(field) { + if (this.sortBy === `${field}-asc`) + return 'fa-arrow-up'; + if (this.sortBy === `${field}-desc`) + return 'fa-arrow-down'; + return 'fa-sort'; + }, + isSortActive(field) { + return this.sortBy === `${field}-asc` || this.sortBy === `${field}-desc`; + }, + reload() + { + this.loadCoursesByStg(this.stg); + }, + toggleMultiWeek(course) + { + let id = course.lehreinheit_id[0]; + let weekIds = new Set(this.multiWeekIds); + if (weekIds.has(id)) + weekIds.delete(id); + else + weekIds.add(id); + + this.multiWeekIds = weekIds; + }, + isMultiWeek(course) + { + return this.multiWeekIds.has(course.lehreinheit_id[0]); + }, }, template: `
@@ -114,6 +187,36 @@ export default { type="text" v-model="searchparam" /> +
+ + + + +
Keine Lehreinheiten @@ -123,6 +226,7 @@ export default {
{{ course.lehrfach }} {{ course.lehrform }} {{ course.raumtyp }} +
diff --git a/public/js/components/Tempus/KeyboardShortcuts.js b/public/js/components/Tempus/KeyboardShortcuts.js new file mode 100644 index 000000000..6ab3310e4 --- /dev/null +++ b/public/js/components/Tempus/KeyboardShortcuts.js @@ -0,0 +1,56 @@ +export default { + name: "KeyboardShortcuts", + props: { + shortcuts: { + type: Array, + default: () => [] + }, + }, + methods: { + isInputFocused(target) + { + if (!target) + return false; + + return (target instanceof HTMLInputElement || target instanceof HTMLTextAreaElement || target.isContentEditable) + }, + shortcutMatches(shortcut, e) + { + let shortcutWantsCtrl = shortcut.ctrl === true; + let ctrlIsPressed = e.ctrlKey || e.metaKey; + + if (shortcutWantsCtrl !== ctrlIsPressed) + return false; + + if (e.key !== shortcut.key) + return false; + + let shouldSkipWhenTyping = shortcut.skipWhenTyping !== false; + return !(shouldSkipWhenTyping && this.isInputFocused(e.target)); + }, + handleKeydown(e) + { + for (let shortcut of this.shortcuts) + { + if (!this.shortcutMatches(shortcut, e)) + continue; + + let shouldPreventDefault = shortcut.preventDefault !== false; + if (shouldPreventDefault) + e.preventDefault(); + + shortcut.handler(e); + return; + } + } + }, + mounted() { + window.addEventListener('keydown', this.handleKeydown); + }, + beforeUnmount() { + window.removeEventListener('keydown', this.handleKeydown); + }, + render() { + return null; + } +}; \ No newline at end of file diff --git a/public/js/components/Tempus/MultiWeekPlanModal.js b/public/js/components/Tempus/MultiWeekPlanModal.js new file mode 100644 index 000000000..88fbbde9f --- /dev/null +++ b/public/js/components/Tempus/MultiWeekPlanModal.js @@ -0,0 +1,191 @@ +import ApiKalender from '../../api/factory/tempus/kalender.js'; +import BsModal from '../../../js/components/Bootstrap/Modal.js'; +export default { + components: { + BsModal, + }, + props: { + show: { type: Boolean, default: false }, + lehreinheitId: { type: [String, Number], default: null }, + ortKurzbz: { type: String, default: null }, + startTime: { type: String, default: null }, + endTime: { type: String, default: null }, + }, + emits: ['close', 'confirmed'], + data() { + return { + loading: false, + plan: [], + skippedWeeks: [], + errors: [], + restOffen: 0, + }; + }, + watch: { + show(val) { + if (val) { + this.loadPlan(); + this.$refs.bsModal.show(); + } + else { + this.$refs.bsModal.hide(); + } + }, + }, + computed: { + countTermin() { + return (this.plan.filter(termin => termin.planen).length); + }, + formattedSkippedWeeks() { + return this.skippedWeeks.map(week => luxon.DateTime.fromISO(week).toFormat('dd.MM.yyyy')).join('\n'); + } + }, + methods: { + async loadPlan() { + this.loading = true; + this.plan = []; + this.errors = []; + this.skippedWeeks = []; + this.restOffen = 0; + + await this.$api.call( + ApiKalender.calculateMultiWeekPlan( + this.lehreinheitId, + this.ortKurzbz, + this.startTime, + this.endTime + )) + .then(result => result.data) + .then(result => { + this.plan = (result.plan || []).map(termin => ({ + ...termin, + selected_ort_kurzbz: termin.raum_vorschlaege?.[0]?.ort_kurzbz ?? termin.ort_kurzbz, + planen: (termin.collisions && termin.collisions.length > 0) ? false : true, + })); + + this.skippedWeeks = result.skipped_weeks || []; + this.errors = result.errors || []; + this.restOffen = result.rest_offen || 0; + this.loading = false; + }) + .catch(error => { + this.$fhcAlert.handleSystemError(error); + this.close(); + }); + }, + + async confirm() { + this.loading = true; + + await this.$api.call( + ApiKalender.confirmMultiWeekPlan(this.plan.filter(termin => termin.planen), this.lehreinheitId) + ); + + this.$emit('confirmed'); + this.close(); + }, + + close() { + this.loading = false; + this.$emit('close'); + }, + hasCollision(termin) { + return termin.collisions && termin.collisions.length > 0; + }, + roomOptions(termin) { + return termin.raum_vorschlaege || []; + }, + formatDate(date) + { + return luxon.DateTime.fromISO(date).toFormat('dd.MM.yyyy'); + } + + }, + template: ` + + + +
+ Wird berechnet... +
+ +
+
+ Achtung: {{ restOffen }} Stunden konnten nicht verplant werden (Semesterende erreicht). +
+ +
+ Übersprungende Ferienwochen:
{{ formattedSkippedWeeks }}
+
+ +
+
{{ err.message }}
+
+ + + + + + + + + + + + + + + + + + + + +
DatumZeitCollisionsRaumPlanen?
{{ formatDate(termin.datum) }}{{ termin.start.split(' ')[1] }} - {{ termin.ende.split(' ')[1] }} + +
{{ termin.collisions.map(c => c.message).join('\\n') }}
+
+ + --- + +
+ + Kein Raum verfügbar + +
+ +
+
+
+ + +
+ ` +} \ No newline at end of file diff --git a/public/js/components/Tempus/Renderer/Lehreinheit/calendarEvent.js b/public/js/components/Tempus/Renderer/Lehreinheit/calendarEvent.js index 51cd5ba8f..7f6190d7d 100644 --- a/public/js/components/Tempus/Renderer/Lehreinheit/calendarEvent.js +++ b/public/js/components/Tempus/Renderer/Lehreinheit/calendarEvent.js @@ -15,6 +15,17 @@ export default { return classes; }, + topicString() { + return Array.isArray(this.event.topic) ? this.event.topic.join(', ') : this.event.topic; + }, + ortString() { + return Array.isArray(this.event.ort_kurzbz) ? this.event.ort_kurzbz.join(', ') : this.event.ort_kurzbz; + }, + gruppeString() { + return Array.isArray(this.event.gruppe) + ? this.event.gruppe.map(gruppe => gruppe.bezeichnung).join(', ') + : this.event.gruppe; + }, tooltipString() { const tooltipArray = []; @@ -25,14 +36,21 @@ export default { tooltipArray.push([ this.$p.t('profilUpdate/topic'), - this.event.topic + this.topicString ].join(": ")); tooltipArray.push([ this.$p.t('person/ort'), - this.event.ort_kurzbz + this.ortString ].join(": ")); + if (this.gruppeString) { + tooltipArray.push([ + this.$p.t('lehre/gruppe'), + this.gruppeString + ].join(": ")); + } + if (Array.isArray(this.event.lektor) && this.event.lektor.length > 0) { if (this.event.lektor.length > 3) { tooltipArray.push([ @@ -80,8 +98,9 @@ export default { {{ end }}
- {{ event.topic }} - {{ event.ort_kurzbz }} + {{ topicString }} + {{ ortString }} + {{ gruppeString }} - {{event.ort_kurzbz}} + {{ortString}} diff --git a/public/js/components/Tempus/Renderer/Reservierungen/calendarEvent.js b/public/js/components/Tempus/Renderer/Reservierungen/calendarEvent.js index b0f75fa59..50be967e4 100644 --- a/public/js/components/Tempus/Renderer/Reservierungen/calendarEvent.js +++ b/public/js/components/Tempus/Renderer/Reservierungen/calendarEvent.js @@ -22,17 +22,24 @@ export default { this.$p.t('global/uhrzeit'), [this.start, this.end].join(' - ') ].join(": ")); - + tooltipArray.push([ this.$p.t('profilUpdate/topic'), - this.event.topic + this.topicString ].join(": ")); - + tooltipArray.push([ this.$p.t('person/ort'), - this.event.ort_kurzbz + this.ortString ].join(": ")); - + + if (this.gruppeString) { + tooltipArray.push([ + this.$p.t('lehre/gruppe'), + this.gruppeString + ].join(": ")); + } + if (Array.isArray(this.event.lektor) && this.event.lektor.length > 0) { if (this.event.lektor.length > 3) { tooltipArray.push([ @@ -47,7 +54,22 @@ export default { ].join(": ")); } } - + + if (Array.isArray(this.event.teilnehmer_person) && this.event.teilnehmer_person.length > 0) { + if (this.event.teilnehmer_person.length > 3) { + tooltipArray.push([ + this.$p.t('lehre/teilnehmer'), + this.event.teilnehmer_person.slice(0, 3).map(person => `${person.vorname} ${person.nachname}`).join("\n") + + "\n" + this.$p.t('lehre/weitereTeilnehmer', [this.event.teilnehmer_person.length - 3]) + ].join(": ")); + } else { + tooltipArray.push([ + this.$p.t('lehre/teilnehmer'), + this.event.teilnehmer_person.map(person => `${person.vorname} ${person.nachname}`).join("\n") + ].join(": ")); + } + } + return tooltipArray.join("\n"); }, start() { @@ -59,7 +81,18 @@ export default { return luxon.Duration .fromISOTime(this.event.ende) .toISOTime({ suppressSeconds: true }); - } + }, + ortString() { + return Array.isArray(this.event.ort_kurzbz) ? this.event.ort_kurzbz.join(', ') : this.event.ort_kurzbz; + }, + topicString() { + return Array.isArray(this.event.topic) ? this.event.topic.join(', ') : this.event.topic; + }, + gruppeString() { + return Array.isArray(this.event.teilnehmer_gruppe) + ? this.event.teilnehmer_gruppe.map(gruppe => gruppe.gruppe_kurzbz).join(', ') + : this.event.teilnehmer_gruppe; + }, }, template: /* html */`
{{ end }}
- {{ event.topic }} + {{ topicString }} ... +{{ event.lektor.length - 3 }} - {{ event.ort_kurzbz }} + + {{ person.vorname }} {{ person.nachname }} + + + ... +{{ event.teilnehmer_person.length - 3 }} + + {{ ortString }} + {{ gruppeString }}
`, -} +} \ No newline at end of file diff --git a/public/js/components/Tempus/Renderer/Reservierungen/modalContent.js b/public/js/components/Tempus/Renderer/Reservierungen/modalContent.js index 8e145af9f..121c70935 100644 --- a/public/js/components/Tempus/Renderer/Reservierungen/modalContent.js +++ b/public/js/components/Tempus/Renderer/Reservierungen/modalContent.js @@ -46,7 +46,10 @@ export default { return this.event.ende; } return numberPadding(this.event.ende.getHours()) + ":" + numberPadding(this.event.ende.getMinutes()); - } + }, + ortString() { + return Array.isArray(this.event.ort_kurzbz) ? this.event.ort_kurzbz.join(', ') : this.event.ort_kurzbz; + }, }, methods: { mehtodNumberPadding: function (number) { @@ -93,7 +96,7 @@ export default { }} - {{event.ort_kurzbz}} + {{ortString}} diff --git a/public/js/components/Tempus/Tempus.js b/public/js/components/Tempus/Tempus.js index 029203d34..149f7f863 100644 --- a/public/js/components/Tempus/Tempus.js +++ b/public/js/components/Tempus/Tempus.js @@ -37,6 +37,10 @@ import ApiStudiengangTree from "../../api/lehrveranstaltung/studiengangtree.js"; import StvStudiensemester from "../Stv/Studentenverwaltung/Studiensemester.js"; import FormInput from "../../../js/components/Form/Input.js"; import Reservierung from "./Reservierung.js"; +import { getTempusShortcuts } from "./shortcuts.js"; +import KeyboardShortcuts from "./KeyboardShortcuts.js"; +import { useContextMenuActions } from "../../composables/Tempus/ContextMenuActions.js"; +import MultiWeekPlanModal from "./MultiWeekPlanModal.js"; export default { name: "Tempus", @@ -56,6 +60,8 @@ export default { Multiselect: primevue.multiselect, FormInput, Reservierung, + KeyboardShortcuts, + MultiWeekPlanModal }, props: { defaultSemester: String, @@ -78,12 +84,24 @@ export default { currentSemester: this.defaultSemester, renderers: Vue.computed(() => this.renderers), appConfig: Vue.computed(() => this.appconfig), - contextMenuActions: Vue.computed(() => this.contextMenuActions), + 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: [], @@ -121,6 +139,7 @@ export default { events: null, minimized: false, currentlySelectedEvent: null, + hoveredEvent: null, //currentDay: new Date(), studiensemesterKurzbz: this.defaultSemester, lists: { @@ -164,64 +183,19 @@ export default { areFormButtonsDisplayed: false, }, currentlyUpdatedEvent: null, + multiWeekModal: { + show: false, + lehreinheitId: null, + ortKurzbz: null, + startTime: null, + endTime: null, + }, }; }, computed: { - contextMenuActions() { - return { - lehreinheit: [ - { - label: "Raumauswahl", - icon: "fa-solid fa-door-open", - action: this.openRaumauswahl, - }, - { - label: "Ressourcen zuordnen", - icon: "fa-solid fa-list", - action: this.openResourcesAssignmentModal, - }, - { - label: "Freischalten für Voransicht", - icon: "fa-solid fa-chalkboard-user", - action: (orig) => - this.$api - .call(ApiKalender.syncToLecturer(orig.kalender_id)) - .then(() => this.$refs.calendar.resetEventLoader()), - }, - { - label: "Freischalten für Live", - icon: "fa-solid fa-user-graduate", - action: (orig) => - this.$api - .call(ApiKalender.syncToStudent(orig.kalender_id)) - .then(() => this.$refs.calendar.resetEventLoader()), - }, - { - label: "History", - icon: "fa-solid fa-clock-rotate-left", - action: this.openHistory, - }, - { - label: "Delete", - icon: "fa-solid fa-calendar-xmark", - action: this.deleteEntry, - }, - ], - reservierung: [ - { - label: "Delete", - icon: "fa-solid fa-calendar-xmark", - action: this.deleteEntry, - }, - ], - }; - }, currentDay() { return luxon.DateTime.now().setZone(this.config.timezone).toISODate(); }, - currentMode() { - return "week"; - }, visibleLecturerUids() { if (!this.lecturers.length) return null; return this.lecturers @@ -244,6 +218,9 @@ export default { label: this.visibleStatusArray[status], })); }, + keyboardShortcuts() { + return getTempusShortcuts(this); + }, dropdownParsedAvailableResources() { return this.resourcesAssignmentModal.availableResources .map((unit) => { @@ -283,15 +260,33 @@ export default { this.$refs.resourcesAssignmentModal.show(); }, - async deleteEntry(orig) { - if (!orig?.kalender_id) return; + async deleteEntry(orig) + { + if (!orig?.kalender_id) + return; - await this.$api - .call(ApiKalender.deleteEntry(orig?.kalender_id)) - .then((result) => { - this.$refs.calendar.resetEventLoader(); - }); - }, + 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 @@ -301,6 +296,18 @@ export default { 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 @@ -368,9 +375,13 @@ export default { if (newDate && luxon.DateTime.isDateTime(newDate) && newDate.isValid) this.calendarDate = newDate.toISODate(); }, - handleChangeMode() { - console.log("handleChangeMode"); - }, + handleChangeMode(newMode, newDate) { + if (!newMode) + return; + this.currentMode = newMode; + if (newDate && luxon.DateTime.isDateTime(newDate) && newDate.isValid) + this.calendarDate = newDate.toISODate(); + }, toggleStatus(selected) { if (!selected || selected.length === 0) { this.visibleStatus = ["all"]; @@ -567,10 +578,12 @@ export default { dates.end_time, () => { this.$refs.calendar.resetEventLoader(); + this.$refs.coursepicker.reload(); }, () => { this.$refs.calendar.clearOutCalendarEventEmphasis(); this.$refs.calendar.resetEventLoader(); + this.$refs.coursepicker.reload(); }, ); }, @@ -585,6 +598,19 @@ export default { 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; @@ -608,6 +634,18 @@ export default { 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( @@ -617,8 +655,10 @@ export default { end_time, ), ) + .then(result => result.data) .then(() => { this.$refs.calendar.resetEventLoader(); + this.$refs.coursepicker.reload(); this.bcc.postMessage("dropped"); }); } else if (obj.type === "kalender") { @@ -634,6 +674,7 @@ export default { id: obj.orig.kalender_id, }); this.$refs.calendar.resetEventLoader(); + this.$refs.coursepicker.reload(); this.bcc.postMessage("dropped"); }, () => { @@ -651,6 +692,21 @@ export default { 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; @@ -1023,6 +1079,47 @@ export default { }); } }, + 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: { @@ -1118,6 +1215,7 @@ export default { }, template: /*html*/ `
+
`, + +
` }; diff --git a/public/js/components/Tempus/shortcuts.js b/public/js/components/Tempus/shortcuts.js new file mode 100644 index 000000000..7b8116142 --- /dev/null +++ b/public/js/components/Tempus/shortcuts.js @@ -0,0 +1,56 @@ +export function getTempusShortcuts(self) { + return [ + { + key: 'F', + ctrl: true, + skipWhenTyping: false, + handler: () => self.focusSearchbar() + }, + { + key: '1', + ctrl: true, + skipWhenTyping: false, + handler: () => self.handleChangeMode('month') + }, + { + key: '2', + ctrl: true, + skipWhenTyping: false, + handler: () => self.handleChangeMode('week') + }, + { + key: '3', + ctrl: true, + skipWhenTyping: false, + handler: () => self.handleChangeMode('tableList') + }, + { + key: 'ArrowLeft', + ctrl: true, + skipWhenTyping: false, + handler: () => self.$refs.calendar.navigatePrev() + }, + { + key: 'ArrowRight', + ctrl: true, + skipWhenTyping: false, + handler: () => self.$refs.calendar.navigateNext() + }, + { + key: 'p', + skipWhenTyping: true, + handler: () => self.parkHoveredEvent() + }, + { + key: 'P', + skipWhenTyping: true, + handler: () => self.parkHoveredEvent() + }, + { + key: 'Delete', + skipWhenTyping: true, + handler: () => self.deleteHoveredEvent() + }, + + ]; +} \ No newline at end of file diff --git a/public/js/composables/Tempus/ContextMenuActions.js b/public/js/composables/Tempus/ContextMenuActions.js new file mode 100644 index 000000000..1c0674cf2 --- /dev/null +++ b/public/js/composables/Tempus/ContextMenuActions.js @@ -0,0 +1,44 @@ +export function useContextMenuActions(handlers) +{ + return Vue.computed(() => ({ + lehreinheit: [ + { + label: 'Raumauswahl', + icon: 'fa-solid fa-door-open', + action: handlers.openRaumauswahl + }, + { + label: 'Ressourcen zuordnen', + icon: 'fa-solid fa-table-list', + action: handlers.openResourcesAssignmentModal + }, + { + label: 'Freischalten für Voransicht', + icon: 'fa-solid fa-chalkboard-user', + action: handlers.syncToLecturer + }, + { + label: 'Freischalten für Live', + icon: 'fa-solid fa-user-graduate', + action: handlers.syncToStudent + }, + { + label: 'History', + icon: 'fa-solid fa-clock-rotate-left', + action: handlers.openHistory + }, + { + label: 'Delete', + icon: 'fa-solid fa-calendar-xmark', + action: handlers.deleteEntry + }, + ], + reservierung: [ + { + label: 'Delete', + icon: 'fa-solid fa-calendar-xmark', + action: handlers.deleteEntry + }, + ] + })); +} \ No newline at end of file diff --git a/tests/cypress/cypress/e2e/specs/ui/tempus/tempus.events.cy.js b/tests/cypress/cypress/e2e/specs/ui/tempus/tempus.events.cy.js index 1ee24af16..a7142e3c0 100644 --- a/tests/cypress/cypress/e2e/specs/ui/tempus/tempus.events.cy.js +++ b/tests/cypress/cypress/e2e/specs/ui/tempus/tempus.events.cy.js @@ -37,7 +37,9 @@ context("Tempus event mutation tests", () => { const eventData = JSON.parse(eventJSON); const eventId = eventData?.id; - const originalRoom = eventData?.orig?.ort_kurzbz; + let originalRoom = eventData?.orig?.ort_kurzbz; + if (Array.isArray(originalRoom)) originalRoom = originalRoom.length ? originalRoom[0] : ""; + expect(eventId, "planner event id").to.exist; expect(originalRoom, "original event room").to.be.a("string").and.not.be .empty; @@ -80,7 +82,9 @@ context("Tempus event mutation tests", () => { const eventData = JSON.parse(eventJSON); const eventId = eventData?.id; - const originalRoom = eventData?.orig?.ort_kurzbz; + let originalRoom = eventData?.orig?.ort_kurzbz; + if (Array.isArray(originalRoom)) originalRoom = originalRoom.length ? originalRoom[0] : ""; + expect(eventId, "planner event id").to.exist; expect(originalRoom, "original event room").to.be.a("string").and.not.be .empty; diff --git a/tests/cypress/cypress/e2e/specs/ui/tempus/tempus.filters.cy.js b/tests/cypress/cypress/e2e/specs/ui/tempus/tempus.filters.cy.js index e13f793c5..c185cfa77 100644 --- a/tests/cypress/cypress/e2e/specs/ui/tempus/tempus.filters.cy.js +++ b/tests/cypress/cypress/e2e/specs/ui/tempus/tempus.filters.cy.js @@ -16,7 +16,9 @@ context("Tempus filter tests", () => { .then((eventJSON) => { expect(eventJSON).to.exist; - const selectedRoom = JSON.parse(eventJSON)?.orig?.ort_kurzbz; + let selectedRoom = JSON.parse(eventJSON)?.orig?.ort_kurzbz; + if (Array.isArray(selectedRoom)) selectedRoom = selectedRoom.length ? selectedRoom[0] : ""; + expect(selectedRoom, "first event room").to.be.a("string").and.not.be .empty; expect( @@ -40,8 +42,11 @@ context("Tempus filter tests", () => { tempusPage.getCalendarEvents().should("have.length.greaterThan", 0); tempusPage.getCalendarEvents().each(($event) => { const eventData = tempusPage.getCalendarEventData($event); + + let eventRoom = eventData?.orig?.ort_kurzbz; + if (Array.isArray(eventRoom)) eventRoom = eventRoom.length ? eventRoom[0] : ""; - expect(eventData?.orig?.ort_kurzbz, "event data room").to.eq( + expect(eventRoom, "event data room").to.eq( selectedRoom, ); cy.wrap($event) @@ -64,7 +69,9 @@ context("Tempus filter tests", () => { .then((eventJSON) => { expect(eventJSON).to.exist; - const selectedRoom = JSON.parse(eventJSON)?.orig?.ort_kurzbz; + let selectedRoom = JSON.parse(eventJSON)?.orig?.ort_kurzbz; + if (Array.isArray(selectedRoom)) selectedRoom = selectedRoom.length ? selectedRoom[0] : ""; + console.log("Selected room:", selectedRoom); expect(selectedRoom, "first event room").to.be.a("string").and.not.be .empty; expect( diff --git a/tests/cypress/cypress/support/pages/tempus.po.js b/tests/cypress/cypress/support/pages/tempus.po.js index 458899508..4ec3e1fce 100644 --- a/tests/cypress/cypress/support/pages/tempus.po.js +++ b/tests/cypress/cypress/support/pages/tempus.po.js @@ -135,7 +135,10 @@ class TempusPage { const eventData = JSON.parse( event.getAttribute("data-fhc-draggable-value"), ); - return !!eventData?.orig?.ort_kurzbz; + + let isArray = Array.isArray(eventData?.orig?.ort_kurzbz); + + return isArray ? eventData?.orig?.ort_kurzbz.length > 0 : !!eventData?.orig?.ort_kurzbz; }); getCalendarEventsWithLecturer = () => this.getCalendarEvents().filter((index, event) => { @@ -435,6 +438,7 @@ class TempusPage { const event = this.getCalendarEventById(eventId); return event + .scrollIntoView() .should("be.visible") .drag(this.getCalendarPartDropTarget(partIndex), { waitForAnimations: false,