diff --git a/application/config/routes.php b/application/config/routes.php index 0be710fde..d55266326 100644 --- a/application/config/routes.php +++ b/application/config/routes.php @@ -125,7 +125,7 @@ $route['api/frontend/v1/stv/[sS]tudents/([WS]S[0-9]{4})/person/(:num)'] = 'api/f $route['lvplan/caldav'] = 'LvPlanCalDAV'; $route['lvplan/caldav/.*'] = 'LvPlanCalDAV'; -$route['lvplan/ical/subscription'] = 'LvPlanICALSubscription'; +$route['lvplan/ical/subscription/google'] = 'LvPlanICALSubscription'; $route['lvplan/ical/download'] = 'LvPlanICALDownload'; $route['lvplan/csv/download'] = 'LvPlanCSVDownload'; $route['lvplan/excel/download'] = 'LvPlanExcelDownload'; diff --git a/application/controllers/LvPlanCSVDownload.php b/application/controllers/LvPlanCSVDownload.php index 42900b5c1..001e262dc 100644 --- a/application/controllers/LvPlanCSVDownload.php +++ b/application/controllers/LvPlanCSVDownload.php @@ -58,7 +58,7 @@ class LvPlanCSVDownload extends Auth_Controller $verbandFilters ); - $filename = 'FH-Kalender_'.date('m_Y', $begin); + $filename = 'FH-Kalender_'.date('m_Y'); if ($target === 'outlook') $filename .= '_outlook'; $filename .= '.csv'; @@ -122,7 +122,7 @@ class LvPlanCSVDownload extends Auth_Controller private function getVerbandFilters() { $stgKz = $this->getInteger('stg_kz', true); - $semester = $this->getInteger('sem', false); + $semester = $this->getInteger('sem', false, true); $verband = $this->getOptionalFilter('ver'); $gruppe = $this->getOptionalFilter('grp'); @@ -141,9 +141,12 @@ class LvPlanCSVDownload extends Auth_Controller ); } - private function getInteger($name, $signed) + 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); diff --git a/application/controllers/LvPlanExcelDownload.php b/application/controllers/LvPlanExcelDownload.php index fb71a64e3..3e833116d 100644 --- a/application/controllers/LvPlanExcelDownload.php +++ b/application/controllers/LvPlanExcelDownload.php @@ -56,7 +56,7 @@ class LvPlanExcelDownload extends Auth_Controller $verbandFilters ); - $filename = 'FH-Kalender_'.date('m_Y', $begin).'.xls'; + $filename = 'Termine_'.date('d_m_Y').'.xls'; $this->output ->set_content_type('application/vnd.ms-excel') @@ -117,7 +117,7 @@ class LvPlanExcelDownload extends Auth_Controller private function getVerbandFilters() { $stgKz = $this->getInteger('stg_kz', true); - $semester = $this->getInteger('sem', false); + $semester = $this->getInteger('sem', false, true); $verband = $this->getOptionalFilter('ver'); $gruppe = $this->getOptionalFilter('grp'); @@ -136,9 +136,12 @@ class LvPlanExcelDownload extends Auth_Controller ); } - private function getInteger($name, $signed) + 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); diff --git a/application/controllers/LvPlanHTMLPreview.php b/application/controllers/LvPlanHTMLPreview.php index 899a25e59..685a3068c 100644 --- a/application/controllers/LvPlanHTMLPreview.php +++ b/application/controllers/LvPlanHTMLPreview.php @@ -8,6 +8,8 @@ require_once(FHCPATH.'include/functions.inc.php'); class LvPlanHTMLPreview extends Auth_Controller { const MAX_DATE_RANGE = 34560000; // 400 days + const DEFAULT_RESERVATION_MIN_DAYS = 1; + const DEFAULT_RESERVATION_END_DATE = '2099-01-01'; /** * Object initialization. @@ -22,6 +24,14 @@ class LvPlanHTMLPreview extends Auth_Controller $this->load->model('crm/Student_model', 'StudentModel'); $this->load->model('organisation/Studiengang_model', 'StudiengangModel'); $this->load->model('ressource/Ort_model', 'OrtModel'); + $this->loadPhrases( + array( + 'lvplan' => array( + 'room_not_reservable', + 'room_reservation_restricted_to_period' + ) + ) + ); } /** @@ -147,7 +157,7 @@ class LvPlanHTMLPreview extends Auth_Controller if (!is_string($ortKurzbz) || $ortKurzbz === '' || !check_utf8($ortKurzbz)) show_error('Ungueltiger Parameter: ort_kurzbz', 400); - $this->OrtModel->addSelect('ort_kurzbz, bezeichnung'); + $this->OrtModel->addSelect('ort_kurzbz, bezeichnung, reservieren'); $room = $this->OrtModel->load($ortKurzbz); if (isError($room)) @@ -157,10 +167,41 @@ class LvPlanHTMLPreview extends Auth_Controller $rooms = getData($room); $roomData = reset($rooms); + $reservationMinDays = defined('RES_TAGE_LEKTOR_MIN') + ? (int)constant('RES_TAGE_LEKTOR_MIN') + : self::DEFAULT_RESERVATION_MIN_DAYS; + $reservationEndDate = defined('RES_TAGE_LEKTOR_BIS') + ? constant('RES_TAGE_LEKTOR_BIS') + : self::DEFAULT_RESERVATION_END_DATE; + $reservationStart = jump_day( + time(), + $reservationMinDays - 1 + ); + $reservationEnd = strtotime($reservationEndDate); return array( 'ort_kurzbz' => $ortKurzbz, - 'bezeichnung' => isset($roomData->bezeichnung) ? $roomData->bezeichnung : '' + 'bezeichnung' => isset($roomData->bezeichnung) ? $roomData->bezeichnung : '', + 'room_reservable' => isset($roomData->reservieren) + && $roomData->reservieren === true, + 'room_reservation_permitted' => $this->permissionlib->isBerechtigt( + 'lehre/reservierung:begrenzt', + 'sui' + ), + 'room_reservation_start' => $reservationStart, + 'room_reservation_end' => $reservationEnd, + 'room_not_reservable_message' => $this->p->t( + 'lvplan', + 'room_not_reservable' + ), + 'room_reservation_restricted_message' => $this->p->t( + 'lvplan', + 'room_reservation_restricted_to_period', + array( + 'start_date' => date('d.m.Y', $reservationStart), + 'end_date' => date('d.m.Y', $reservationEnd) + ) + ) ); } @@ -172,7 +213,7 @@ class LvPlanHTMLPreview extends Auth_Controller // Legacy wochenplan links always carry pers_uid. It does not filter or // authorize a Lehrverband plan and is intentionally ignored here. $stgKz = $this->getInteger('stg_kz', true); - $semester = $this->getInteger('sem', false); + $semester = $this->getInteger('sem', false, true); $verband = $this->getOptionalFilter('ver'); $gruppe = $this->getOptionalFilter('grp'); @@ -217,11 +258,15 @@ class LvPlanHTMLPreview extends Auth_Controller /** * @param string $name GET parameter name. * @param bool $signed Whether negative values are allowed. - * @return int + * @param bool $optional Whether the parameter may be omitted or empty. + * @return int|null */ - private function getInteger($name, $signed) + 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)) diff --git a/application/controllers/LvPlanICALDownload.php b/application/controllers/LvPlanICALDownload.php index 6a9375220..911c5d83f 100644 --- a/application/controllers/LvPlanICALDownload.php +++ b/application/controllers/LvPlanICALDownload.php @@ -119,7 +119,7 @@ class LvPlanICALDownload extends Auth_Controller private function getVerbandFilters() { $stgKz = $this->getInteger('stg_kz', true); - $semester = $this->getInteger('sem', false); + $semester = $this->getInteger('sem', false, true); $verband = $this->getOptionalFilter('ver'); $gruppe = $this->getOptionalFilter('grp'); @@ -138,9 +138,12 @@ class LvPlanICALDownload extends Auth_Controller ); } - private function getInteger($name, $signed) + 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); diff --git a/application/controllers/LvPlanICALSubscription.php b/application/controllers/LvPlanICALSubscription.php index 7d6fa1901..b4bb393e3 100644 --- a/application/controllers/LvPlanICALSubscription.php +++ b/application/controllers/LvPlanICALSubscription.php @@ -63,7 +63,12 @@ class LvPlanICALSubscription extends FHC_Controller $content = $this->lvplanicallib->getContent( $this->uid, $this->begin, - $this->ende + $this->ende, + null, + 2, + null, + null, + 'FH-Kalender' ); $filename = 'FH-Kalender_'.date('m_Y', $this->begin).'_ical.ics'; diff --git a/application/controllers/system/MigrateKalender.php b/application/controllers/system/MigrateKalender.php index 49e895dbc..05fc9d0cb 100644 --- a/application/controllers/system/MigrateKalender.php +++ b/application/controllers/system/MigrateKalender.php @@ -264,6 +264,58 @@ class MigrateKalender extends CLI_Controller } } + /** + * Removes calendar entries created by the Stundenplan and Reservierung migrations, + * so that both imports can be started from scratch. + */ + public function resetBeforeNewStundenplandevImport() + { + $db = new DB_Model(); + + $db->db->trans_start(); + + $db->db->query('DELETE FROM sync.tbl_stundenplandev_kalender'); + $db->db->query('DELETE FROM sync.tbl_reservierung_kalender'); + + $db->db->query("DELETE FROM lehre.tbl_kalender_event_teilnehmer + WHERE kalender_id IN ( + SELECT kalender_id + FROM lehre.tbl_kalender + WHERE typ = 'reservierung' + )"); + + $db->db->query("DELETE FROM lehre.tbl_kalender_event + WHERE kalender_id IN ( + SELECT kalender_id + FROM lehre.tbl_kalender + WHERE typ = 'reservierung' + )"); + + $db->db->query("DELETE FROM lehre.tbl_kalender_lehreinheit + WHERE kalender_id IN ( + SELECT kalender_id + FROM lehre.tbl_kalender + WHERE typ = 'lehreinheit' OR typ = 'reservierung' + )"); + + $db->db->query("DELETE FROM lehre.tbl_kalender_ort + WHERE kalender_id IN ( + SELECT kalender_id + FROM lehre.tbl_kalender + WHERE typ IN ('lehreinheit', 'reservierung') + )"); + + $db->db->query("DELETE FROM lehre.tbl_kalender + WHERE typ IN ('lehreinheit', 'reservierung')"); + + $db->db->trans_complete(); + + if ($db->db->trans_status() === false) + return error('Reset before new import failed'); + + return success('Migration data reset successfully'); + } + public function migrateStundenplanBetriebsmittelEntries() { $this->setKalendarEntriesGroupIDs(); $this->setKalendarEntriesGroupIDsForChildren(); diff --git a/application/libraries/KalenderLib.php b/application/libraries/KalenderLib.php index 9157ec01f..f353d1e06 100644 --- a/application/libraries/KalenderLib.php +++ b/application/libraries/KalenderLib.php @@ -63,11 +63,12 @@ class KalenderLib lehrfach.kurzbz AS lehrfach_kurzbz, lehrfach.bezeichnung AS lehrfach_bezeichnung, lehrfach.farbe, - COALESCE(orginasator.uid, tbl_lehreinheitmitarbeiter.mitarbeiter_uid) as mitarbeiter_uid, + COALESCE(organisator.uid, tbl_lehreinheitmitarbeiter.mitarbeiter_uid) as mitarbeiter_uid, COALESCE(reservierung_person.vorname, tbl_person.vorname) as vorname, COALESCE(reservierung_person.nachname, tbl_person.nachname) as nachname, COALESCE(reservierung_ma.kurzbz, tbl_mitarbeiter.kurzbz) AS ma_kurzbz, teilnehmergruppe.gruppe_kurzbz as teilnehmerg_grp, + teilnehmergruppe.bezeichnung as teilnehmerg_grp_bezeichnung, COALESCE ( UPPER(tbl_studiengang.typ || tbl_studiengang.kurzbz) || COALESCE(verbandgruppe.semester::varchar, \'\') || @@ -103,7 +104,7 @@ 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('lehre.tbl_kalender_event_teilnehmer orginasator', 'tbl_kalender_event.kalender_id = orginasator.kalender_id AND orginasator.rolle_kurzbz = \'organisator\'', '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'); $this->_ci->KalenderModel->addJoin('public.tbl_benutzer teilbenutzer', 'teilnehmer.uid = teilbenutzer.uid', 'LEFT'); @@ -129,7 +130,7 @@ class KalenderLib $this->_ci->KalenderModel->addJoin('public.tbl_benutzer', 'tbl_mitarbeiter.mitarbeiter_uid = tbl_benutzer.uid', 'LEFT'); $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 = orginasator.uid', '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'); @@ -239,6 +240,7 @@ class KalenderLib { $events[$id]->teilnehmer_gruppe[] = [ 'gruppe_kurzbz' => $row->teilnehmerg_grp, + 'bezeichnung' => 222, ]; } } @@ -307,7 +309,7 @@ class KalenderLib $this->_ci->KalenderModel->db->where('tbl_kalender_ort.ort_kurzbz', $ort); $this->_ci->KalenderModel->db->where('tbl_kalender.status_kurzbz', 'live'); - $this->_ci->KalenderModel->addOrder('tbl_kalender.eindeutige_gruppen_id', 'DESC'); + $this->_ci->KalenderModel->addOrder('tbl_kalender.von', 'ASC'); $data = $this->_ci->KalenderModel->load(); return $this->_mapEvents($data, false); @@ -501,13 +503,13 @@ class KalenderLib return $this->_mapEvents($data); } - public function getPlanForVerband($start_date, $end_date, $studiengangKz, $semester, $verband = null, $gruppe = null) + public function getPlanForVerband($start_date, $end_date, $studiengangKz, $semester = null, $verband = null, $gruppe = null) { $this->_getBasePlan($start_date, $end_date); $db = $this->_ci->KalenderModel->db; $escapedStudiengangKz = $db->escape($studiengangKz); - $escapedSemester = $db->escape($semester); + $escapedSemester = is_null($semester) ? null : $db->escape($semester); if (is_null($verband) || trim((string)$verband) === '' || (string)$verband === '0') $verband = null; @@ -523,9 +525,13 @@ class KalenderLib ) { $lehrverbandConditions = array( $assignmentAlias.'.gruppe_kurzbz IS NULL', - $assignmentAlias.'.studiengang_kz = '.$escapedStudiengangKz, - '('.$assignmentAlias.'.semester = '.$escapedSemester.' OR '.$assignmentAlias.'.semester IS NULL)' + $assignmentAlias.'.studiengang_kz = '.$escapedStudiengangKz ); + if (!is_null($escapedSemester)) + { + $lehrverbandConditions[] = '('.$assignmentAlias.'.semester = ' + .$escapedSemester.' OR '.$assignmentAlias.'.semester IS NULL)'; + } if (!is_null($verband)) { @@ -545,14 +551,22 @@ class KalenderLib OR '.$assignmentAlias.'.gruppe::text = \'0\')'; } + $specialGroupConditions = array( + $specialGroupAlias.'.gruppe_kurzbz = '.$assignmentAlias.'.gruppe_kurzbz', + $specialGroupAlias.'.studiengang_kz = '.$escapedStudiengangKz, + $specialGroupAlias.'.direktinskription = FALSE' + ); + if (!is_null($escapedSemester)) + { + $specialGroupConditions[] = '('.$specialGroupAlias.'.semester = ' + .$escapedSemester.' OR '.$specialGroupAlias.'.semester IS NULL)'; + } + $specialGroupCondition = $assignmentAlias.'.gruppe_kurzbz IS NOT NULL AND EXISTS ( SELECT 1 FROM public.tbl_gruppe '.$specialGroupAlias.' - WHERE '.$specialGroupAlias.'.gruppe_kurzbz = '.$assignmentAlias.'.gruppe_kurzbz - AND '.$specialGroupAlias.'.studiengang_kz = '.$escapedStudiengangKz.' - AND ('.$specialGroupAlias.'.semester = '.$escapedSemester.' OR '.$specialGroupAlias.'.semester IS NULL) - AND '.$specialGroupAlias.'.direktinskription = FALSE + WHERE '.implode("\n\t\t\t\t\t\tAND ", $specialGroupConditions).' )'; return "(( diff --git a/application/libraries/LvPlanCSVLib.php b/application/libraries/LvPlanCSVLib.php index 841dad6dd..f7d090af0 100644 --- a/application/libraries/LvPlanCSVLib.php +++ b/application/libraries/LvPlanCSVLib.php @@ -133,7 +133,7 @@ class LvPlanCSVLib foreach ($events as $event) { - $eventData = $this->getEventData($event); + $eventData = $this->getEventData($event, $type); if ($eventData === null) continue; @@ -151,7 +151,7 @@ class LvPlanCSVLib * @param object $event Calendar event. * @return array|null Normalized CSV values or null for invalid dates. */ - private function getEventData($event) + private function getEventData($event, $type = null) { $start = $this->createDateTime(isset($event->isostart) ? $event->isostart : null); $end = $this->createDateTime(isset($event->isoend) ? $event->isoend : null); @@ -164,38 +164,38 @@ class LvPlanCSVLib $end->setTimezone($timezone); $isReservation = isset($event->type) && $event->type === 'reservierung'; + $title = $isReservation ? (isset($event->titel) ? $event->titel : '') : $this->joinValues(isset($event->topic) ? $event->topic : array()); + if ($type === 'verband') { + $title = ''; + } + $location = $this->joinValues( isset($event->ort_kurzbz) ? $event->ort_kurzbz : array(), ' / ' ); $groups = $this->getGroups($event); $lecturers = $this->getLecturers($event); + $participants = $this->getParticipants($event); + - if ($isReservation) - { - $description = isset($event->beschreibung) ? $event->beschreibung : ''; - } - else - { - $descriptionParts = array( + $descriptionParts = array( 'Stundenplan', $title, - isset($event->lehrfach_bez) ? $event->lehrfach_bez : '', - $lecturers, + $isReservation ? '' : (isset($event->lehrfach_bez) ? $event->lehrfach_bez : '') , + $isReservation ? $participants : $lecturers, $groups, $location, isset($event->beschreibung) ? $event->beschreibung : '' ); - $description = implode("\r\n", array_values(array_filter( - $descriptionParts, - function ($value) { - return $value !== ''; - } - ))); - } + $description = implode("\r\n", array_values(array_filter( + $descriptionParts, + function ($value) { + return $value !== ''; + } + ))); return array( 'title' => $title, @@ -288,6 +288,8 @@ class LvPlanCSVLib } } + sort($lecturers, SORT_NATURAL | SORT_FLAG_CASE); + return implode(' / ', array_unique($lecturers)); } @@ -317,6 +319,8 @@ class LvPlanCSVLib } } + sort($groups, SORT_NATURAL | SORT_FLAG_CASE); + return implode(' / ', array_unique($groups)); } @@ -370,4 +374,23 @@ class LvPlanCSVLib return null; } } + + private function getParticipants($event) + { + $values = array(); + if (!isset($event->teilnehmer_person) || !is_array($event->teilnehmer_person)) + return ''; + + foreach ($event->teilnehmer_person as $participant) + { + $value = $participant['kurzbz']; + + if ($value !== '') + $values[] = $value; + } + + sort($values, SORT_NATURAL | SORT_FLAG_CASE); + + return implode(', ', array_unique($values)); + } } diff --git a/application/libraries/LvPlanExcelLib.php b/application/libraries/LvPlanExcelLib.php index 4af18dca2..cd84c6eb8 100644 --- a/application/libraries/LvPlanExcelLib.php +++ b/application/libraries/LvPlanExcelLib.php @@ -164,7 +164,7 @@ class LvPlanExcelLib $isReservation = isset($event->type) && $event->type === 'reservierung'; $subject = $isReservation - ? (isset($event->titel) ? $event->titel : '') + ? $event->titel . " " . $this->joinValues(isset($event->ort_kurzbz) ? $event->ort_kurzbz : array()) . " - " . $this->getGroups($event) : (isset($event->lehrfach_bez) ? $event->lehrfach_bez : ''); if ($subject === '') @@ -179,12 +179,12 @@ class LvPlanExcelLib $start->format('H:i:s'), $end->format('H:i:s'), $this->joinValues(isset($event->ort_kurzbz) ? $event->ort_kurzbz : array()), - $this->getLecturers($event), + !$isReservation ? $this->getParticipants($event) : $this->getLecturers($event), $this->getGroups($event), $subject, $note, - $this->findHour($start->format('H:i:s'), $hourRaster), - $this->findHour($end->format('H:i:s'), $hourRaster) + $this->findHour($start->format('H:i:s'), $hourRaster, "start"), + $this->findHour($end->format('H:i:s'), $hourRaster, "end") ); } @@ -274,6 +274,8 @@ class LvPlanExcelLib } } + sort($lecturers, SORT_NATURAL | SORT_FLAG_CASE); + return implode(', ', array_unique($lecturers)); } @@ -303,6 +305,8 @@ class LvPlanExcelLib } } + sort($groups, SORT_NATURAL | SORT_FLAG_CASE); + return implode(',', array_unique($groups)); } @@ -311,7 +315,7 @@ class LvPlanExcelLib * @param array $hourRaster Timetable-hour raster. * @return string|int Timetable-hour number or an empty string. */ - private function findHour($time, array $hourRaster) + private function findHour($time, array $hourRaster, string $context = '') { $seconds = $this->timeToSeconds($time); if ($seconds === null || count($hourRaster) === 0) @@ -324,7 +328,10 @@ class LvPlanExcelLib foreach ($hourRaster as $hour) { - if ($seconds <= $hour['end']) + if ( + ($seconds < $hour['end'] && $context === "start") || + ($seconds <= $hour['end'] && $context === "end") + ) return $hour['stunde']; } @@ -417,4 +424,36 @@ class LvPlanExcelLib return null; } } + + private function getParticipants($event) + { + $values = array(); + if (!isset($event->teilnehmer_person) || !is_array($event->teilnehmer_person)) + return ''; + + foreach ($event->teilnehmer_person as $participant) + { + $value = $participant['vorname'] . ' ' . $participant['nachname']; + + if ($value !== '') + $values[] = $value; + } + + sort($values, SORT_NATURAL | SORT_FLAG_CASE); + + return implode(', ', array_unique($values)); + } + + private function getNestedValue($value, array $properties) + { + foreach ($properties as $property) + { + if (is_array($value) && isset($value[$property])) + return (string)$value[$property]; + if (is_object($value) && isset($value->{$property})) + return (string)$value->{$property}; + } + + return is_scalar($value) ? (string)$value : ''; + } } diff --git a/application/libraries/LvPlanHTMLLib.php b/application/libraries/LvPlanHTMLLib.php index 267acac77..cee64348f 100644 --- a/application/libraries/LvPlanHTMLLib.php +++ b/application/libraries/LvPlanHTMLLib.php @@ -9,6 +9,8 @@ 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() @@ -39,7 +41,7 @@ class LvPlanHTMLLib $hourRaster = $this->loadHourRaster(); $normalizedEvents = $this->normalizeEvents($events); - + return $this->renderDocument( $begin, $ende, @@ -180,9 +182,16 @@ class LvPlanHTMLLib continue; $isReservation = isset($event->type) && $event->type === 'reservierung'; - $title = $isReservation - ? $this->getScalar($event, 'titel') - : $this->joinValue(isset($event->topic) ? $event->topic : ''); + + $title = ''; + if ($isReservation) { + $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 === '') $title = $this->getScalar($event, 'lehrfach_bez'); @@ -200,6 +209,7 @@ class LvPlanHTMLLib isset($event->ort_kurzbz) ? $event->ort_kurzbz : '' ), 'lecturers' => $this->getLecturers($event), + 'participants' => $this->getParticipants($event), 'groups' => $this->getGroups($event), 'note' => $this->getEventNote($event, $isReservation), 'color' => $this->getColor($event) @@ -227,13 +237,14 @@ class LvPlanHTMLLib $timezone = new DateTimeZone(self::TIMEZONE); $rangeStart = (new DateTime('@'.$begin))->setTimezone($timezone)->setTime(0, 0, 0); $rangeEnd = (new DateTime('@'.$ende))->setTimezone($timezone)->setTime(23, 59, 59); + $weeks = $this->buildWeeks($rangeStart, $rangeEnd, $hourRaster, $events); $title = $this->getDocumentTitle($type, $filters); $appRoot = $this->getAppRoot(); $html = ''; $html .= ''; - $html .= ''.$this->escape($title).''; + $html .= ''.$this->escape('Kalender').''; $html .= ''; $html .= ''; $html .= ''; @@ -360,7 +371,7 @@ class LvPlanHTMLLib { $weekday = (int)$day['date']->format('N'); $html .= ''.$weekdayNames[$weekday].'
'; - $html .= $day['date']->format('d.m.Y').'
'; + $html .= $day['date']->format('j.M Y').'
'; foreach ($day['cells'] as $slotIndex => $events) { @@ -407,9 +418,51 @@ class LvPlanHTMLLib $html .= ''; } + if ($type === 'ort') + $html .= $this->renderRoomReservationNotice($week, $filters); + return $html.''; } + /** + * Renders the room reservation warning used by the legacy weekly plan. + */ + private function renderRoomReservationNotice(array $week, array $filters) + { + $requiredFilters = array( + 'room_reservable', + 'room_reservation_permitted', + 'room_reservation_start', + 'room_reservation_end', + 'room_not_reservable_message', + 'room_reservation_restricted_message' + ); + foreach ($requiredFilters as $filter) + { + if (!array_key_exists($filter, $filters)) + return ''; + } + + $weekEnd = (clone $week['end'])->modify('+1 day')->getTimestamp(); + $canReserveInWeek = $filters['room_reservation_permitted'] + && $filters['room_reservable'] + && $weekEnd >= time() + && $weekEnd >= $filters['room_reservation_start'] + && $week['start']->getTimestamp() <= $filters['room_reservation_end']; + + if ($canReserveInWeek) + return ''; + + $message = $filters['room_reservation_permitted'] + && !$filters['room_reservable'] + ? $filters['room_not_reservable_message'] + : $filters['room_reservation_restricted_message']; + + return '

' + .$this->escape($message) + .'
'; + } + /** * Renders an escaped event card. */ @@ -447,9 +500,11 @@ class LvPlanHTMLLib if (($type === 'lektor' || $type === 'verband' || $type === 'ort') && $event['groups'] !== '') $html .= '
'.$this->renderLines($event['groups']); - if ($type !== 'lektor' && $event['lecturers'] !== '') + if ($type !== 'lektor' && $type !== 'ort' && $event['lecturers'] !== '') $html .= '
'.$this->renderLines($event['lecturers']); - if ($event['location'] !== '') + if ($type === 'ort' && $event['participants'] !== '') + $html .= '
'.$this->renderLines($event['participants']); + if ($event['location'] !== '' && $type !== 'ort') $html .= '
'.$this->escape($event['location']); if ($event['note'] !== '' && $this->shouldShowNotes()) $html .= '
'.$this->escape($event['note']); @@ -520,6 +575,7 @@ class LvPlanHTMLLib private function getDocumentTitle($type, array $filters) { + //return 'Kalender'; if ($type === 'ort') { $title = 'Ort: '.$filters['ort_kurzbz']; @@ -598,13 +654,33 @@ class LvPlanHTMLLib $values[] = $value; } + sort($values, SORT_STRING | SORT_FLAG_CASE); + + return implode(', ', array_unique($values)); + } + + private function getParticipants($event) + { + $values = array(); + if (!isset($event->teilnehmer_person) || !is_array($event->teilnehmer_person)) + return ''; + + foreach ($event->teilnehmer_person as $participant) + { + $value = $this->getNestedValue($participant, array('kurzbz', 'student_uid')); + if ($value !== '') + $values[] = $value; + } + + sort($values, SORT_STRING | SORT_FLAG_CASE); + return implode(', ', array_unique($values)); } private function getGroups($event) { $values = array(); - foreach (array('gruppe', 'teilnehmer_gruppe') as $property) + foreach (array('teilnehmer_gruppe', 'gruppe') as $property) { if (!isset($event->{$property}) || !is_array($event->{$property})) continue; @@ -620,6 +696,8 @@ class LvPlanHTMLLib } } + sort($values, SORT_STRING | SORT_FLAG_CASE); + return implode(', ', array_unique($values)); } diff --git a/application/libraries/LvPlanICALLib.php b/application/libraries/LvPlanICALLib.php index 1627e4eb8..febd24d32 100644 --- a/application/libraries/LvPlanICALLib.php +++ b/application/libraries/LvPlanICALLib.php @@ -34,6 +34,7 @@ class LvPlanICALLib * @param int $version iCalendar major version (1 or 2). * @param string|null $ortKurzbz Room identifier for room plans. * @param array|null $verbandFilters Lehrverband filters. + * @param string|null $calendarName Display name for calendar subscriptions. * @return string */ public function getContent( @@ -43,7 +44,8 @@ class LvPlanICALLib $type = null, $version = 2, $ortKurzbz = null, - $verbandFilters = null + $verbandFilters = null, + $calendarName = null ) { if (!in_array($version, array(1, 2), true)) throw new InvalidArgumentException('Unsupported iCalendar version'); @@ -106,7 +108,7 @@ class LvPlanICALLib if (!is_array($events)) $events = array(); - $content = $this->buildCalendarHeader($version); + $content = $this->buildCalendarHeader($version, $calendarName); foreach ($events as $event) $content .= $this->buildEvent($event); @@ -114,15 +116,23 @@ class LvPlanICALLib return $content.$this->buildICalLine('END', 'VCALENDAR'); } - private function buildCalendarHeader($version) + private function buildCalendarHeader($version, $calendarName = null) { $productId = defined('CAMPUS_NAME') && CAMPUS_NAME !== '' ? CAMPUS_NAME : 'FHComplete'; - return $this->buildICalLine('BEGIN', 'VCALENDAR') + $header = $this->buildICalLine('BEGIN', 'VCALENDAR') .$this->buildICalLine('VERSION', $version.'.0') - .$this->buildICalTextLine('PRODID', $productId) + .$this->buildICalTextLine('PRODID', $productId); + + if (is_string($calendarName) && $calendarName !== '') + { + $header .= $this->buildICalTextLine('NAME', $calendarName); + $header .= $this->buildICalTextLine('X-WR-CALNAME', $calendarName); + } + + return $header .$this->buildICalLine('BEGIN', 'VTIMEZONE') .$this->buildICalLine('TZID', 'Europe/Vienna') .$this->buildICalLine('BEGIN', 'DAYLIGHT') @@ -144,26 +154,27 @@ class LvPlanICALLib private function buildEvent($event) { + $isReservation = $event->type === 'reservierung'; + $summaryIcon = ''; - $summary = $event->type === 'reservierung' - ? $event->titel + $summary = $isReservation + ? implode(' - ', array_filter(array($event->titel, implode('/', $event->ort_kurzbz), $this->getGroups($event)))) : $this->joinValues($event->topic); - if ($event->type === 'reservierung') - { - $description = $event->beschreibung; - } - else - { - $description = $event->lehrfach_bez."\r\n"; - - if (isset($event->lektor) && is_array($event->lektor) && count($event->lektor) > 0) - { - $description .= 'Lektor*in: '.implode(', ', array_map(function ($teacher) { - return $teacher['kurzbz']; - }, $event->lektor))."\r\n"; + $descriptionParts = array( + $event->titel, + isset($event->lehrfach_bez) ? $event->lehrfach_bez : '', + $isReservation ? $this->getParticipants($event) : $this->getLecturers($event), + $this->getGroups($event), + implode('/', $event->ort_kurzbz), + isset($event->beschreibung) ? $event->beschreibung : '' + ); + $description = implode("\r\n", array_values(array_filter( + $descriptionParts, + function ($value) { + return $value !== ''; } - } + ))); $category = self::CATEGORY_TIMETABLE; if ($event->type === 'lehreinheit') @@ -319,4 +330,76 @@ class LvPlanICALLib return null; } } + + /** + * @param object $event Calendar event. + * @return string Participant group labels. + */ + private function getGroups($event) + { + $groups = array(); + + if (isset($event->gruppe) && is_array($event->gruppe)) + { + foreach ($event->gruppe as $group) + { + if (isset($group['bezeichnung']) && $group['bezeichnung'] !== '') + $groups[] = $group['bezeichnung']; + } + } + + if (isset($event->teilnehmer_gruppe) && is_array($event->teilnehmer_gruppe)) + { + foreach ($event->teilnehmer_gruppe as $group) + { + if (isset($group['gruppe_kurzbz']) && $group['gruppe_kurzbz'] !== '') + $groups[] = $group['gruppe_kurzbz']; + } + } + + sort($groups, SORT_NATURAL | SORT_FLAG_CASE); + + return implode(',', array_unique($groups)); + } + + /** + * @param object $event Calendar event. + * @return string Lecturer abbreviations. + */ + private function getLecturers($event) + { + $lecturers = array(); + + if (isset($event->lektor) && is_array($event->lektor)) + { + foreach ($event->lektor as $lecturer) + { + if (isset($lecturer['kurzbz']) && $lecturer['kurzbz'] !== '') + $lecturers[] = $lecturer['kurzbz']; + } + } + + sort($lecturers, SORT_NATURAL | SORT_FLAG_CASE); + + return implode(' / ', array_unique($lecturers)); + } + + private function getParticipants($event) + { + $values = array(); + if (!isset($event->teilnehmer_person) || !is_array($event->teilnehmer_person)) + return ''; + + foreach ($event->teilnehmer_person as $participant) + { + $value = $participant['vorname'] . ' ' . $participant['nachname']; + + if ($value !== '') + $values[] = $value; + } + + sort($values, SORT_NATURAL | SORT_FLAG_CASE); + + return implode(', ', array_unique($values)); + } } diff --git a/cis/private/lvplan/index.php b/cis/private/lvplan/index.php index 7f6e2e091..d4c210858 100644 --- a/cis/private/lvplan/index.php +++ b/cis/private/lvplan/index.php @@ -337,6 +337,7 @@ function LoadGruppe(type) echo '
  • '.$p->t('lvplan/caldavURL').'
  • '; echo '
  • '.$p->t('lvplan/caldavURLMac').'
  • '; echo '
  • '.$p->t('lvplan/googleURL').'
  • '; + echo '
  • NEW '.$p->t('lvplan/googleURL').'
  • '; echo ''; echo ' '; } diff --git a/system/phrasesupdate.php b/system/phrasesupdate.php index e74242b3a..00d1d6406 100644 --- a/system/phrasesupdate.php +++ b/system/phrasesupdate.php @@ -61219,6 +61219,47 @@ I have been informed that I am under no obligation to consent to the transmissio ) ) ), + //******************************* CORE/lvplan + array( + 'app' => 'core', + 'category' => 'lvplan', + 'phrase' => 'room_not_reservable', + 'insertvon' => 'system', + 'phrases' => array( + array( + 'sprache' => 'German', + 'text' => 'Die Möglichkeit der Reservierung ist für diesen Raum deaktiviert.', + 'description' => '', + 'insertvon' => 'system' + ), + array( + 'sprache' => 'English', + 'text' => 'Reservation is disabled for this room.', + 'description' => '', + 'insertvon' => 'system' + ) + ) + ), + array( + 'app' => 'core', + 'category' => 'lvplan', + 'phrase' => 'room_reservation_restricted_to_period', + 'insertvon' => 'system', + 'phrases' => array( + array( + 'sprache' => 'German', + 'text' => 'Die Raumreservierung ist auf den Zeitraum vom {start_date} bis {end_date} eingeschränkt', + 'description' => '', + 'insertvon' => 'system' + ), + array( + 'sprache' => 'English', + 'text' => 'Room reservation is limited to the period from {start_date} to {end_date}', + 'description' => '', + 'insertvon' => 'system' + ) + ) + ), // generic tabulator phrases END );