diff options
| author | Moritz Strohm <strohm@data-quest.de> | 2026-01-14 10:29:35 +0000 |
|---|---|---|
| committer | Moritz Strohm <strohm@data-quest.de> | 2026-01-14 10:29:35 +0000 |
| commit | 78e46de33b3f205aae375d1ea6d4fe088e0e5124 (patch) | |
| tree | 4b305bf3f7b5d066ac28f011fe752e98901e714c /app/controllers | |
| parent | f637e7ae2d086941a11297ccc29ac273ad6759b0 (diff) | |
allow booking separable rooms in courses, closes #639
Closes #639
Merge request studip/studip!4039
Diffstat (limited to 'app/controllers')
| -rw-r--r-- | app/controllers/course/block_appointments.php | 347 | ||||
| -rw-r--r-- | app/controllers/course/dates.php | 7 | ||||
| -rw-r--r-- | app/controllers/course/timesrooms.php | 858 | ||||
| -rw-r--r-- | app/controllers/resources/admin.php | 39 | ||||
| -rw-r--r-- | app/controllers/resources/room_request.php | 17 |
5 files changed, 708 insertions, 560 deletions
diff --git a/app/controllers/course/block_appointments.php b/app/controllers/course/block_appointments.php index 88d7378..9e6b9c2 100644 --- a/app/controllers/course/block_appointments.php +++ b/app/controllers/course/block_appointments.php @@ -40,75 +40,74 @@ class Course_BlockAppointmentsController extends AuthenticatedController } - protected function setAvailableRooms() - { - $this->room_search = null; - $this->selectable_rooms = []; - if (Config::get()->RESOURCES_ENABLE) { - //Check for how many rooms the user has booking permissions. - //In case these permissions exist for more than 50 rooms - //show a quick search. Otherwise show a select field - //with the list of rooms. - - $current_user = User::findCurrent(); - $current_user_is_resource_admin = ResourceManager::userHasGlobalPermission( - $current_user, - 'admin' - ); - - $rooms_with_booking_permissions = 0; - if ($current_user_is_resource_admin) { - $rooms_with_booking_permissions = Room::countAll(); - } else { - $user_rooms = RoomManager::getUserRooms($current_user); - foreach ($user_rooms as $room) { - if ($room->userHasBookingRights($current_user)) { - $rooms_with_booking_permissions++; - $this->selectable_rooms[] = $room; - } - } - } - - if ($rooms_with_booking_permissions > 50) { - $room_search_type = new RoomSearch(); - $room_search_type->setAcceptedPermissionLevels( - ['autor', 'tutor', 'admin'] - ); - $room_search_type->setAdditionalDisplayProperties( - ['seats'] - ); - $this->room_search = new QuickSearch( - 'room_id', - $room_search_type - ); - } else { - if (ResourceManager::userHasGlobalPermission($current_user, 'admin')) { - $this->selectable_rooms = Room::findAll(); - } - } - } - } - - /** * Display the block appointments */ public function index_action() { - if (!Request::isXhr() && Navigation::hasItem('/course/admin/timesrooms')) { + if (Navigation::hasItem('/course/admin/timesrooms')) { Navigation::activateItem('/course/admin/timesrooms'); } + PageLayout::setTitle(_('Neuen Blocktermin anlegen')); + $this->linkAttributes = ['fromDialog' => Request::int('fromDialog') ? 1 : 0]; $this->start_ts = strtotime('this monday'); $this->request = $this->flash['request'] ?? $_SESSION['block_appointments'] ?? []; - $this->confirm_many = isset($this->flash['confirm_many']) ? $this->flash['confirm_many'] : false; - $this->lecturers = CourseMember::findByCourseAndStatus( + $this->lecturers = CourseMember::findByCourseAndStatus( $this->course_id, 'dozent' ); - if (Config::get()->RESOURCES_ENABLE) { - $this->setAvailableRooms(); + $this->start = null; + $this->end = null; + $this->date_types = []; + foreach ($GLOBALS['TERMIN_TYP'] as $id => $data) { + $this->date_types[] = [ + 'id' => $id, + 'name' => $data['name'] + ]; + } + $this->available_lecturers = []; + $course = Course::find($this->course_id); + $lecturers = $course->getMembersWithStatus('dozent'); + foreach ($lecturers as $lecturer) { + $this->available_lecturers[$lecturer->user_id] = $lecturer->getUserFullname(); + } + $this->selected_lecturer_ids = []; + $this->selected_date_type = 0; + $this->dow = ['all']; + $this->preparation_time = 0; + $this->subsequent_time = 0; + + if ($this->request instanceof Request) { + $this->start = $this->request->getDateTime('start_date', 'd.m.Y', 'start_time', 'H:i'); + $this->end = $this->request->getDateTime('end_date', 'd.m.Y', 'end_time', 'H:i'); + $this->selected_lecturer_ids = $this->request->getArray('lecturers'); + $this->selected_date_type = $this->request->int('date_type'); + $this->dow = $this->request->getArray('dow'); + $this->preparation_time = $this->request->int('preparation_time', 0); + $this->subsequent_time = $this->request->int('subsequent_time', 0); + } elseif (is_array($this->request)) { + $this->start = $this->request['start'] ?? null; + $this->end = $this->request['end'] ?? null; + $this->selected_date_type = $this->request['date_type'] ?? 0; + $this->dow = $this->request['dow'] ?? ['all']; + $this->preparation_time = $this->request['preparation_time'] ?? 0; + $this->subsequent_time = $this->request['subsequent_time'] ?? 0; } + if (!$this->start || !$this->end) { + //Provide some default values: + $this->start = new DateTime(); + $this->start = $this->start->add(new DateInterval('PT1H')); + $this->start->setTime(intval($this->start->format('H')), 0, 0); + $this->end = clone $this->start; + $this->end = $this->end->add(new DateInterval('PT30M')); + } + + $this->allow_multiple_room_bookings = ResourceManager::userHasGlobalPermission( + User::findCurrent(), + Config::get()->ROOM_PERMISSIONS_FOR_MULTIPLE_BOOKINGS_PER_COURSE_DATE + ); + $this->max_preparation_time = intval(Config::get()->RESOURCES_MAX_PREPARATION_TIME) ?? 999; } /** @@ -120,59 +119,50 @@ class Course_BlockAppointmentsController extends AuthenticatedController { $errors = []; - $start_day = strtotime(Request::get('block_appointments_start_day')); - $end_day = strtotime(Request::get('block_appointments_end_day')); - $start_time = null; - $end_time = null; - if (!($start_day && $end_day && $start_day <= $end_day)) { + $start = Request::getDateTime('start_date', 'd.m.Y', 'start_time', 'H:i'); + $end = Request::getDateTime('end_date', 'd.m.Y', 'end_time', 'H:i'); + if (!$start || !$end || $start >= $end) { $errors[] = _('Bitte geben Sie korrekte Werte für Start- und Enddatum an!'); - } else { - $start_time = strtotime(Request::get('block_appointments_start_time'), $start_day); - $end_time = strtotime(Request::get('block_appointments_end_time'), $end_day); - - if (!($start_time && $end_time && (strtotime(Request::get('block_appointments_start_time')) < strtotime(Request::get('block_appointments_end_time'))))) { - $errors[] = _('Bitte geben Sie korrekte Werte für Start- und Endzeit an!'); - } } - //Calculate the duration if a minimum booking time is set: - if (Config::get()->RESOURCES_MIN_BOOKING_TIME) { - $fake_start_time = strtotime(Request::get('block_appointments_start_time'), $start_day); - $fake_end_time = strtotime(Request::get('block_appointments_end_time'), $start_day); - $duration = $fake_end_time - $fake_start_time; + $room_choice = Request::get('room'); + $preparation_time = 0; + $subsequent_time = 0; + $room_name = ''; + if ($room_choice === 'room' && Config::get()->RESOURCES_MIN_BOOKING_TIME) { + //Calculate the duration if a minimum booking time is set + //and one or more rooms shall be booked: + $fake_start_time = clone $start; + $fake_end_time = clone $start; + $fake_end_time->setTime(intval($end->format('H')), intval($end->format('i')), 0); + $duration = $fake_end_time->getTimestamp() - $fake_start_time->getTimestamp(); if ($duration < Config::get()->RESOURCES_MIN_BOOKING_TIME * 60) { $errors[] = sprintf( ngettext( - 'Die minimale Dauer eines Termins von einer Minute wurde unterschritten.', - 'Die minimale Dauer eines Termins von %u Minuten wurde unterschritten.', + 'Die minimale Dauer einer Raumbuchung von einer Minute wurde unterschritten.', + 'Die minimale Dauer einer Raumbuchung von %u Minuten wurde unterschritten.', Config::get()->RESOURCES_MIN_BOOKING_TIME ), Config::get()->RESOURCES_MIN_BOOKING_TIME ); } + $preparation_time = Request::int('preparation_time', 0); + $subsequent_time = Request::int('subsequent_time', 0); + } elseif ($room_choice === 'freetext') { + $room_name = Request::get('room_name'); } - $termin_typ = Request::int('block_appointments_termin_typ', 0); - $free_room_text = Request::get('block_appointments_room_text'); - $date_count = Request::int('block_appointments_date_count'); - $days = Request::getArray('block_appointments_days'); - $lecturer_ids = Request::getArray('lecturers'); - - $lecturers = User::findBySql( - "INNER JOIN seminar_user USING (user_id) - WHERE seminar_id = :course_id - AND seminar_user.user_id IN (:lecturer_ids) - AND seminar_user.status = 'dozent'", - [ - 'course_id' => $this->course_id, - 'lecturer_ids' => $lecturer_ids, - ] - ); - - if (!is_array($days)) { + $date_type = Request::int('date_type', 0); + $dow = Request::getArray('dow'); + if (empty($dow)) { $errors[] = _('Bitte wählen Sie mindestens einen Tag aus!'); } + $date_count = Request::int('date_count'); + if ($date_count < 1) { + $errors[] = _('Bitte setzen Sie die Menge der zu erstellenden Termine mindestens auf 1.'); + } + if (count($errors)) { $this->flash['request'] = Request::getInstance(); PageLayout::postMessage(MessageBox::error(_('Bitte korrigieren Sie Ihre Eingaben:'), $errors)); @@ -180,87 +170,124 @@ class Course_BlockAppointmentsController extends AuthenticatedController return; } - $dates = []; - /* - * Recalculate end hour of last day to first day, so we don't run - * into problems with daylight saving time which would add or - * remove an hour. - */ - $delta = (strtotime(Request::get('block_appointments_start_day') . ' ' . - Request::get('block_appointments_end_time')) - $start_time) % (24 * 60 * 60); - $last_day = strtotime(Request::get('block_appointments_start_time'), $end_day); - - if (in_array('everyday', $days)) { - $days = range(1, 7); + $lecturer_ids = Request::getArray('assigned_lecturers'); + $lecturers = []; + if ($lecturer_ids) { + $lecturers = User::findBySql( + "INNER JOIN seminar_user USING (user_id) + WHERE seminar_id = :course_id + AND seminar_user.user_id IN (:lecturer_ids) + AND seminar_user.status = 'dozent'", + [ + 'course_id' => $this->course_id, + 'lecturer_ids' => $lecturer_ids, + ] + ); } - if (in_array('weekdays', $days)) { - $days = range(1, 5); + + if (in_array('all', $dow)) { + $dow = ['1', '2', '3', '4', '5', '6', '7']; + } elseif (in_array('mon_fri', $dow)) { + $dow = ['1', '2', '3', '4', '5']; } - $t = $start_time; - while ($t <= $last_day) { - if (in_array(date('N', $t), $days)) { - for ($i = 1; $i <= $date_count; $i++) { - $date = new CourseDate(); - $date->range_id = $course_id; - $date->date_typ = $termin_typ; - $date->raum = $free_room_text; - $date->date = $t; - $date->end_time = $t + $delta; + $dates = []; + $t = clone $start; + $i = 1; + while ($t < $end && $i <= $date_count) { + if (in_array($t->format('N'), $dow)) { + $date_end = clone $t; + $date_end->setTime(intval($end->format('H')), intval($end->format('i')), 0); + $date = new CourseDate(); + $date->range_id = $course_id; + $date->date_typ = $date_type; + $date->raum = $room_name; + $date->date = $t->getTimestamp(); + $date->end_time = $date_end->getTimestamp(); + if ($lecturers) { $date->dozenten = $lecturers; - $dates[] = $date; } + $dates[] = $date; + $i++; } - $t = strtotime('+1 day', $t); + $t = $t->add(new DateInterval('P1D')); } - if (count($dates) > 100 && !Request::int('confirmed')) { - $this->flash['request'] = Request::getInstance(); - $this->flash['confirm_many'] = count($dates); - $this->redirect('course/block_appointments/index'); - return; - } elseif (count($dates)) { - if (Request::submitted('preview')) { - //TODO + //Store the last used values in the session as default values. + $_SESSION['block_appointments'] = [ + 'start' => $start, + 'end' => $end, + 'date_type' => $date_type, + 'room_name' => $room_name, + 'date_count' => $date_count, + 'dow' => $dow + ]; + $partially_booked_dates = []; + $dates_created = array_filter(array_map(function ($d) use ($room_choice, $preparation_time, $subsequent_time, &$partially_booked_dates) { + $result = $d->store(); + $room_ids = []; + if ($room_choice === 'room') { + $room_ids = Request::getArray('room_ids'); } + //Process the room-IDs: If a separable room is selected, set all its room parts as room-IDs. + //Remove the prefix in all other cases. + $processed_room_ids = []; + foreach ($room_ids as $room_id) { + $id_parts = explode('-', $room_id); + if (count($id_parts) !== 2) { + //Invalid ID. + continue; + } - if (Request::submitted('save')) { - // store last used values in session as defaults - $_SESSION['block_appointments'] = [ - 'block_appointments_start_day' => date('d.m.Y', $start_day), - 'block_appointments_end_day' => date('d.m.Y', $end_day), - 'block_appointments_start_time' => date('H:i', $start_time), - 'block_appointments_end_time' => date('H:i', $end_time), - 'block_appointments_termin_typ' => $termin_typ, - 'block_appointments_room_text' => $free_room_text, - 'block_appointments_date_count' => $date_count, - 'block_appointments_days' => $days - ]; - $dates_created = array_filter(array_map(function ($d) use ($free_room_text) { - if (!Request::get('room_id')) { - $d->raum = $free_room_text; - $result = $d->store(); - } else { - $result = $d->store(); - $room = Resource::find(Request::option('room_id'))?->getDerivedClassInstance(); - $d->bookRoom($room); + if ($id_parts[0] === 'separable_room') { + //A separable room was selected. + $separable_room = SeparableRoom::find($id_parts[1]); + if ($separable_room) { + foreach ($separable_room->parts as $part) { + $processed_room_ids[] = $part->room_id; + } } - return $result ? $d->getFullName() : null; - }, $dates)); - if ($date_count > 1) { - $dates_created = array_count_values($dates_created); - $dates_created = array_map(function ($k, $v) { - return $k . ' (' . $v . 'x)'; - }, array_keys($dates_created), array_values($dates_created)); + } elseif ($id_parts[0] === 'room') { + //An ordinary room. + $processed_room_ids[] = $id_parts[1]; } - PageLayout::postSuccess(_('Folgende Termine wurden erstellt:'), $dates_created); - } - } else { - $this->flash['request'] = Request::getInstance(); - PageLayout::postError(_('Keiner der ausgewählten Tage liegt in dem angegebenen Zeitraum!')); - $this->redirect('course/block_appointments/index'); - return; + $room_ids = $processed_room_ids; + if ($room_ids) { + $resources = Resource::findMany($room_ids); + $rooms = []; + foreach ($resources as $resource) { + $rooms[] = $resource->getDerivedClassInstance(); + } + $booking_failures = 0; + foreach ($rooms as $room) { + try { + $r = $d->bookRoom($room, $preparation_time * 60, $subsequent_time * 60); + if (!$r) { + $booking_failures++; + } + } catch (ResourceBookingException|ResourceBookingOverlapException $e) { + $booking_failures++; + } + } + if ($result && $booking_failures) { + //Not all selected rooms for the date could be booked: + $partially_booked_dates[] = $d->getFullName(); + } + } + + return $result ? $d->getFullName() : null; + }, $dates)); + + if ($date_count > 1) { + $dates_created = array_count_values($dates_created); + $dates_created = array_map(function ($k, $v) { + return $k . ' (' . $v . 'x)'; + }, array_keys($dates_created), array_values($dates_created)); + } + PageLayout::postSuccess(_('Folgende Termine wurden erstellt:'), $dates_created); + if (!empty($partially_booked_dates)) { + PageLayout::postWarning(_('Für folgende Termine konnten nicht alle ausgewählten Räume gebucht werden:'), $partially_booked_dates); } if (Request::int('fromDialog')) { diff --git a/app/controllers/course/dates.php b/app/controllers/course/dates.php index 1a766a9..488f4f5 100644 --- a/app/controllers/course/dates.php +++ b/app/controllers/course/dates.php @@ -442,7 +442,7 @@ class Course_DatesController extends AuthenticatedController 'start' => $singledate->date, 'related_persons' => $singledate->dozenten, 'groups' => $singledate->statusgruppen, - 'room' => (string) ($singledate->getRoom() ?? $singledate->raum), + 'room' => $singledate->getRoomNames(), 'type' => $GLOBALS['TERMIN_TYP'][$singledate->date_typ]['name'], ]; } elseif ($singledate instanceof CourseExDate && $singledate->content) { @@ -545,8 +545,9 @@ class Course_DatesController extends AuthenticatedController }) ); - $room = $date->getRoom(); - if ($room) { + $rooms = $date->getRooms(); + if (!empty($rooms)) { + $room = reset($rooms); $row[] = $room->name; $row[] = $room->description; $row[] = $room->seats; diff --git a/app/controllers/course/timesrooms.php b/app/controllers/course/timesrooms.php index 8ee11ac..ae2c567 100644 --- a/app/controllers/course/timesrooms.php +++ b/app/controllers/course/timesrooms.php @@ -120,6 +120,7 @@ class Course_TimesroomsController extends AuthenticatedController } + protected function bookingTooShort(int $start_time, int $end_time) { return Config::get()->RESOURCES_MIN_BOOKING_TIME && @@ -173,20 +174,27 @@ class Course_TimesroomsController extends AuthenticatedController $this->cycle_dates[$cycle->metadate_id]['dates'][$sem->id] = []; } $this->cycle_dates[$cycle->metadate_id]['dates'][$sem->id][] = $val; - if ($val->getRoom()) { - $this->cycle_dates[$cycle->metadate_id]['room_request'][] = $val->getRoom(); + if ($rooms = $val->getRooms()) { + $first_room = reset($rooms); + if ($first_room) { + $this->cycle_dates[$cycle->metadate_id]['room_request'][] = $first_room; + } } $matched[] = $val->termin_id; - //Check if a room is booked for the date: - if (($val->room_booking instanceof ResourceBooking) - && !$cycle_has_multiple_rooms) { - $date_room = $val->room_booking->resource->name; - if (isset($this->cycle_room_names[$cycle->id])) { - if ($date_room && $date_room != $this->cycle_room_names[$cycle->id]) { - $cycle_has_multiple_rooms = true; + if ($val instanceof CourseDate) { + //Check if a room is booked for the date: + foreach ($val->room_bookings as $room_booking) { + if (($room_booking instanceof ResourceBooking) + && !$cycle_has_multiple_rooms) { + $date_room = $room_booking->resource->name; + if (isset($this->cycle_room_names[$cycle->id])) { + if ($date_room && $date_room != $this->cycle_room_names[$cycle->id]) { + $cycle_has_multiple_rooms = true; + } + } elseif ($date_room) { + $this->cycle_room_names[$cycle->id] = $date_room; + } } - } elseif ($date_room) { - $this->cycle_room_names[$cycle->id] = $date_room; } } } @@ -333,7 +341,7 @@ class Course_TimesroomsController extends AuthenticatedController } /** - * Primary function to edit date-informations + * Action to edit a single date. * * @param string $termin_id */ @@ -341,47 +349,144 @@ class Course_TimesroomsController extends AuthenticatedController { PageLayout::setTitle(_('Einzeltermin bearbeiten')); $this->date = CourseDate::find($termin_id) ?: CourseExDate::find($termin_id); - $this->attributes = []; - - $request = RoomRequest::findByDate($this->date->id); - if ($request) { - $this->params = ['request_id' => $request->id]; - } else { - $this->params = ['new_room_request_type' => 'date_' . $this->date->id]; + $this->date_types = []; + foreach ($GLOBALS['TERMIN_TYP'] as $id => $data) { + $this->date_types[] = [ + 'id' => $id, + 'name' => $data['name'] + ]; } - $this->only_bookable_rooms = Request::submitted('only_bookable_rooms'); + $this->selected_room_ids = []; if (Config::get()->RESOURCES_ENABLE) { - $room_booking_id = ''; - if ($this->date->room_booking instanceof ResourceBooking) { - $room_booking_id = $this->date->room_booking->id; - $room = $this->date->room_booking->resource; - if ($room instanceof Resource) { - $room = $room->getDerivedClassInstance(); - if (!$room->userHasBookingRights(User::findCurrent())) { - PageLayout::postWarning( - _('Die Raumbuchung zu diesem Termin wird bei der Verlängerung des Zeitbereiches gelöscht, da sie keine Buchungsrechte am Raum haben!') + //Collect all room bookings: + $booked_rooms = []; + $separable_rooms = []; + if ($this->date->room_bookings) { + foreach ($this->date->room_bookings as $booking) { + $room = $booking->resource; + if ($room instanceof Resource) { + $room = $room->getDerivedClassInstance(); + if (!$room->userHasBookingRights(User::findCurrent())) { + PageLayout::postWarning( + studip_interpolate( + _('Die Buchung des Raumes %{room_name} zu diesem Termin wird bei der Verlängerung des Zeitbereiches gelöscht, da sie keine Buchungsrechte an dem Raum haben!'), + ['room_name' => $room->name] + ) + ); + } + //Check if the room is part of a separable room: + if (count($room->separable_room)) { + $sr = $room->separable_room[0]; + $separable_rooms[$sr->id] = $sr; + } + + $booked_rooms[strval($booking->resource->id)] = $booking->resource->getDerivedClassInstance(); + } + } + + //Loop over all separable rooms and check if the IDs of all of their parts + //are present in the $booked_room_ids array: + foreach ($separable_rooms as $separable_room) { + $room_part_ids = []; + foreach ($separable_room->parts as $part) { + if (!in_array($part->room_id, array_keys($booked_rooms))) { + //The separable room is not fully booked and can be skipped. + continue 2; + } + $room_part_ids[] = $part->room_id; + } + //At this point, all the parts of the separable room are booked + //so that the separable room can be added to the $assigned_room_ids array + //and its parts can be removed from the $booked_room_ids array. + $this->selected_room_ids[] = [ + 'id' => 'separable_room-' . $separable_room->id, + 'label' => sprintf( + '%1$s (%2$s)', + $separable_room->name, + _('Teilbarer Raum'), + ), + 'separable_room_id' => $separable_room->id, + 'info_text' => sprintf('%1$s: %2$s', $separable_room->name, $separable_room->description) + ]; + //Filter out the room parts from the list of booked rooms: + $booked_rooms = array_filter( + $booked_rooms, + function ($item) use ($room_part_ids) { + return !in_array($item->id, $room_part_ids); + } + ); + } + + //All the remaining entries in $booked_room_ids are separable rooms that are + //only partially booked or ordinary rooms: + foreach ($booked_rooms as $room) { + $room_data = [ + 'id' => 'room-' . $room->id, + 'label' => studip_interpolate( + _('%{room_name} (%{seats} Sitzplätze)'), + [ + 'room_name' => $room->getFullName(), + 'seats' => $room->seats + ] + ) + ]; + if (count($room->separable_room) > 0) { + $first_separable_room = $room->separable_room[0]; + $room_data['label'] = studip_interpolate( + _('%{room_name} (%{seats} Sitzplätze) [Teil von %{separable_room_name}]'), + [ + 'room_name' => $room->getFullName(), + 'seats' => $room->seats, + 'separable_room_name' => $first_separable_room->name + ] + ); + $room_data['separable_room_id'] = $first_separable_room->id; + $room_data['info_text'] = sprintf( + '%1$s: %2$s', + $first_separable_room->name, + $first_separable_room->description ); } + $this->selected_room_ids[] = $room_data; } } - $this->setAvailableRooms([$this->date], [$room_booking_id], $this->only_bookable_rooms); } - $this->teachers = $this->course->getMembersWithStatus('dozent'); - $this->assigned_teachers = $this->date->dozenten; - - $this->groups = $this->course->statusgruppen; - $this->assigned_groups = $this->date->statusgruppen; + $this->available_lecturers = []; + $this->assigned_lecturers = []; + $this->available_groups = []; + $this->assigned_groups = []; + $lecturers = $this->course->getMembersWithStatus('dozent'); + foreach ($lecturers as $lecturer) { + $this->available_lecturers[$lecturer->user_id] = $lecturer->getUserFullname(); + } + foreach ($this->date->dozenten as $assigned_lecturer) { + $this->assigned_lecturers[] = $assigned_lecturer->user_id; + } + foreach ($this->course->statusgruppen as $group) { + $this->available_groups[$group->id] = $group->name; + } + foreach ($this->date->statusgruppen as $assigned_group) { + $this->assigned_groups[] = $assigned_group->id; + } - if ($this->date->room_booking instanceof ResourceBooking) { - $this->preparation_time = $this->date->room_booking->preparation_time / 60; - $this->subsequent_time = $this->date->room_booking->subsequent_time / 60; - } else { - $this->preparation_time = 0; - $this->subsequent_time = 0; + $first_booking = null; + if (count($this->date->room_bookings) > 0) { + $first_booking = $this->date->room_bookings[0]; } - $this->max_preparation_time = Config::get()->RESOURCES_MAX_PREPARATION_TIME; + $this->preparation_time = $first_booking instanceof ResourceBooking + ? intval(floor($first_booking->preparation_time / 60)) + : 0; + $this->subsequent_time = $first_booking instanceof ResourceBooking + ? intval(floor($first_booking->subsequent_time / 60)) + : 0; + $this->max_preparation_time = intval(Config::get()->RESOURCES_MAX_PREPARATION_TIME); + + $this->allow_multiple_room_bookings = ResourceManager::userHasGlobalPermission( + User::findCurrent(), + Config::get()->ROOM_PERMISSIONS_FOR_MULTIPLE_BOOKINGS_PER_COURSE_DATE + ); } @@ -419,21 +524,38 @@ class Course_TimesroomsController extends AuthenticatedController * * @throws Trails\Exceptions\DoubleRenderError */ - public function saveDate_action($termin_id) + public function saveDate_action($termin_id = '') { // TODO :: TERMIN -> SINGLEDATE CSRFProtection::verifyUnsafeRequest(); - $termin = CourseDate::find($termin_id); - $date = strtotime(sprintf('%s %s:00', Request::get('date'), Request::get('start_time'))); - $end_time = strtotime(sprintf('%s %s:00', Request::get('date'), Request::get('end_time'))); + $termin = null; + if ($termin_id) { + $termin = CourseDate::find($termin_id); + } else { + $termin = new CourseDate(); + $termin->range_id = $this->course->id; + } + $start = Request::getDateTime('date', 'd.m.Y', 'start_time', 'H:i'); + $end = Request::getDateTime('date', 'd.m.Y', 'end_time', 'H:i'); + $max_preparation_time = Config::get()->RESOURCES_MAX_PREPARATION_TIME; - if ($date === false || $end_time === false || $date > $end_time) { - $date = $termin->date; - $end_time = $termin->end_time; + if ($start === false || $end === false || $start > $end) { + if (!$termin->isNew()) { + $date = new DateTime(); + $end = new DateTime(); + $date->setTimestamp($termin->date); + $end->setTimestamp($termin->end_time); + } PageLayout::postError(_('Die Zeitangaben sind nicht korrekt. Bitte überprüfen Sie diese!')); } - if ($this->bookingTooShort($date, $end_time)) { + + if ($termin->isNew()) { + $termin->date = $start->getTimestamp(); + $termin->end_time = $end->getTimestamp(); + } + + if ($this->bookingTooShort($start->getTimestamp(), $end->getTimestamp())) { PageLayout::postError( sprintf( ngettext( @@ -448,7 +570,16 @@ class Course_TimesroomsController extends AuthenticatedController return; } - $time_changed = ($date != $termin->date || $end_time != $termin->end_time); + $time_changed = !$termin->isNew() && ($start->getTimestamp() != $termin->date || $end->getTimestamp() != $termin->end_time); + $preparation_time = Request::int('preparation_time', 0); + $subsequent_time = Request::int('subsequent_time', 0); + $preparation_time_changed = false; + $subsequent_time_changed = false; + $first_booking = reset($termin->room_bookings); + if ($first_booking) { + $preparation_time_changed = $first_booking->preparation_time !== $preparation_time * 60 ; + $subsequent_time_changed = $first_booking->subsequent_time !== $subsequent_time * 60 ; + } if ($time_changed) { if ($termin->metadate_id != '') { //time changed for regular date. create normal singledate and cancel the regular date @@ -466,53 +597,56 @@ class Course_TimesroomsController extends AuthenticatedController $termin = new CourseDate(); unset($termin_values['metadate_id']); $termin->setData($termin_values); - $termin->date = $date; - $termin->end_time = $end_time; + $termin->date = $start->getTimestamp(); + $termin->end_time = $end->getTimestamp(); $termin->setId($termin->getNewId()); } else { //Time changed for single date. - $termin->date = $date; - $termin->end_time = $end_time; + $termin->date = $start->getTimestamp(); + $termin->end_time = $end->getTimestamp(); } } - $termin->date_typ = Request::get('course_type'); + $termin->date_typ = Request::get('date_type'); // Set assigned teachers - $assigned_teachers = Request::optionArray('assigned_teachers'); + $assigned_lecturers = Request::optionArray('assigned_lecturers'); $dozenten = $this->course->getMembersWithStatus('dozent'); - if (count($assigned_teachers) === count($dozenten) || empty($assigned_teachers)) { + if (count($assigned_lecturers) === count($dozenten) || empty($assigned_lecturers)) { //The amount of lecturers of the course date is the same as the amount of lecturers of the course //or no lecturers are assigned to the course date. $termin->dozenten = []; } else { //The assigned lecturers (amount or persons) have been changed in the form. //In those cases, the lecturers of the course date have to be set. - $termin->dozenten = User::findMany($assigned_teachers); + $termin->dozenten = User::findMany($assigned_lecturers); } // Set assigned groups - $assigned_groups = Request::optionArray('assigned-groups'); + $assigned_groups = Request::optionArray('assigned_groups'); $termin->statusgruppen = Statusgruppen::findMany($assigned_groups); if (Config::get()->ENABLE_NUMBER_OF_PARTICIPANTS) { $termin->number_of_participants = strlen(Request::get('number_of_participants')) && Request::int('number_of_participants') >= 0 ? Request::int('number_of_participants') : null; } + $new_date = $termin->isNew(); + $termin->store(); - if ($time_changed) { + if ($new_date || $time_changed) { NotificationCenter::postNotification('CourseDidChangeSchedule', $this->course); } - // Set Room - $old_room_id = $termin->room_booking->resource_id ?? ''; + // Set Rooms + $old_room_ids = []; + foreach ($termin->room_bookings as $booking) { + $old_room_ids[] = $booking->resource_id; + } if ((Request::option('room') == 'room') || Request::option('room') == 'nochange') { - $room_id = null; - $preparation_time = Request::int('preparation_time', 0); - $subsequent_time = Request::int('subsequent_time', 0); + $room_ids = []; if (Request::option('room') == 'room') { - $room_id = Request::get('room_id'); + $room_ids = Request::getArray('room_ids'); if ($preparation_time > $max_preparation_time || $subsequent_time > $max_preparation_time) { PageLayout::postError( sprintf( @@ -521,128 +655,189 @@ class Course_TimesroomsController extends AuthenticatedController ) ); } + //Process the room-IDs: If a separable room is selected, set all its room parts as room-IDs. + //Remove the prefix in all other cases. + $processed_room_ids = []; + foreach ($room_ids as $room_id) { + $id_parts = explode('-', $room_id); + + if ($id_parts[0] === 'separable_room') { + //A separable room was selected. + $separable_room = SeparableRoom::find($id_parts[1]); + if ($separable_room) { + foreach ($separable_room->parts as $part) { + $processed_room_ids[] = $part->room_id; + } + } + } elseif ($id_parts[0] === 'room') { + //An ordinary room. + $processed_room_ids[] = $id_parts[1]; + } + } + $room_ids = $processed_room_ids; } elseif (Request::option('room') == 'nochange') { //Use the ID of the current room as room-ID: - $room_id = $old_room_id; + $room_ids = $old_room_ids; } - if ($room_id) { - $room = Resource::find($room_id)?->getDerivedClassInstance(); - if ($room_id !== $old_room_id || $time_changed) { - $failure = false; - if ($room instanceof Room) { - try { - $failure = !$termin->bookRoom($room, $preparation_time, $subsequent_time); - } catch (ResourceBookingException|ResourceBookingOverlapException $e) { - $course = $e->getRange(); - $message_links = []; - - if ($course instanceof Course) { - if ($course->isEditableByUser()) { - //Link to the times/rooms page: - $link = new LinkElement( - _('Direkt zur Veranstaltung'), - URLHelper::getURL('dispatch.php/course/timesrooms/index', ['cid' => $course->id]), - Icon::create('link-intern') - ); - $message_links[] = $link->render(); - } elseif ($course->isAccessibleToUser()) { - //Link to the details page: - $link = new LinkElement( - _('Direkt zur Veranstaltung'), - URLHelper::getURL('course/details/index', ['cid' => $course->id]), - Icon::create('link-intern') + if ($room_ids) { + $resources = Resource::findMany($room_ids); + $rooms = []; + foreach ($resources as $resource) { + $rooms[] = $resource->getDerivedClassInstance(); + } + if ($time_changed || $preparation_time_changed) { + //Remove the old bookings first. + ResourceBooking::deleteBySQL( + '`range_id` = :date_id AND `resource_id` IN ( :room_ids )', + ['date_id' => $termin->id, 'room_ids' => $room_ids] + ); + } + if ($room_ids !== $old_room_ids || $time_changed) { + if (count($room_ids) > 1) { + //Check if the user has sufficient permissions to book multiple rooms. + $min_perms = Config::get()->ROOM_PERMISSIONS_FOR_MULTIPLE_BOOKINGS_PER_COURSE_DATE; + if (!ResourceManager::userHasGlobalPermission(User::findCurrent(), $min_perms)) { + PageLayout::postError( + _('Ihre globalen Berechtigungen in der Raumverwaltung reichen nicht aus, um mehrere Räume für einen Termin buchen zu können.') + ); + $this->relocate('course/timesrooms/index', ['contentbox_open' => $termin->metadate_id]); + return; + } + } + $unbooked_room_ids = $old_room_ids; + foreach ($rooms as $room) { + if (in_array($room->id, $old_room_ids)) { + $unbooked_room_ids = array_diff($unbooked_room_ids, [$room->id]); + } + $failure = false; + if ($room instanceof Room) { + try { + $failure = !$termin->bookRoom($room, $preparation_time, $subsequent_time); + } catch (ResourceBookingException|ResourceBookingOverlapException $e) { + $course = $e->getRange(); + $message_links = []; + + if ($course instanceof Course) { + if ($course->isEditableByUser()) { + //Link to the times/rooms page: + $link = new LinkElement( + _('Direkt zur Veranstaltung'), + URLHelper::getURL('dispatch.php/course/timesrooms/index', ['cid' => $course->id]), + Icon::create('link-intern') + ); + $message_links[] = $link->render(); + } elseif ($course->isAccessibleToUser()) { + //Link to the details page: + $link = new LinkElement( + _('Direkt zur Veranstaltung'), + URLHelper::getURL('course/details/index', ['cid' => $course->id]), + Icon::create('link-intern') + ); + $message_links[] = $link->render(); + } + } + if ($room->userHasBookingRights(User::findCurrent())) { + $room_link = new LinkElement( + _('Zum Belegungsplan'), + $room->getActionURL('booking_plan') ); - $message_links[] = $link->render(); + $message_links[] = $room_link->render(); } - } - if ($room->userHasBookingRights(User::findCurrent())) { - $room_link = new LinkElement( - _('Zum Belegungsplan'), - $room->getActionURL('booking_plan') - ); - $message_links[] = $room_link->render(); - } - if ($e instanceof ResourceBookingException) { - PageLayout::postError( - sprintf( - _('Der angegebene Raum konnte für den Termin %1$s nicht gebucht werden: %2$s'), - '<strong>' . htmlReady($termin->getFullName()) . '</strong>', - $e->getMessage() - ), - $message_links - ); - } else { - //$e is a ResourceBookingOverlapException - if ($course instanceof Course) { + if ($e instanceof ResourceBookingException) { PageLayout::postError( - studip_interpolate( - _('Der Raum %{room_name} wird an dem Termin %{date} bereits durch die Veranstaltung %{course_name} belegt.'), - [ - 'room_name' => $room->name, - 'date' => $termin->getFullName(), - 'course_name' => $course->name - ] + sprintf( + _('Der angegebene Raum konnte für den Termin %1$s nicht gebucht werden: %2$s'), + '<strong>' . htmlReady($termin->getFullName()) . '</strong>', + $e->getMessage() ), $message_links ); } else { - PageLayout::postError( - studip_interpolate( - _('Der Raum %{room_name} wird an dem Termin %{date} bereits anderweitig belegt.'), - [ - 'room_name' => $room->name, - 'date' => $termin->getFullName() - ] - ), - $message_links - ); + //$e is a ResourceBookingOverlapException + if ($course instanceof Course) { + PageLayout::postError( + studip_interpolate( + _('Der Raum %{room_name} wird an dem Termin %{date} bereits durch die Veranstaltung %{course_name} belegt.'), + [ + 'room_name' => $room->name, + 'date' => $termin->getFullName(), + 'course_name' => $course->name + ] + ), + $message_links + ); + } else { + PageLayout::postError( + studip_interpolate( + _('Der Raum %{room_name} wird an dem Termin %{date} bereits anderweitig belegt.'), + [ + 'room_name' => $room->name, + 'date' => $termin->getFullName() + ] + ), + $message_links + ); + } } } } + if ($failure) { + PageLayout::postError(sprintf( + _('Der angegebene Raum konnte für den Termin %s nicht gebucht werden!'), + '<strong>' . htmlReady($termin->getFullName()) . '</strong>' + )); + } } - if ($failure) { - PageLayout::postError(sprintf( - _('Der angegebene Raum konnte für den Termin %s nicht gebucht werden!'), - '<strong>' . htmlReady($termin->getFullName()) . '</strong>' - )); + //Delete the bookings of the delesected rooms: + ResourceBooking::deleteBySQL( + '`range_id` = :date_id AND `resource_id` IN ( :unbooked_room_ids )', + ['date_id' => $termin->id, 'unbooked_room_ids' => $unbooked_room_ids] + ); + } elseif ($preparation_time_changed || $subsequent_time_changed) { + foreach ($rooms as $room) { + if ($room instanceof Room) { + $termin->bookRoom($room, $preparation_time * 60, $subsequent_time * 60); + } } - } elseif ($room instanceof Room - && ( - $termin->room_booking->preparation_time != ($preparation_time * 60) - || $termin->room_booking->subsequent_time != ($subsequent_time * 60))) { - $termin->bookRoom($room, $preparation_time, $subsequent_time); } - } else if ($old_room_id && empty($termin->room_booking->resource_id)) { + } else if ($old_room_ids && empty($termin->room_bookings)) { PageLayout::postInfo( sprintf( - _('Die Raumbuchung für den Termin %s wurde aufgehoben, da die neuen Zeiten außerhalb der alten liegen!'), + _('Die Raumbuchungen für den Termin %s wurden aufgehoben, da die neuen Zeiten außerhalb der alten liegen!'), '<strong>'.htmlReady($termin->getFullName()) .'</strong>' )); } else if (Request::get('room_id_parameter')) { PageLayout::postInfo( - _('Um eine Raumbuchung durchzuführen, müssen Sie einen Raum aus dem Suchergebnis auswählen!') + _('Um Raumbuchungen durchzuführen, müssen Sie einen Raum aus dem Suchergebnis auswählen!') ); } - } elseif (Request::option('room') == 'freetext') { - $termin->raum = Request::get('freeRoomText_sd'); - if ($termin->room_booking) { - $termin->room_booking->delete(); + } elseif (Request::option('room') === 'freetext') { + $termin->raum = Request::get('room_name'); + if ($termin->room_bookings) { + $termin->room_bookings->each(function($b){$b->delete();}); } $termin->store(); - PageLayout::postSuccess(sprintf( - _('Der Termin %s wurde geändert, Raumbuchungen zu diesem Termin wurden entfernt und stattdessen der angegebene Freitext eingetragen!'), - '<strong>' . htmlReady($termin->getFullName()) . '</strong>' - )); + if (!$new_date) { + PageLayout::postSuccess(sprintf( + _('Der Termin %s wurde geändert, Raumbuchungen zu diesem Termin wurden entfernt und stattdessen der angegebene Freitext eingetragen!'), + '<strong>' . htmlReady($termin->getFullName()) . '</strong>' + )); + } } elseif (Request::option('room') == 'noroom') { $termin->raum = ''; - if ($termin->room_booking) { - $termin->room_booking->delete(); + if ($termin->room_bookings) { + $termin->room_bookings->each(function($b){$b->delete();}); } $termin->store(); - PageLayout::postSuccess(sprintf( - _('Der Termin %s wurde geändert, freie Ortsangaben und Raumbuchungen an diesem Termin wurden entfernt.'), - '<strong>' . htmlReady($termin) . '</strong>' - )); + if (!$new_date) { + PageLayout::postSuccess(sprintf( + _('Der Termin %s wurde geändert, freie Ortsangaben und Raumbuchungen an diesem Termin wurden entfernt.'), + '<strong>' . htmlReady($termin) . '</strong>' + )); + } + } + if ($new_date) { + PageLayout::postSuccess(_('Der Termin wurde gespeichert.')); } $this->redirect('course/timesrooms/index', ['contentbox_open' => $termin->metadate_id]); } @@ -659,91 +854,28 @@ class Course_TimesroomsController extends AuthenticatedController $_SESSION['last_single_date'] ?? null ); - if (Config::get()->RESOURCES_ENABLE) { - $this->setAvailableRooms(null); - } - $this->teachers = $this->course->getMembersWithStatus('dozent'); - $this->groups = Statusgruppen::findBySeminar_id($this->course->id); - } - - /** - * Save Single Date - * - * @throws Trails\Exceptions\DoubleRenderError - */ - public function saveSingleDate_action() - { - CSRFProtection::verifyUnsafeRequest(); - - $start_time = strtotime(sprintf('%s %s:00', Request::get('date'), Request::get('start_time'))); - $end_time = strtotime(sprintf('%s %s:00', Request::get('date'), Request::get('end_time'))); - - if ($start_time === false || $end_time === false || $start_time > $end_time) { - $this->storeRequest(); - - PageLayout::postError(_('Die Zeitangaben sind nicht korrekt. Bitte überprüfen Sie diese!')); - $this->redirect('course/timesrooms/createSingleDate'); - - return; - } - if ($this->bookingTooShort($start_time, $end_time)) { - PageLayout::postError( - sprintf( - ngettext( - 'Die minimale Dauer eines Termins von einer Minute wurde unterschritten.', - 'Die minimale Dauer eines Termins von %u Minuten wurde unterschritten.', - Config::get()->RESOURCES_MIN_BOOKING_TIME - ), - Config::get()->RESOURCES_MIN_BOOKING_TIME - ) - ); - $this->redirect('course/timesrooms/createSingleDate'); - return; - } - - $termin = new CourseDate(); - $termin->termin_id = $termin->getNewId(); - $termin->range_id = $this->course->id; - $termin->date = $start_time; - $termin->end_time = $end_time; - $termin->autor_id = $GLOBALS['user']->id; - $termin->date_typ = Request::get('dateType'); - - $current_count = CourseMember::countByCourseAndStatus($this->course->id, 'dozent'); - $related_ids = Request::optionArray('related_teachers'); - if ($related_ids && count($related_ids) !== $current_count) { - $termin->dozenten = User::findMany($related_ids); - } - - $groups = Statusgruppen::findBySeminar_id($this->course->id); - $related_groups = Request::getArray('related_statusgruppen'); - if ($related_groups && count($related_groups) !== count($groups)) { - $termin->statusgruppen = Statusgruppen::findMany($related_groups); + $this->date_types = []; + foreach ($GLOBALS['TERMIN_TYP'] as $id => $data) { + $this->date_types[] = [ + 'id' => $id, + 'name' => $data['name'] + ]; } - - if (!Request::get('room_id')) { - $termin->raum = Request::get('freeRoomText'); - $termin->store(); - } else { - $termin->store(); - $room = Resource::find(Request::option('room_id'))?->getDerivedClassInstance(); - if ($room instanceof Room) { - $termin->bookRoom($room); - } + $lecturer_objects = $this->course->getMembersWithStatus('dozent'); + $group_objects = Statusgruppen::findBySeminar_id($this->course->id); + $this->available_lecturers = []; + $this->available_groups = []; + foreach ($lecturer_objects as $lecturer) { + $this->available_lecturers[$lecturer->user_id] = $lecturer->getUserFullname(); } - if (Request::get('room_id_parameter')) { - PageLayout::postInfo( - _('Um eine Raumbuchung durchzuführen, müssen Sie einen Raum aus dem Suchergebnis auswählen!') - ); + foreach ($group_objects as $group) { + $this->available_groups[$group->id] = $group->name; } - // store last created date in session - $_SESSION['last_single_date'] = Request::getInstance(); - - PageLayout::postSuccess(sprintf(_('Der Termin %s wurde hinzugefügt!'), htmlReady($termin->getFullname()))); - $this->course->store(); - - $this->relocate('course/timesrooms/index'); + $this->allow_multiple_room_bookings = ResourceManager::userHasGlobalPermission( + User::findCurrent(), + Config::get()->ROOM_PERMISSIONS_FOR_MULTIPLE_BOOKINGS_PER_COURSE_DATE + ); } /** @@ -825,47 +957,58 @@ class Course_TimesroomsController extends AuthenticatedController $this->teachers = $this->course->getMembersWithStatus('dozent'); $this->gruppen = Statusgruppen::findBySeminar_id($this->course->id); $checked_course_dates = CourseDate::findMany($_SESSION['_checked_dates']); - $this->only_bookable_rooms = Request::submitted('only_bookable_rooms'); - - $date_booking_ids = []; - if ($this->only_bookable_rooms) { - $date_ids = []; - foreach ($checked_course_dates as $date) { - $date_ids[] = $date->termin_id; - } - $db = DBManager::get(); - $stmt = $db->prepare( - "SELECT DISTINCT `id` FROM `resource_bookings` WHERE `range_id` IN ( :date_ids )" - ); - $stmt->execute(['date_ids' => $date_ids]); - $date_booking_ids = $stmt->fetchAll(PDO::FETCH_COLUMN, 0); - } - $this->setAvailableRooms($checked_course_dates, $date_booking_ids, $this->only_bookable_rooms); - /* - * Extract a single date for start and end time - * (all cycle dates have the same start and end time, - * so it doesn't matter which date we get). - */ - $this->date = CourseDate::findOneByMetadate_id($cycle_id); $this->checked_dates = $_SESSION['_checked_dates']; $this->selected_lecturer_ids = $this->getSameFieldValue($checked_course_dates, function (CourseDate $date) { return $date->dozenten->pluck('user_id'); }); - $this->selected_room_id = $this->getSameFieldValue($checked_course_dates, function (CourseDate $date) { - return $date->room_booking->resource_id ?? ''; + $this->selected_room_ids = $this->getSameFieldValue($checked_course_dates, function (CourseDate $date) { + $ids = []; + foreach ($date->room_bookings as $booking) { + $ids[] = $booking->resource->id; + } + return $ids; }); $this->selected_room_name = $this->getSameFieldValue($checked_course_dates, function (CourseDate $date) { return $date->raum ?? ''; }); $preparation_time = $this->getSameFieldValue($checked_course_dates, function (CourseDate $date) { - return $date->room_booking->preparation_time ?? 0; + $first_booking = null; + if (count($date->room_bookings) > 0) { + $first_booking = $date->room_bookings[0]; + } + return $first_booking->preparation_time ?? 0; + }); + $subsequent_time = $this->getSameFieldValue($checked_course_dates, function (CourseDate $date) { + $first_booking = null; + if (count($date->room_bookings) > 0) { + $first_booking = $date->room_bookings[0]; + } + return $first_booking->subsequent_time ?? 0; + }); + $this->time_ranges = []; + foreach ($checked_course_dates as $course_date) { + $this->time_ranges[] = [ + 'start' => $course_date->date, + 'end' => $course_date->end_time + ]; + } + $this->max_preparation_time = intval(Config::get()->RESOURCES_MAX_PREPARATION_TIME); + $this->preparation_time = intval($preparation_time) / 60; + $this->subsequent_time = intval($subsequent_time) / 60; + $this->allow_multiple_room_bookings = ResourceManager::userHasGlobalPermission( + User::findCurrent(), + Config::get()->ROOM_PERMISSIONS_FOR_MULTIPLE_BOOKINGS_PER_COURSE_DATE + ); + $this->selected_room_ids = $this->getSameFieldValue($checked_course_dates, function (CourseDate $date) { + $room_ids = []; + foreach ($date->room_bookings as $booking) { + $room_ids[] = $booking->resource_id; + } + return $room_ids; }); - $this->preparation_time = floor($preparation_time / 60); - - $this->max_preparation_time = Config::get()->RESOURCES_MAX_PREPARATION_TIME; $this->render_template('course/timesrooms/editStack'); } @@ -987,11 +1130,6 @@ class Course_TimesroomsController extends AuthenticatedController public function saveStack_action($cycle_id = '') { CSRFProtection::verifyUnsafeRequest(); - if (Request::submitted('only_bookable_rooms')) { - //Redirect to the editStack method and do not save anything. - $this->editStack($cycle_id); - return; - } switch (Request::get('method')) { case 'edit': $this->saveEditedStack(); @@ -1129,17 +1267,44 @@ class Course_TimesroomsController extends AuthenticatedController PageLayout::postSuccess(_('Zugewiesene Gruppen für die Termine wurden geändert.')); } - if (in_array(Request::get('action'), ['room', 'freetext', 'noroom']) || Request::get('course_type')) { + if (in_array(Request::get('room'), ['room', 'freetext', 'noroom']) || Request::get('course_type')) { $success_cases = 0; $success_messages = []; $error_messages = []; + $room_ids = Request::getArray('room_ids'); + $rooms = []; + //Collect all rooms and distinguish between real rooms and separable rooms: + foreach ($room_ids as $room_id) { + $id_parts = explode('-', $room_id); + if (count($id_parts) != 2) { + //Invalid ID. + continue; + } + if ($id_parts[0] === 'room') { + //The ID belongs to a real room. + $room = Room::find($id_parts[1]); + if ($room) { + $rooms[$room->id] = $room; + } + } elseif ($id_parts[0] === 'separable_room') { + //The ID belongs to a separable room: Get all the room parts. + $separable_room = SeparableRoom::find($id_parts[1]); + foreach ($separable_room->parts as $part) { + if ($part->room instanceof Room) { + $rooms[$part->room->id] = $part->room; + } + } + } + } + foreach ($singledates as $singledate) { if ($singledate instanceof CourseExDate) { continue; } - if (Request::option('action') == 'room') { - $preparation_time = Request::get('preparation_time'); - $max_preparation_time = Config::get()->RESOURCES_MAX_PREPARATION_TIME; + if (Request::get('room') === 'room') { + $preparation_time = Request::int('preparation_time', 0); + $subsequent_time = Request::int('subsequent_time', 0); + $max_preparation_time = intval(Config::get()->RESOURCES_MAX_PREPARATION_TIME); if ($preparation_time > $max_preparation_time) { $error_messages[] = sprintf( studip_interpolate( @@ -1150,12 +1315,13 @@ class Course_TimesroomsController extends AuthenticatedController ); continue; } - if (Request::option('room_id')) { - $room = Resource::find(Request::option('room_id'))?->getDerivedClassInstance(); - if ($room instanceof Room) { + if (!empty($room_ids)) { + //Delete all existing bookings for the date: + ResourceBooking::deleteBySQL('`range_id` = :date_id',['date_id' => $singledate->id]); + foreach ($rooms as $room) { $failure = false; try { - $failure = !$singledate->bookRoom($room, intval($preparation_time)); + $failure = !$singledate->bookRoom($room, $preparation_time, $subsequent_time); } catch (ResourceBookingException $e) { $error_messages[] = sprintf( _('Der angegebene Raum konnte für den Termin %1$s nicht gebucht werden: %2$s'), @@ -1168,8 +1334,8 @@ class Course_TimesroomsController extends AuthenticatedController $error_messages[] = studip_interpolate( _('Der Raum %{room_name} wird an dem Termin %{date} bereits durch die Veranstaltung %{course_name} belegt.'), [ - 'room_name' => $room->name, - 'date' => $singledate->getFullName(), + 'room_name' => $room->name, + 'date' => $singledate->getFullName(), 'course_name' => $course->name ] ); @@ -1177,8 +1343,8 @@ class Course_TimesroomsController extends AuthenticatedController $error_messages[] = studip_interpolate( _('Der Raum %{room_name} wird an dem Termin %{date} bereits anderweitig belegt.'), [ - 'room_name' => $room->name, - 'date' => $singledate->getFullName() + 'room_name' => $room->name, + 'date' => $singledate->getFullName() ] ); } @@ -1192,26 +1358,25 @@ class Course_TimesroomsController extends AuthenticatedController $success_cases++; } } - } else if (Request::get('room_id_parameter')) { - PageLayout::postInfo( - ('Um eine Raumbuchung durchzuführen, müssen Sie einen Raum aus dem Suchergebnis auswählen!') - ); + } elseif (Request::get('room') === 'room') { + //No room has been selected despite having the room selector set to book at least one room. + PageLayout::postInfo(_('Um eine Raumbuchung durchzuführen, müssen Sie einen Raum aus dem Suchergebnis auswählen!')); } - } elseif (Request::option('action') == 'freetext') { - $singledate->raum = Request::get('freeRoomText'); + } elseif (Request::get('room') === 'freetext') { + $singledate->raum = Request::get('room_name'); $singledate->store(); - if ($singledate->room_booking instanceof ResourceBooking) { - $singledate->room_booking->delete(); + if (!empty($singledate->room_bookings)) { + $singledate->room_bookings->each(function($b){$b->delete();}); } $success_messages[] = sprintf( _('Der Termin %s wurde geändert, etwaige Raumbuchungen wurden entfernt und stattdessen der angegebene Freitext eingetragen!'), '<strong>' . htmlReady($singledate->getFullName()) . '</strong>' ); - } elseif (Request::option('action') == 'noroom') { + } elseif (Request::get('room') == 'noroom') { $singledate->raum = ''; $singledate->store(); - if ($singledate->room_booking instanceof ResourceBooking) { - $singledate->room_booking->delete(); + if (!empty($singledate->room_bookings)) { + $singledate->room_bookings->each(function($b){$b->delete();}); } $success_messages[] = sprintf( _('Der Termin %s wurde geändert, etwaige freie Ortsangaben und Raumbuchungen wurden entfernt.'), @@ -1714,7 +1879,7 @@ class Course_TimesroomsController extends AuthenticatedController private function deleteDate($termin, $cancel_comment) { $seminar_id = $termin->range_id; - $termin_room = $termin->getRoomName(); + $termin_room = $termin->getRoomNames(); $termin_date = $termin->getFullName(); $has_topics = $termin->topics->count(); @@ -1750,10 +1915,10 @@ class Course_TimesroomsController extends AuthenticatedController if ($termin_room) { PageLayout::postSuccess( studip_interpolate( - _('Der Termin %{date} wurde gelöscht! Die Buchung für den Raum %{room} wurde gelöscht.'), + _('Der Termin %{date} wurde gelöscht! Die Raumbuchungen für die folgenden Räume wurden gelöscht: %{room_names}'), [ - 'date' => htmlReady($termin_date), - 'room' => htmlReady($termin_room) + 'date' => htmlReady($termin_date), + 'room_names' => htmlReady($termin_room) ] ) ); @@ -1822,99 +1987,6 @@ class Course_TimesroomsController extends AuthenticatedController } } - - protected function setAvailableRooms($dates, $date_booking_ids = [], $only_bookable_rooms = false) - { - $this->room_search = null; - $this->selectable_rooms = []; - if (Config::get()->RESOURCES_ENABLE) { - //Check for how many rooms the user has booking permissions. - //In case these permissions exist for more than 50 rooms - //show a quick search. Otherwise show a select field - //with the list of rooms. - - $current_user = User::findCurrent(); - $current_user_is_resource_admin = ResourceManager::userHasGlobalPermission( - $current_user, - 'admin' - ); - $all_time_intervals = []; - if (!empty($dates)) { - $dates = SimpleCollection::createFromArray($dates); - foreach ($dates as $date) { - $begin = new DateTime(); - $begin->setTimestamp($date->date); - $end = new DateTime(); - $end->setTimestamp($date->end_time); - $all_time_intervals[] = [ - 'begin' => $begin, - 'end' => $end - ]; - } - } - $this->selectable_rooms = []; - $rooms_with_booking_permissions = 0; - if ($current_user_is_resource_admin) { - $rooms_with_booking_permissions = Room::countAll(); - } else { - $user_rooms = RoomManager::getUserRooms($current_user); - foreach ($user_rooms as $room) { - if ($room->userHasBookingRights($current_user, $begin, $end)) { - $rooms_with_booking_permissions++; - if ($only_bookable_rooms) { - foreach ($all_time_intervals as $interval) { - $available = $room->isAvailable($interval['begin'], $interval['end'], $date_booking_ids); - if (!$available) { - continue 2; - } - } - //At this point, the room is available on all - //time intervals. - $this->selectable_rooms[] = $room; - } else { - $this->selectable_rooms[] = $room; - } - } - } - } - - if (($rooms_with_booking_permissions > 50) && !$only_bookable_rooms) { - $room_search_type = new RoomSearch(); - $room_search_type->with_seats = 0; - $room_search_type->setAcceptedPermissionLevels( - ['autor', 'tutor', 'admin'] - ); - $room_search_type->setAdditionalDisplayProperties( - ['seats'] - ); - $room_search_type->setAdditionalPropertyFormat(_('(%s Sitzplätze)')); - $this->room_search = new QuickSearch( - 'room_id', - $room_search_type - ); - } else { - if (ResourceManager::userHasGlobalPermission($current_user, 'admin')) { - if ($only_bookable_rooms) { - $rooms = Room::findAll(); - foreach ($rooms as $room) { - foreach ($all_time_intervals as $interval) { - $available = $room->isAvailable($interval['begin'], $interval['end'], $date_booking_ids); - $booking_rights = $room->userHasBookingRights($current_user, $interval['begin'], $interval['end']); - - if (!$available || !$booking_rights) { - continue 2; - } - } - $this->selectable_rooms[] = $room; - } - } else { - $this->selectable_rooms = Room::findAll(); - } - } - } - } - } - private function validateDateIds(array $date_ids): array { if (count($date_ids) === 0) { diff --git a/app/controllers/resources/admin.php b/app/controllers/resources/admin.php index bea43ed..5d36b34 100644 --- a/app/controllers/resources/admin.php +++ b/app/controllers/resources/admin.php @@ -1034,6 +1034,45 @@ class Resources_AdminController extends AuthenticatedController } + public function edit_separable_room_action($separable_room_id) + { + PageLayout::setTitle(_('Teilbaren Raum bearbeiten')); + $this->separable_room = SeparableRoom::find($separable_room_id); + if (!$this->separable_room) { + PageLayout::postError(_('Der teilbare Raum wurde nicht gefunden.')); + } + } + + + public function save_separable_room_action($separable_room_id) + { + CSRFProtection::verifyUnsafeRequest(); + + $this->separable_room = SeparableRoom::find($separable_room_id); + if (!$this->separable_room) { + PageLayout::postError(_('Der teilbare Raum wurde nicht gefunden.')); + $this->relocate('resources/admin/separable_rooms'); + return; + } + + $this->separable_room->name = Request::get('name', ''); + $this->separable_room->description = Request::get('description', ''); + if (!$this->separable_room->name) { + PageLayout::postError(_('Der Name des teilbaren Raumes darf nicht leer sein!')); + $this->relocate('resources/admin/separable_rooms'); + return; + } + + $success = $this->separable_room->store() !== false; + if ($success) { + PageLayout::postSuccess(_('Die Änderungen wurden gespeichert.')); + } else { + PageLayout::postError(_('Die Änderungen konnten nicht gespeichert werden.')); + } + $this->relocate('resources/admin/separable_rooms'); + } + + public function configuration_action() { if (Navigation::hasItem('/resources/admin/configuration')) { diff --git a/app/controllers/resources/room_request.php b/app/controllers/resources/room_request.php index ebe7baa..f7d9a66 100644 --- a/app/controllers/resources/room_request.php +++ b/app/controllers/resources/room_request.php @@ -1288,9 +1288,9 @@ class Resources_RoomRequestController extends AuthenticatedController foreach ($this->request_time_intervals as $metadate_id => $data) { if ($data['metadate'] instanceof SeminarCycleDate && !$this->expand_metadates) { $all_dates_same_room = true; - // check, if ALL dates are booked for the same room + //Check if all dates are booked for the same room: foreach ($data['intervals'] as $interval) { - if ($interval['booked_room'] != $selected_room->id) { + if (!in_array($selected_room->id, $interval['booked_rooms'])) { $all_dates_same_room = false; break; } @@ -1567,7 +1567,12 @@ class Resources_RoomRequestController extends AuthenticatedController return; } - if (!$course_date->room_booking || $course_date->room_booking->resource_id !== $room_id) { + $room_ids = []; + foreach ($course_date->room_bookings as $booking) { + $room_ids[] = $booking->resource_id; + } + + if (empty($room_ids) || !in_array($room_id, $room_ids)) { try { $booking = $room->createBooking( $this->current_user, @@ -1623,7 +1628,11 @@ class Resources_RoomRequestController extends AuthenticatedController if ($metadate->dates) { $overlap_messages = []; foreach ($metadate->dates as $date) { - if (!$date->room_booking || $date->room_booking->resource_id != $room_id) { + $room_ids = []; + foreach ($date->room_bookings as $booking) { + $room_ids[] = $booking->resource_id; + } + if (empty($room_ids) || !in_array($room_id, $room_ids)) { try { $booking = $room->createBooking( $this->current_user, |
