From 200c79edaafe29e420080ea5ceb070ed34185c02 Mon Sep 17 00:00:00 2001 From: Ivymaster Date: Thu, 30 Jul 2026 13:04:16 +0200 Subject: [PATCH] Added new event details endopint for html preview --- application/config/routes.php | 1 + application/controllers/LvPlanHTMLDetail.php | 333 ++++++++ application/libraries/KalenderLib.php | 99 ++- application/libraries/LvPlanHTMLDetailLib.php | 776 ++++++++++++++++++ application/libraries/LvPlanHTMLLib.php | 7 +- include/wochenplan.class.php | 2 - 6 files changed, 1206 insertions(+), 12 deletions(-) create mode 100644 application/controllers/LvPlanHTMLDetail.php create mode 100644 application/libraries/LvPlanHTMLDetailLib.php diff --git a/application/config/routes.php b/application/config/routes.php index d55266326..1aba832b0 100644 --- a/application/config/routes.php +++ b/application/config/routes.php @@ -130,6 +130,7 @@ $route['lvplan/ical/download'] = 'LvPlanICALDownload'; $route['lvplan/csv/download'] = 'LvPlanCSVDownload'; $route['lvplan/excel/download'] = 'LvPlanExcelDownload'; $route['lvplan/html/preview'] = 'LvPlanHTMLPreview'; +$route['lvplan/html/detail'] = 'LvPlanHTMLDetail'; // load routes from extensions, also look for environment-specific configs $subdirs = ['application/config/extensions', 'application/config/' . ENVIRONMENT . '/extensions']; diff --git a/application/controllers/LvPlanHTMLDetail.php b/application/controllers/LvPlanHTMLDetail.php new file mode 100644 index 000000000..47ccc0a08 --- /dev/null +++ b/application/controllers/LvPlanHTMLDetail.php @@ -0,0 +1,333 @@ + array('basis/cis:r') + )); + + $this->load->model('person/Benutzer_model', 'BenutzerModel'); + $this->load->model('organisation/Studiengang_model', 'StudiengangModel'); + $this->load->model('ressource/Ort_model', 'OrtModel'); + $this->load->model('ressource/Stunde_model', 'StundeModel'); + $this->loadPhrases(array( + 'global' => array('termine'), + 'ui' => array('organisierende', 'teilnehmende') + )); + } + + /** + * Serves details for one hour in the HTML timetable preview. + * + * @return void + */ + public function index() + { + $type = $this->getType(); + $datum = $this->getDate(); + $hour = $this->getHour(); + + if ($type === 'verband') + $filters = $this->getVerbandFilters(); + elseif ($type === 'ort') + $filters = $this->getRoomFilters(); + else + $filters = $this->getPersonalFilters(); + + $this->load->library('LvPlanHTMLDetailLib'); + $content = $this->lvplanhtmldetaillib->getContent( + $datum, + $hour, + $type, + $filters + ); + + $this->output + ->set_content_type('text/html', 'UTF-8') + ->set_header('X-Content-Type-Options: nosniff') + ->set_output($content); + } + + /** + * @return string Requested timetable type. + */ + private function getType() + { + $type = $this->input->get('type', true); + + if (!in_array($type, array('student', 'lektor', 'verband', 'ort'), true)) + show_error('Ungueltiger Parameter: type', 400); + + return $type; + } + + /** + * @return string Date in YYYY-MM-DD format. + */ + private function getDate() + { + $value = $this->input->get('datum', true); + if (!is_string($value) || !preg_match('/^\d{4}-\d{2}-\d{2}$/', $value)) + show_error('Ungueltiger Parameter: datum', 400); + + $date = DateTime::createFromFormat( + '!Y-m-d', + $value, + new DateTimeZone(self::TIMEZONE) + ); + $errors = DateTime::getLastErrors(); + if ($date === false + || ($errors !== false + && ($errors['warning_count'] > 0 || $errors['error_count'] > 0)) + || $date->format('Y-m-d') !== $value) + { + show_error('Ungueltiger Parameter: datum', 400); + } + + return $value; + } + + /** + * @return array Selected timetable hour. + */ + private function getHour() + { + $number = $this->getInteger('stunde', false); + $this->StundeModel->addSelect('stunde, beginn, ende'); + $result = $this->StundeModel->load($number); + + if (isError($result)) + show_error(getError($result), 500); + if (!hasData($result)) + show_error('Ungueltiger Parameter: stunde', 400); + + $hours = getData($result); + $hour = reset($hours); + $begin = isset($hour->beginn) ? (string)$hour->beginn : ''; + $end = isset($hour->ende) ? (string)$hour->ende : ''; + $beginSeconds = $this->timeToSeconds($begin); + $endSeconds = $this->timeToSeconds($end); + + if ($beginSeconds === null || $endSeconds === null || $endSeconds <= $beginSeconds) + show_error('Ungueltiges Stundenraster', 500); + + return array( + 'number' => (string)$hour->stunde, + 'begin' => $begin, + 'end' => $end + ); + } + + /** + * @return array Personal timetable filters. + */ + private function getPersonalFilters() + { + $uid = $this->input->get('pers_uid', true); + if (!is_string($uid) || $uid === '' || !check_utf8($uid)) + show_error('Ungueltiger Parameter: pers_uid', 400); + + if ($uid !== getAuthUID() + && !$this->permissionlib->isBerechtigt('basis/other_lv_plan')) + { + show_error('Forbidden', 403); + } + + $this->BenutzerModel->addSelect( + 'tbl_benutzer.uid, tbl_person.titelpre, tbl_person.vorname, ' + .'tbl_person.nachname, tbl_person.titelpost' + ); + $this->BenutzerModel->addJoin('public.tbl_person', 'person_id', 'LEFT'); + $result = $this->BenutzerModel->load(array($uid)); + + if (isError($result)) + show_error(getError($result), 500); + if (!hasData($result)) + show_error('Ungueltiger Parameter: pers_uid', 400); + + $users = getData($result); + $user = reset($users); + $nameParts = array(); + foreach (array('titelpre', 'vorname', 'nachname', 'titelpost') as $property) + { + if (isset($user->{$property}) && trim($user->{$property}) !== '') + $nameParts[] = trim($user->{$property}); + } + + return array( + 'uid' => $uid, + 'full_name' => implode(' ', $nameParts) + ); + } + + /** + * @return array Room timetable filters. + */ + private function getRoomFilters() + { + $ortKurzbz = $this->input->get('ort_kurzbz', true); + if (!is_string($ortKurzbz) || $ortKurzbz === '' || !check_utf8($ortKurzbz)) + show_error('Ungueltiger Parameter: ort_kurzbz', 400); + + $this->OrtModel->addSelect('ort_kurzbz, bezeichnung, content_id'); + $result = $this->OrtModel->load($ortKurzbz); + + if (isError($result)) + show_error(getError($result), 500); + if (!hasData($result)) + show_error('Ungueltiger Parameter: ort_kurzbz', 400); + + $rooms = getData($result); + $room = reset($rooms); + + return array( + 'ort_kurzbz' => $ortKurzbz, + 'bezeichnung' => isset($room->bezeichnung) ? $room->bezeichnung : '', + 'content_id' => isset($room->content_id) ? $room->content_id : null + ); + } + + /** + * @return array Lehrverband timetable filters. + */ + private function getVerbandFilters() + { + $stgKz = $this->getInteger('stg_kz', true); + $semester = $this->getInteger('sem', false, true); + $verband = $this->getOptionalFilter('ver'); + $gruppe = $this->getOptionalFilter('grp'); + $ortKurzbz = $this->getOptionalRoomFilter(); + + $this->StudiengangModel->addSelect( + 'studiengang_kz, UPPER(typ || kurzbz) AS stg_kurzbz' + ); + $result = $this->StudiengangModel->load($stgKz); + + if (isError($result)) + show_error(getError($result), 500); + if (!hasData($result)) + show_error('Ungueltiger Parameter: stg_kz', 400); + + $programs = getData($result); + $program = reset($programs); + + return array( + 'stg_kz' => $stgKz, + 'stg_kurzbz' => isset($program->stg_kurzbz) + ? $program->stg_kurzbz + : '', + 'sem' => $semester, + 'ver' => $verband, + 'grp' => $gruppe, + 'ort_kurzbz' => $ortKurzbz + ); + } + + /** + * Returns the room selected in a Verband preview cell, if present. + * + * @return string|null + */ + private function getOptionalRoomFilter() + { + $ortKurzbz = $this->input->get('ort_kurzbz', true); + if ($ortKurzbz === null || $ortKurzbz === '') + return null; + if (!is_string($ortKurzbz) || !check_utf8($ortKurzbz)) + show_error('Ungueltiger Parameter: ort_kurzbz', 400); + + $this->OrtModel->addSelect('ort_kurzbz'); + $result = $this->OrtModel->load($ortKurzbz); + + if (isError($result)) + show_error(getError($result), 500); + if (!hasData($result)) + show_error('Ungueltiger Parameter: ort_kurzbz', 400); + + return $ortKurzbz; + } + + /** + * @param string $name GET parameter name. + * @param bool $signed Whether negative values are allowed. + * @param bool $optional Whether the parameter may be omitted or empty. + * @return int|null + */ + private function getInteger($name, $signed, $optional = false) + { + $value = $this->input->get($name, true); + if ($optional && ($value === null || $value === '')) + return null; + + $pattern = $signed ? '/^-?\d+$/' : '/^\d+$/'; + if (!is_string($value) || !preg_match($pattern, $value)) + show_error('Ungueltiger Parameter: '.$name, 400); + + $isNegative = $signed && substr($value, 0, 1) === '-'; + $digits = $isNegative ? substr($value, 1) : $value; + $digits = ltrim($digits, '0'); + if ($digits === '') + $digits = '0'; + $normalizedValue = $isNegative && $digits !== '0' ? '-'.$digits : $digits; + + $integer = filter_var($normalizedValue, FILTER_VALIDATE_INT); + if ($integer === false || (!$signed && $integer < 0)) + show_error('Ungueltiger Parameter: '.$name, 400); + + return $integer; + } + + /** + * Returns null for the legacy wildcard values "" and "0". + * + * @param string $name GET parameter name. + * @return string|null + */ + private function getOptionalFilter($name) + { + $value = $this->input->get($name, true); + if ($value === null || $value === '' || $value === '0') + return null; + + if (!is_string($value) + || !check_utf8($value) + || mb_strlen($value) > 20 + || !preg_match('/^[[:alnum:]_-]+$/u', $value)) + { + show_error('Ungueltiger Parameter: '.$name, 400); + } + + return $value; + } + + /** + * @param string $time PostgreSQL time value. + * @return int|null + */ + private function timeToSeconds($time) + { + if (!preg_match('/^(\d{1,2}):(\d{2})(?::(\d{2})(?:\.\d+)?)?$/', $time, $matches)) + return null; + + $hours = (int)$matches[1]; + $minutes = (int)$matches[2]; + $seconds = isset($matches[3]) ? (int)$matches[3] : 0; + + if ($hours > 23 || $minutes > 59 || $seconds > 59) + return null; + + return $hours * 3600 + $minutes * 60 + $seconds; + } +} diff --git a/application/libraries/KalenderLib.php b/application/libraries/KalenderLib.php index f353d1e06..c047ad4ea 100644 --- a/application/libraries/KalenderLib.php +++ b/application/libraries/KalenderLib.php @@ -52,10 +52,18 @@ class KalenderLib tbl_kalender.typ, tbl_kalender.von, tbl_kalender.bis, + tbl_kalender.insertvon AS created_by_uid, + kalender_ersteller_person.titelpre AS created_by_titelpre, + kalender_ersteller_person.titelpost AS created_by_titelpost, + kalender_ersteller_person.vorname AS created_by_vorname, + kalender_ersteller_person.nachname AS created_by_nachname, tbl_kalender_ort.ort_kurzbz, tbl_kalender_ort.ort_kurzbz as ko_ort_kurzbz, tbl_kalender_ort.location as ko_location, + kalender_ort.bezeichnung AS ko_ort_bezeichnung, + kalender_ort.content_id AS ko_ort_content_id, tbl_lehreinheit.lehreinheit_id, + tbl_lehreinheit.unr, tbl_lehreinheit.lehrveranstaltung_id, tbl_lehreinheit.lehrfach_id, tbl_lehreinheit.lehrform_kurzbz, @@ -64,9 +72,12 @@ class KalenderLib lehrfach.bezeichnung AS lehrfach_bezeichnung, lehrfach.farbe, COALESCE(organisator.uid, tbl_lehreinheitmitarbeiter.mitarbeiter_uid) as mitarbeiter_uid, + COALESCE(reservierung_person.titelpre, tbl_person.titelpre) as titelpre, + COALESCE(reservierung_person.titelpost, tbl_person.titelpost) as titelpost, COALESCE(reservierung_person.vorname, tbl_person.vorname) as vorname, COALESCE(reservierung_person.nachname, tbl_person.nachname) as nachname, COALESCE(reservierung_ma.kurzbz, tbl_mitarbeiter.kurzbz) AS ma_kurzbz, + COALESCE(reservierung_ma.personalnummer, tbl_mitarbeiter.personalnummer) AS personalnummer, teilnehmergruppe.gruppe_kurzbz as teilnehmerg_grp, teilnehmergruppe.bezeichnung as teilnehmerg_grp_bezeichnung, COALESCE ( @@ -78,6 +89,8 @@ class KalenderLib tbl_kalender_event.beschreibung, tbl_kalender_event.titel, teilmitarbeiter.kurzbz as teilnehmer_kurzbz, + teilperson.titelpre as teilnehmer_titelpre, + teilperson.titelpost as teilnehmer_titelpost, teilperson.vorname as teilnehmer_vorname, teilperson.nachname as teilnehmer_nachname, teilbenutzer.uid as teilnehmer_uid, @@ -103,6 +116,12 @@ class KalenderLib $this->_ci->KalenderModel->addJoin('lehre.tbl_kalender_lehreinheit', 'tbl_kalender.kalender_id = tbl_kalender_lehreinheit.kalender_id', 'LEFT'); $this->_ci->KalenderModel->addJoin('lehre.tbl_kalender_event', 'tbl_kalender.kalender_id = tbl_kalender_event.kalender_id', 'LEFT'); + $this->_ci->KalenderModel->addJoin('public.tbl_benutzer kalender_ersteller', 'tbl_kalender.insertvon = kalender_ersteller.uid', 'LEFT'); + $this->_ci->KalenderModel->addJoin( + 'public.tbl_person kalender_ersteller_person', + 'kalender_ersteller.person_id = kalender_ersteller_person.person_id', + 'LEFT' + ); $this->_ci->KalenderModel->addJoin('lehre.tbl_kalender_event_teilnehmer organisator', 'tbl_kalender_event.kalender_id = organisator.kalender_id AND organisator.rolle_kurzbz = \'organisator\'', 'LEFT'); $this->_ci->KalenderModel->addJoin('lehre.tbl_kalender_event_teilnehmer teilnehmer', 'tbl_kalender_event.kalender_id = teilnehmer.kalender_id AND teilnehmer.rolle_kurzbz = \'teilnehmer\'', 'LEFT'); @@ -124,6 +143,7 @@ class KalenderLib $this->_ci->KalenderModel->addJoin('lehre.tbl_lehrveranstaltung', 'tbl_lehreinheit.lehrveranstaltung_id = tbl_lehrveranstaltung.lehrveranstaltung_id', 'LEFT'); $this->_ci->KalenderModel->addJoin('lehre.tbl_lehrveranstaltung lehrfach', 'tbl_lehreinheit.lehrfach_id = lehrfach.lehrveranstaltung_id', 'LEFT'); $this->_ci->KalenderModel->addJoin('lehre.tbl_kalender_ort', 'tbl_kalender.kalender_id = tbl_kalender_ort.kalender_id', 'LEFT'); + $this->_ci->KalenderModel->addJoin('public.tbl_ort kalender_ort', 'tbl_kalender_ort.ort_kurzbz = kalender_ort.ort_kurzbz', 'LEFT'); $this->_ci->KalenderModel->addJoin('lehre.tbl_lehreinheitmitarbeiter', 'tbl_lehreinheit.lehreinheit_id = tbl_lehreinheitmitarbeiter.lehreinheit_id', 'LEFT'); $this->_ci->KalenderModel->addJoin('public.tbl_mitarbeiter', 'tbl_mitarbeiter.mitarbeiter_uid = tbl_lehreinheitmitarbeiter.mitarbeiter_uid', 'LEFT'); @@ -131,8 +151,12 @@ class KalenderLib $this->_ci->KalenderModel->addJoin('public.tbl_person', 'tbl_person.person_id = tbl_benutzer.person_id', 'LEFT'); $this->_ci->KalenderModel->addJoin('public.tbl_mitarbeiter reservierung_ma', 'reservierung_ma.mitarbeiter_uid = organisator.uid', 'LEFT'); - $this->_ci->KalenderModel->addJoin('public.tbl_benutzer resevierung_benutzer', 'reservierung_ma.mitarbeiter_uid = resevierung_benutzer.uid', 'LEFT'); - $this->_ci->KalenderModel->addJoin('public.tbl_person reservierung_person', 'reservierung_person.person_id = resevierung_benutzer.person_id', 'LEFT'); + $this->_ci->KalenderModel->addJoin('public.tbl_benutzer reservierung_benutzer', 'organisator.uid = reservierung_benutzer.uid', 'LEFT'); + $this->_ci->KalenderModel->addJoin( + 'public.tbl_person reservierung_person', + 'reservierung_person.person_id = reservierung_benutzer.person_id', + 'LEFT' + ); $this->_ci->KalenderModel->addJoin('lehre.tbl_lehreinheitgruppe', 'tbl_lehreinheit.lehreinheit_id = tbl_lehreinheitgruppe.lehreinheit_id', 'LEFT'); $this->_ci->KalenderModel->addJoin('public.tbl_gruppe le_gruppe', 'tbl_lehreinheitgruppe.gruppe_kurzbz = le_gruppe.gruppe_kurzbz', 'LEFT'); @@ -179,19 +203,32 @@ class KalenderLib 'tooltip' => 'tip', 'status_kurzbz' => $row->status_kurzbz, 'ort_kurzbz' => [], + 'ort_details' => [], + 'locations' => [], 'ko_ort_kurzbz' => isset($row->ko_ort_kurzbz) ? $row->ko_ort_kurzbz : '', 'ko_location' => isset($row->ko_location) ? $row->ko_location : '', '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 : '', + 'lehrfach_details' => [], 'farbe' => isset($row->farbe) ? $row->farbe : '', 'lehrveranstaltung_id' => $row->lehrveranstaltung_id, 'organisationseinheit' => isset($row->oe_kurzbz) ? $row->oe_kurzbz : '', 'lehreinheit_id' => [], + 'unr' => [], 'lektor' => [], 'teilnehmer_gruppe' => [], 'teilnehmer_person' => [], 'gruppe' => [], + 'created_by' => isset($row->created_by_uid) && $row->created_by_uid !== '' + ? [ + 'uid' => $row->created_by_uid, + 'titelpre' => $row->created_by_titelpre, + 'titelpost' => $row->created_by_titelpost, + 'vorname' => $row->created_by_vorname, + 'nachname' => $row->created_by_nachname, + ] + : null, 'titel' => isset($row->titel) ? $row->titel : '', 'beschreibung' => isset($row->beschreibung) ? $row->beschreibung : '', 'topic' => [], @@ -204,9 +241,58 @@ class KalenderLib if ($row->lehreinheit_id && !in_array($row->lehreinheit_id, $events[$id]->lehreinheit_id)) $events[$id]->lehreinheit_id[] = $row->lehreinheit_id; + if (isset($row->unr) && $row->unr !== null && $row->unr !== '' + && !in_array($row->unr, $events[$id]->unr)) + { + $events[$id]->unr[] = $row->unr; + } + if (isset($row->ort_kurzbz) && $row->ort_kurzbz !== '' && !in_array($row->ort_kurzbz, $events[$id]->ort_kurzbz)) $events[$id]->ort_kurzbz[] = $row->ort_kurzbz; + if (isset($row->ort_kurzbz) && $row->ort_kurzbz !== '' + && !in_array( + $row->ort_kurzbz, + array_column($events[$id]->ort_details, 'ort_kurzbz') + )) + { + $events[$id]->ort_details[] = [ + 'ort_kurzbz' => $row->ort_kurzbz, + 'bezeichnung' => isset($row->ko_ort_bezeichnung) + ? $row->ko_ort_bezeichnung + : '', + 'content_id' => isset($row->ko_ort_content_id) + ? $row->ko_ort_content_id + : null, + ]; + } + + if (isset($row->ko_location) && $row->ko_location !== '' + && !in_array($row->ko_location, $events[$id]->locations)) + { + $events[$id]->locations[] = $row->ko_location; + } + + if (isset($row->lehrfach_id) && $row->lehrfach_id + && !in_array( + $row->lehrfach_id, + array_column($events[$id]->lehrfach_details, 'lehrfach_id') + )) + { + $events[$id]->lehrfach_details[] = [ + 'lehrfach_id' => $row->lehrfach_id, + 'kurzbz' => isset($row->lehrfach_kurzbz) + ? $row->lehrfach_kurzbz + : '', + 'bezeichnung' => isset($row->lehrfach_bezeichnung) + ? $row->lehrfach_bezeichnung + : '', + 'lehrform' => isset($row->lehrform_kurzbz) + ? $row->lehrform_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; @@ -217,16 +303,19 @@ class KalenderLib { $events[$id]->lektor[] = [ 'mitarbeiter_uid' => $row->mitarbeiter_uid, + 'titelpre' => $row->titelpre, + 'titelpost' => $row->titelpost, 'vorname' => $row->vorname, 'nachname' => $row->nachname, 'kurzbz' => $row->ma_kurzbz, + 'personalnummer' => $row->personalnummer, ]; } } if ($row->verband_grp) { - if (!in_array($row->teilnehmerg_grp, array_column($events[$id]->teilnehmer_gruppe, 'gruppe_kurzbz'))) + if (!in_array($row->verband_grp, array_column($events[$id]->teilnehmer_gruppe, 'gruppe_kurzbz'))) { $events[$id]->teilnehmer_gruppe[] = [ 'gruppe_kurzbz' => $row->verband_grp, @@ -240,7 +329,7 @@ class KalenderLib { $events[$id]->teilnehmer_gruppe[] = [ 'gruppe_kurzbz' => $row->teilnehmerg_grp, - 'bezeichnung' => 222, + 'bezeichnung' => $row->teilnehmerg_grp_bezeichnung, ]; } } @@ -267,6 +356,8 @@ class KalenderLib { $events[$id]->teilnehmer_person[] = [ 'uid' => $row->teilnehmer_uid, + 'titelpre' => $row->teilnehmer_titelpre, + 'titelpost' => $row->teilnehmer_titelpost, 'vorname' => $row->teilnehmer_vorname, 'nachname' => $row->teilnehmer_nachname, 'kurzbz' => $row->teilnehmer_kurzbz, diff --git a/application/libraries/LvPlanHTMLDetailLib.php b/application/libraries/LvPlanHTMLDetailLib.php new file mode 100644 index 000000000..2b7ff4be3 --- /dev/null +++ b/application/libraries/LvPlanHTMLDetailLib.php @@ -0,0 +1,776 @@ +ci =& get_instance(); + $this->legacyPhrases = new phrasen(getSprache()); + } + + /** + * Builds a timetable detail document for one date and hour. + * + * @param string $datum Date in YYYY-MM-DD format. + * @param array $hour Selected timetable hour. + * @param string $type Timetable type. + * @param array $filters Type-specific filters. + * @return string + */ + public function getContent($datum, array $hour, $type, array $filters) + { + if (!in_array($type, array('student', 'lektor', 'verband', 'ort'), true)) + throw new InvalidArgumentException('Unsupported timetable type'); + foreach (array('number', 'begin', 'end') as $property) + { + if (!isset($hour[$property]) || !is_scalar($hour[$property])) + throw new InvalidArgumentException('Invalid timetable hour'); + } + + $events = $this->loadEvents($datum, $type, $filters); + $events = $this->filterEventsByHour($events, $datum, $hour); + if ($type === 'verband' + && isset($filters['ort_kurzbz']) + && $filters['ort_kurzbz'] !== '') + { + $events = $this->filterEventsByRoom( + $events, + $filters['ort_kurzbz'] + ); + } + $sections = array( + 'teaching' => array(), + 'reservations' => array(), + 'events' => array() + ); + + foreach ($events as $event) + { + if ($event->type === 'lehreinheit') + $sections['teaching'][] = $event; + elseif ($event->type === 'reservierung') + $sections['reservations'][] = $event; + else + $sections['events'][] = $event; + } + + return $this->renderDocument($datum, $hour, $sections); + } + + /** + * Loads the same published calendar data used by the HTML preview. + * + * @return array + */ + private function loadEvents($datum, $type, array $filters) + { + $uid = isset($filters['uid']) && is_string($filters['uid']) + ? $filters['uid'] + : getAuthUID(); + $this->ci->load->library('KalenderLib', array('uid' => $uid)); + + if ($type === 'verband') + { + $events = $this->ci->kalenderlib->getPlanForVerband( + $datum, + $datum, + $filters['stg_kz'], + $filters['sem'], + isset($filters['ver']) ? $filters['ver'] : null, + isset($filters['grp']) ? $filters['grp'] : null + ); + } + elseif ($type === 'ort') + { + $events = $this->ci->kalenderlib->getPlanForRoom( + $datum, + $datum, + $filters['ort_kurzbz'] + ); + } + elseif ($type === 'lektor') + { + $events = $this->ci->kalenderlib->getPlanForLecturerByLecturer( + $datum, + $datum, + $filters['uid'] + ); + } + else + { + $events = $this->ci->kalenderlib->getPlanForStudentByStudent( + $datum, + $datum, + $filters['uid'] + ); + } + + return is_array($events) ? $events : array(); + } + + /** + * Keeps events that overlap the selected timetable hour. + * + * @return array + */ + private function filterEventsByHour(array $events, $datum, array $hour) + { + $timezone = new DateTimeZone(self::TIMEZONE); + $hourStart = DateTime::createFromFormat( + '!Y-m-d H:i:s', + $datum.' '.$this->normalizeTime($hour['begin']), + $timezone + ); + $hourEnd = DateTime::createFromFormat( + '!Y-m-d H:i:s', + $datum.' '.$this->normalizeTime($hour['end']), + $timezone + ); + + if ($hourStart === false || $hourEnd === false || $hourEnd <= $hourStart) + throw new InvalidArgumentException('Invalid timetable hour range'); + + $matches = array(); + foreach ($events as $event) + { + $start = $this->getEventDateTime($event, 'isostart', 'datum', 'beginn'); + $end = $this->getEventDateTime($event, 'isoend', 'datum', 'ende'); + + if ($start === null || $end === null || $end <= $start) + continue; + if ($start < $hourEnd && $end > $hourStart) + { + $matches[] = array( + 'event' => $event, + 'start' => $start, + 'end' => $end + ); + } + } + + usort($matches, function ($first, $second) { + $comparison = $first['start']->getTimestamp() + <=> $second['start']->getTimestamp(); + if ($comparison !== 0) + return $comparison; + + $firstId = isset($first['event']->kalender_id) + ? (int)$first['event']->kalender_id + : 0; + $secondId = isset($second['event']->kalender_id) + ? (int)$second['event']->kalender_id + : 0; + return $firstId <=> $secondId; + }); + + return array_map(function ($match) { + return $match['event']; + }, $matches); + } + + /** + * Keeps Verband events assigned to the room selected in the preview cell. + * + * @return array + */ + private function filterEventsByRoom(array $events, $ortKurzbz) + { + $matches = array(); + foreach ($events as $event) + { + $rooms = isset($event->ort_kurzbz) + ? $event->ort_kurzbz + : array(); + if (!is_array($rooms)) + $rooms = array($rooms); + + foreach ($rooms as $room) + { + if (is_scalar($room) && (string)$room === (string)$ortKurzbz) + { + $matches[] = $event; + continue 2; + } + } + + if (!isset($event->ort_details) || !is_array($event->ort_details)) + continue; + + foreach ($event->ort_details as $room) + { + if ($this->getNestedValue($room, 'ort_kurzbz') === (string)$ortKurzbz) + { + $matches[] = $event; + continue 2; + } + } + } + + return $matches; + } + + /** + * Renders the complete HTML document. + */ + private function renderDocument($datum, array $hour, array $sections) + { + $title = $this->phrase('lvplan', 'lehrveranstaltungsplanDetails'); + $html = ''; + $html .= ''; + $html .= ''.$this->escape($title).''; + $html .= ''; + $html .= ''; + $html .= '

'.$this->escape($this->phrase('lvplan', 'lehrveranstaltungsplan')) + .' ⇒ '.$this->escape($this->phrase('abgabetool', 'details')).'

'; + $html .= $this->escape($this->phrase('abgabetool', 'datum')).': ' + .$this->escape($this->formatDate($datum)).'
'; + $html .= $this->escape($this->phrase('global', 'stunde')).': ' + .$this->escape($hour['number']).' (' + .$this->escape(substr((string)$hour['begin'], 0, 5)).'–' + .$this->escape(substr((string)$hour['end'], 0, 5)).')

'; + + if (!empty($sections['teaching'])) + $html .= $this->renderTeachingTable($sections['teaching']); + if (!empty($sections['reservations'])) + $html .= $this->renderReservationTable($sections['reservations']); + if (!empty($sections['events'])) + $html .= $this->renderEventTable($sections['events']); + + if (empty($sections['teaching']) + && empty($sections['reservations']) + && empty($sections['events'])) + { + $html .= '

0 ' + .$this->escape($this->phrase('global', 'termine')).'

'; + } + + if (defined('MAIL_LVPLAN')) + { + $html .= '

'.$this->phrase( + 'lvplan', + 'FragenZuLvPlan', + array(constant('MAIL_LVPLAN')) + ).'

'; + } + + return $html.''; + } + + /** + * Renders teaching-unit details. + */ + private function renderTeachingTable(array $events) + { + $html = ''; + foreach (array( + array('lvplan', 'unr'), + array('lvaliste', 'lektor'), + array('lvplan', 'ort'), + array('lvaliste', 'lehrfach'), + array('global', 'bezeichnung'), + array('global', 'verband'), + array('lvplan', 'einheit'), + array('lvplan', 'info') + ) as $heading) + { + $html .= ''; + } + $html .= ''; + + foreach ($events as $index => $event) + { + $html .= ''; + $html .= $this->cell($this->renderScalarLines( + isset($event->unr) ? $event->unr : array() + )); + $html .= $this->cell($this->renderPeople( + isset($event->lektor) ? $event->lektor : array(), + 'profile' + )); + $html .= $this->cell($this->renderRooms($event)); + $html .= $this->cell($this->renderSubjectValues($event, 'kurzbz')); + $html .= $this->cell($this->renderSubjectValues($event, 'bezeichnung')); + $html .= $this->cell($this->renderTeachingGroups($event, false)); + $html .= $this->cell($this->renderTeachingGroups($event, true)); + $html .= $this->cell($this->renderDescription($event)); + $html .= ''; + } + + return $html.'
'.$this->escape($this->phrase($heading[0], $heading[1])).'
'; + } + + /** + * Renders reservation details. + */ + private function renderReservationTable(array $events) + { + $html = '

'.$this->escape($this->phrase('lvplan', 'reservierungen')).'

'; + $html .= ''; + foreach (array( + array('global', 'titel'), + array('lvplan', 'ort'), + array('global', 'person'), + array('global', 'beschreibung'), + array('lvplan', 'reserviertVon') + ) as $heading) + { + $html .= ''; + } + $html .= ''; + + foreach ($events as $index => $event) + { + $html .= ''; + $html .= $this->cell($this->escape($this->getScalar($event, 'titel'))); + $html .= $this->cell($this->renderRooms($event)); + $html .= $this->cell($this->renderParticipants($event)); + $html .= $this->cell($this->renderText( + $this->getScalar($event, 'beschreibung') + )); + $html .= $this->cell($this->renderCreator($event)); + $html .= ''; + } + + return $html.'
'.$this->escape($this->phrase($heading[0], $heading[1])).'
'; + } + + /** + * Renders non-teaching, non-reservation calendar events. + */ + private function renderEventTable(array $events) + { + $html = '

'.$this->escape($this->phrase('global', 'termine')).'

'; + $html .= ''; + foreach (array( + array('global', 'titel'), + array('lvplan', 'ort'), + array('ui', 'organisierende'), + array('ui', 'teilnehmende'), + array('global', 'beschreibung') + ) as $heading) + { + $html .= ''; + } + $html .= ''; + + foreach ($events as $index => $event) + { + $html .= ''; + $html .= $this->cell($this->escape($this->getEventTitle($event))); + $html .= $this->cell($this->renderRooms($event)); + $html .= $this->cell($this->renderPeople( + isset($event->lektor) ? $event->lektor : array(), + 'mail' + )); + $html .= $this->cell($this->renderParticipants($event)); + $html .= $this->cell($this->renderText( + $this->getScalar($event, 'beschreibung') + )); + $html .= ''; + } + + return $html.'
'.$this->escape($this->phrase($heading[0], $heading[1])).'
'; + } + + private function renderSubjectValues($event, $property) + { + $values = array(); + if (isset($event->lehrfach_details) && is_array($event->lehrfach_details)) + { + foreach ($event->lehrfach_details as $subject) + { + $value = $this->getNestedValue($subject, $property); + if ($value !== '') + $values[] = $value; + } + } + + if (empty($values)) + { + $fallback = $property === 'kurzbz' + ? $this->getScalar($event, 'lehrfach') + : $this->getScalar($event, 'lehrfach_bez'); + if ($fallback !== '') + $values[] = $fallback; + } + + return $this->renderScalarLines($values); + } + + private function renderTeachingGroups($event, $specialGroups) + { + $values = array(); + if (isset($event->gruppe) && is_array($event->gruppe)) + { + foreach ($event->gruppe as $group) + { + $groupCode = $this->getNestedValue($group, 'gruppe_kurzbz'); + if ($specialGroups && $groupCode !== '') + $values[] = $groupCode; + elseif (!$specialGroups && $groupCode === '') + { + $designation = $this->getNestedValue($group, 'bezeichnung'); + if ($designation !== '') + $values[] = $designation; + } + } + } + + return $this->renderGroupLinks($values); + } + + private function renderParticipants($event) + { + $parts = array(); + $people = $this->renderPeople( + isset($event->teilnehmer_person) ? $event->teilnehmer_person : array(), + 'mail' + ); + if ($people !== '') + $parts[] = $people; + + $groups = array(); + if (isset($event->teilnehmer_gruppe) && is_array($event->teilnehmer_gruppe)) + { + foreach ($event->teilnehmer_gruppe as $group) + { + $value = $this->getNestedValue($group, 'bezeichnung'); + if ($value === '') + $value = $this->getNestedValue($group, 'gruppe_kurzbz'); + if ($value !== '') + $groups[] = $value; + } + } + $groupLinks = $this->renderGroupLinks($groups); + if ($groupLinks !== '') + $parts[] = $groupLinks; + + return implode('
', $parts); + } + + private function renderCreator($event) + { + if (isset($event->created_by) && (is_array($event->created_by) || is_object($event->created_by))) + return $this->renderPeople(array($event->created_by), 'plain'); + + return $this->renderPeople( + isset($event->lektor) ? $event->lektor : array(), + 'plain' + ); + } + + private function renderPeople(array $people, $linkType) + { + $values = array(); + $seen = array(); + foreach ($people as $person) + { + $uid = $this->getNestedValue($person, 'mitarbeiter_uid'); + if ($uid === '') + $uid = $this->getNestedValue($person, 'uid'); + + $nameParts = array(); + foreach (array('titelpre', 'vorname', 'nachname', 'titelpost') as $property) + { + $value = trim($this->getNestedValue($person, $property)); + if ($value !== '') + $nameParts[] = $value; + } + $name = implode(' ', $nameParts); + if ($name === '') + $name = $uid !== '' ? $uid : $this->getNestedValue($person, 'kurzbz'); + if ($name === '') + continue; + + $key = $uid !== '' ? $uid : $name; + if (isset($seen[$key])) + continue; + $seen[$key] = true; + + $label = $this->escape($name); + if ($linkType === 'profile' && $uid !== '') + { + $personalNumber = $this->getNestedValue($person, 'personalnummer'); + $profileAllowed = $personalNumber === '' + || !is_numeric($personalNumber) + || (int)$personalNumber >= 0; + if ($profileAllowed) + $label = $this->link($this->getProfileUrl($uid), $label); + } + elseif ($linkType === 'mail' && $uid !== '' && defined('DOMAIN')) + { + $label = $this->link( + 'mailto:'.$uid.'@'.constant('DOMAIN'), + $label + ); + } + + $values[] = $label; + } + + return implode('
', $values); + } + + private function renderRooms($event) + { + $values = array(); + $seen = array(); + if (isset($event->ort_details) && is_array($event->ort_details)) + { + foreach ($event->ort_details as $room) + { + $code = $this->getNestedValue($room, 'ort_kurzbz'); + if ($code === '' || isset($seen[$code])) + continue; + $seen[$code] = true; + + $label = $this->escape($code); + $contentId = $this->getNestedValue($room, 'content_id'); + $title = $this->getNestedValue($room, 'bezeichnung'); + if ($contentId !== '') + $label = $this->link($this->getRoomUrl($contentId), $label, $title); + elseif ($title !== '') + $label = ''.$label.''; + + $values[] = $label; + } + } + + if (isset($event->ort_kurzbz) && is_array($event->ort_kurzbz)) + { + foreach ($event->ort_kurzbz as $code) + { + $code = is_scalar($code) ? (string)$code : ''; + if ($code === '' || isset($seen[$code])) + continue; + $seen[$code] = true; + $values[] = $this->escape($code); + } + } + + if (isset($event->locations) && is_array($event->locations)) + { + foreach ($event->locations as $location) + { + $location = is_scalar($location) ? trim((string)$location) : ''; + if ($location !== '' && !isset($seen['location:'.$location])) + { + $seen['location:'.$location] = true; + $values[] = $this->escape($location); + } + } + } + + return implode('
', $values); + } + + private function renderGroupLinks(array $groups) + { + $values = array(); + foreach ($this->uniqueScalarValues($groups) as $group) + { + $label = $this->escape($group); + if (defined('DOMAIN')) + { + $label = $this->link( + 'mailto:'.mb_strtolower($group).'@'.constant('DOMAIN'), + $label + ); + } + $values[] = $label; + } + + return implode('
', $values); + } + + private function renderScalarLines($values) + { + if (!is_array($values)) + $values = array($values); + + $escaped = array_map(array($this, 'escape'), $this->uniqueScalarValues($values)); + return implode('
', $escaped); + } + + private function uniqueScalarValues(array $values) + { + $result = array(); + $seen = array(); + foreach ($values as $value) + { + if (!is_scalar($value)) + continue; + $value = trim((string)$value); + if ($value === '' || isset($seen[$value])) + continue; + $seen[$value] = true; + $result[] = $value; + } + + sort($result, SORT_STRING | SORT_FLAG_CASE); + return $result; + } + + private function renderDescription($event) + { + $values = array(); + foreach (array('titel', 'beschreibung') as $property) + { + $value = trim($this->getScalar($event, $property)); + if ($value !== '') + $values[] = $this->renderText($value); + } + + return implode('
', array_unique($values)); + } + + private function renderText($value) + { + return nl2br($this->escape($value), false); + } + + private function getEventTitle($event) + { + foreach (array('titel', 'lehrfach_bez', 'lehrfach') as $property) + { + $value = trim($this->getScalar($event, $property)); + if ($value !== '') + return $value; + } + + return ''; + } + + private function getEventDateTime($event, $isoProperty, $dateProperty, $timeProperty) + { + $value = isset($event->{$isoProperty}) ? $event->{$isoProperty} : null; + if (($value === null || $value === '') + && isset($event->{$dateProperty}) + && isset($event->{$timeProperty})) + { + $value = $event->{$dateProperty}.' '.$event->{$timeProperty}; + } + + if ($value === null || $value === '') + return null; + + try + { + $date = is_numeric($value) + ? new DateTime('@'.(int)$value) + : new DateTime($value, new DateTimeZone(self::TIMEZONE)); + $date->setTimezone(new DateTimeZone(self::TIMEZONE)); + return $date; + } + catch (Exception $exception) + { + return null; + } + } + + private function normalizeTime($time) + { + $time = (string)$time; + if (preg_match('/^(\d{1,2}:\d{2}:\d{2})(?:\.\d+)?$/', $time, $matches)) + return $matches[1]; + if (preg_match('/^\d{1,2}:\d{2}$/', $time)) + return $time.':00'; + + return $time; + } + + private function formatDate($datum) + { + $date = DateTime::createFromFormat( + '!Y-m-d', + $datum, + new DateTimeZone(self::TIMEZONE) + ); + return $date === false ? $datum : $date->format('d.m.Y'); + } + + private function getScalar($object, $property) + { + return isset($object->{$property}) && is_scalar($object->{$property}) + ? (string)$object->{$property} + : ''; + } + + private function getNestedValue($value, $property) + { + if (is_array($value) && isset($value[$property]) && is_scalar($value[$property])) + return (string)$value[$property]; + if (is_object($value) && isset($value->{$property}) && is_scalar($value->{$property})) + return (string)$value->{$property}; + + return ''; + } + + private function cell($content) + { + return ''.$content.''; + } + + private function link($url, $escapedLabel, $title = '') + { + $html = 'escape($title).'"'; + return $html.'>'.$escapedLabel.''; + } + + private function getProfileUrl($uid) + { + return $this->getAppRoot().'cis/private/profile/index.php?' + .http_build_query(array('uid' => $uid), '', '&', PHP_QUERY_RFC3986); + } + + private function getRoomUrl($contentId) + { + return $this->getAppRoot().'cms/content.php?' + .http_build_query( + array('content_id' => $contentId), + '', + '&', + PHP_QUERY_RFC3986 + ); + } + + private function phrase($category, $phrase, array $parameters = array()) + { + if (($category === 'global' && $phrase === 'termine') + || ($category === 'ui' + && in_array($phrase, array('organisierende', 'teilnehmende'), true))) + { + return $this->ci->p->t($category, $phrase, $parameters); + } + + return $this->legacyPhrases->t($category.'/'.$phrase, $parameters); + } + + private function escape($value) + { + return htmlspecialchars((string)$value, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'); + } + + private function getAppRoot() + { + return defined('APP_ROOT') ? rtrim(APP_ROOT, '/').'/' : '/'; + } +} diff --git a/application/libraries/LvPlanHTMLLib.php b/application/libraries/LvPlanHTMLLib.php index cee64348f..058bffbb9 100644 --- a/application/libraries/LvPlanHTMLLib.php +++ b/application/libraries/LvPlanHTMLLib.php @@ -9,8 +9,6 @@ class LvPlanHTMLLib const DETAIL_WINDOW_NAME = 'Details'; const DETAIL_WINDOW_OPTIONS = 'height=320,width=550,left=0,top=0,hotkeys=0,resizable=yes,status=no,scrollbars=no,toolbar=no,location=no,menubar=no,dependent=yes'; - const ICON_EXAM = '📝'; - private $ci; public function __construct() @@ -188,9 +186,6 @@ class LvPlanHTMLLib $title = $this->getScalar($event, 'titel'); } else { $title = $this->joinValue(isset($event->topic) ? $event->topic : ''); - - if ($title !== '' && strtoupper($event->lehrform) === 'EXAM') - $title = $title . ' ' . self::ICON_EXAM; } if ($title === '') @@ -542,7 +537,7 @@ class LvPlanHTMLLib 'ort_kurzbz' => $room ); - return $this->getAppRoot().'cis/private/lvplan/stpl_detail.php?' + return $this->getAppRoot().'index.ci.php/lvplan/html/detail?' .http_build_query($parameters, '', '&', PHP_QUERY_RFC3986); } diff --git a/include/wochenplan.class.php b/include/wochenplan.class.php index a172f74f5..efc69e37f 100644 --- a/include/wochenplan.class.php +++ b/include/wochenplan.class.php @@ -593,7 +593,6 @@ class wochenplan extends basis_db echo ' excel-new '.$this->crlf; echo ' CSV-new '.$this->crlf; echo ' CSV-Outlook-new '.$this->crlf; - echo ' iCal1-new '.$this->crlf; echo ' iCal2-new'.$this->crlf; } } @@ -614,7 +613,6 @@ class wochenplan extends basis_db echo ' excel-new '.$this->crlf; echo ' CSV-new '.$this->crlf; echo ' CSV-Outlook-new '.$this->crlf; - echo ' iCal1-new '.$this->crlf; echo ' iCal2-new'.$this->crlf; } }