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 | |
| parent | f637e7ae2d086941a11297ccc29ac273ad6759b0 (diff) | |
allow booking separable rooms in courses, closes #639
Closes #639
Merge request studip/studip!4039
44 files changed, 2255 insertions, 1439 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, diff --git a/app/views/calendar/contentbox/_termin.php b/app/views/calendar/contentbox/_termin.php index 963116d..39e1455 100644 --- a/app/views/calendar/contentbox/_termin.php +++ b/app/views/calendar/contentbox/_termin.php @@ -20,7 +20,7 @@ <? if ($termin instanceof CalendarDateAssignment): ?> <?= $termin->getLocation() ? _('Raum') . ': ' . formatLinks($termin->getLocation()) : '' ?> <? else: ?> - <?= $termin->getRoomName() ? _('Raum') . ': ' . formatLinks($termin->getRoomName()) : '' ?> + <?= $termin->getRoomNames() ? _('Raum') . ': ' . formatLinks($termin->getRoomNames()) : '' ?> <? endif; ?> </span> <? if ($admin && $isProfile && $termin->getObjectClass() === 'CalendarDateAssignment') : ?> diff --git a/app/views/course/block_appointments/index.php b/app/views/course/block_appointments/index.php index da8d693..6329f20 100644 --- a/app/views/course/block_appointments/index.php +++ b/app/views/course/block_appointments/index.php @@ -1,153 +1,39 @@ -<? if (!Request::isXhr()) : ?> - <h1><?= _('Neuen Blocktermin anlegen') ?></h1> -<? endif ?> - +<?php +/** + * @var Course_BlockAppointmentsController $controller + * @var string $course_id + * @var int $preparation_time + * @var int $subsequent_time + * @var int $max_preparation_time + * @var array $date_types + * @var array $dow + * @var int $selected_date_type + * @var array $lecturers + * @var bool $allow_multiple_room_bookings + * @var string $selected_date_type + * @var array $selected_lecturer_ids + * @var array $available_lecturers + * @var array $assigned_lecturers + */ +?> <form <?= Request::isXhr() ? 'data-dialog="size=big"' : '' ?> - class="default collapsable" + class="default" action="<?= $controller->link_for('course/block_appointments/save/' . $course_id) ?>" method="post"> - -<? if ($confirm_many): ?> - <?= MessageBox::info(sprintf( - _('Sie legen %s%u%s Termine an. Bitte kontrollieren Sie Ihre Eingaben ' - . 'oder bestätigen Sie, dass die Termine angelegt werden sollen.'), - '<strong>', - $confirm_many, - '</strong>' - ), [ - sprintf( - '<label><input type="checkbox" name="confirmed" value="1">%s</label>', - studip_interpolate(_('Ja, ich möchte wirklich %{ n } Termine erstellen.'), ['n' => $confirm_many]) - ), - ]) ?> -<? endif; ?> - - <fieldset> - <legend><?= _('Zeitraum') ?></legend> - <label for="block_appointments_start_day" class="col-3"> - <?= _('Startdatum') ?> - <input type="text" class="size-s has-date-picker" data-date-picker id="block_appointments_start_day" - name="block_appointments_start_day" value="<?= $request['block_appointments_start_day'] ?? '' ?>"> - </label> - <label for="block_appointments_end_day" class="col-3"> - <?= _('Enddatum') ?> - <input type="text" class="size-s has-date-picker" data-date-picker='{">=":"#block_appointments_start_day"}' id="block_appointments_end_day" - name="block_appointments_end_day" value="<?= $request['block_appointments_end_day'] ?? '' ?>"> - </label> - <label for="block_appointments_start_time" class="col-3"> - <?= _('Startzeit') ?> - <input type="text" class="size-s studip-timepicker" id="block_appointments_start_time" - name="block_appointments_start_time" value="<?= $request['block_appointments_start_time'] ?? '' ?>" - placeholder="HH:mm"> - </label> - - <label for="block_appointments_end_time" class="col-3"> - <?= _('Endzeit') ?> - <input type="text" class="size-s studip-timepicker" id="block_appointments_end_time" - name="block_appointments_end_time" value="<?= $request['block_appointments_end_time'] ?? '' ?>" - placeholder="HH:mm"> - </label> - - <div id="block_appointments_days"> - <label><?= _('Die Veranstaltung findet an folgenden Tagen statt') ?></label> - <label for="block_appointments_days_0" class="col-2"> - <input <?= empty($request['block_appointments_days']) || in_array('everyday', $request['block_appointments_days']) ? 'checked' : '' ?> - class="block_appointments_days" - name="block_appointments_days[]" id="block_appointments_days_0" type="checkbox" value="everyday"> - <?= _('Jeden Tag') ?> - </label> - - <label for="block_appointments_days_1" class="col-2"> - <input <?= in_array('weekdays', (array) ($request['block_appointments_days'] ?? [])) ? 'checked ' : '' ?> - class="block_appointments_days" - name="block_appointments_days[]" id="block_appointments_days_1" type="checkbox" value="weekdays"> - <?= _('Mo-Fr') ?> - </label> - <? foreach (range(0, 6) as $d) : ?> - <? $id = 2 + $d ?> - <label for="block_appointments_days_<?= $id ?>" class="col-2"> - <input <?= in_array($d + 1, (array) ($request['block_appointments_days'] ?? [])) ? 'checked ' : '' ?> - class="block_appointments_days" - name="block_appointments_days[]" id="block_appointments_days_<?= $id ?>" type="checkbox" - value="<?= $d + 1 ?>"> - <?= strftime('%A', strtotime("+$d day", $start_ts)) ?> - </label> - <? endforeach ?> - </div> - - </fieldset> - - <fieldset class="collapsed"> - <legend><?= _('Weitere Daten') ?></legend> - <label for="block_appointments_termin_typ"> - <?= _('Art der Termine') ?> - <select clas="size-l" name="block_appointments_termin_typ" id="block_appointments_termin_typ"> - <? foreach ($GLOBALS['TERMIN_TYP'] as $key => $value) : ?> - <option - value="<?= $key ?>" <?= ($request['block_appointments_termin_typ'] ?? '') == $key ? 'selected' : '' ?>> - <?= htmlReady($value['name']) ?> - </option> - <? endforeach ?> - </select> - </label> - - <? if (Config::get()->RESOURCES_ENABLE && ($selectable_rooms || $room_search)) : ?> - <label> - <?= _('Raum') ?> - <? if ($room_search): ?> - <?= $room_search->render() ?> - <? else: ?> - <select name="room_id" style="width: calc(100% - 23px);"> - <option value=""><?= _('<em>Keinen</em> Raum buchen') ?></option> - <? foreach ($selectable_rooms as $room): ?> - <option value="<?= htmlReady($room->id) ?>"> - <?= htmlReady($room->name) ?> - <? if ($room->seats > 1) : ?> - <?= sprintf(_('(%d Sitzplätze)'), $room->seats) ?> - <? endif ?> - </option> - <? endforeach ?> - </select> - <? endif ?> - </label> - <? endif ?> - - <label for="block_appointments_room_text"> - <?= _('Freie Ortsangabe') ?> - <input type="text" name="block_appointments_room_text" id="block_appointments_room_text" - value="<?= htmlReady($request['block_appointments_room_text'] ?? '') ?>"> - </label> - - <? if (count($lecturers)): ?> - <label for="lecturers[]"> - <?= _('Durchführende Lehrende') ?> - <? if (count($lecturers) > 1): ?> - <select name="lecturers[]" multiple="multiple" class="multiple"> - <? foreach ($lecturers as $lecturer): ?> - <option value="<?= $lecturer->user_id ?>" - <? if (in_array($lecturer->user_id, Request::optionArray('lecturers'))) echo 'selected'; ?>> - <?= htmlReady($lecturer->user->getFullName()) ?> - </option> - <? endforeach ?> - </select> - <? else: ?> - <p style="margin-left: 15px"> - <? $lecturer = array_pop($lecturers) ?> - <?= htmlReady($lecturer->user->getFullName()) ?> - </p> - <? endif; ?> - </label> - <? endif ?> - - <label for="block_appointments_date_count"> - <?= _('Anzahl der Termine') ?> - <input type="text" name="block_appointments_date_count" id="block_appointments_date_count" class="size-s" value="<?= $request['block_appointments_date_count'] ?? 1 ?>"> - </label> - - </fieldset> - + <?= CSRFProtection::tokenTag() ?> + <?= Studip\VueApp::create('CourseBlockAppointments') + ->withProps([ + 'initial_preparation_time' => $preparation_time, + 'initial_subsequent_time' => $subsequent_time, + 'max_preparation_time' => $max_preparation_time, + 'room_management_enabled' => Config::get()->RESOURCES_ENABLE, + 'allow_multiple_room_bookings' => $allow_multiple_room_bookings ?? false, + 'date_types' => $date_types ?? [], + 'available_lecturers' => $available_lecturers ?? [], + 'selected_lecturers' => $selected_lecturer_ids + ]) ?> <footer data-dialog-button> <?= Studip\Button::createAccept(_('Speichern'), 'save') ?> - <?= Studip\LinkButton::create(_('Zurück zur Übersicht'), $controller->url_for('course/timesrooms/index'), ['data-dialog' => 'size=big']) ?> + <?= Studip\LinkButton::createCancel(_('Abbrechen'), $controller->url_for('course/timesrooms/index'), ['data-dialog' => 'size=big']) ?> </footer> </form> diff --git a/app/views/course/dates/_date_row.php b/app/views/course/dates/_date_row.php index b45bd55..4238af3 100644 --- a/app/views/course/dates/_date_row.php +++ b/app/views/course/dates/_date_row.php @@ -54,11 +54,16 @@ $dialog_url = $show_raumzeit </td> <? endif ?> <td> - <? $room = $date->getRoom(); ?> - <? if ($room): ?> - <a href="<?= $room->getActionLink('show') ?>" data-dialog> - <?= htmlReady($room->name) ?> - </a> + <? $rooms = $date->getRooms(); ?> + <? if ($rooms): ?> + <? foreach ($rooms as $room) : ?> + <span class="no-break"> + <a href="<?= $room->getActionLink('show') ?>" data-dialog> + <?= Icon::create('link-intern')->asImg(16, ['class' => 'text-bottom']) ?> + <?= htmlReady($room->name) ?> + </a> + </span> + <? endforeach ?> <? else: ?> <?= htmlReady($date->raum) ?> <? endif ?> diff --git a/app/views/course/dates/current_day_dates.php b/app/views/course/dates/current_day_dates.php index 9df4b09..38a1299 100644 --- a/app/views/course/dates/current_day_dates.php +++ b/app/views/course/dates/current_day_dates.php @@ -16,11 +16,13 @@ <?= htmlReady($date->getFullName(CourseDate::FORMAT_VERBOSE)) ?> </td> <td> - <? $room = $date->getRoom(); ?> - <? if ($room): ?> - <a href="<?= $room->getActionLink('show') ?>" data-dialog> - <?= htmlReady($room->name) ?> - </a> + <? $rooms = $date->getRooms(); ?> + <? if ($rooms): ?> + <? foreach ($rooms as $room) : ?> + <a href="<?= $room->getActionLink('show') ?>" data-dialog> + <?= htmlReady($room->name) ?> + </a> + <? endforeach ?> <? else: ?> <?= htmlReady($date->raum) ?> <? endif ?> diff --git a/app/views/course/timesrooms/_cycleRow.php b/app/views/course/timesrooms/_cycleRow.php index ce286ed..c6495be 100644 --- a/app/views/course/timesrooms/_cycleRow.php +++ b/app/views/course/timesrooms/_cycleRow.php @@ -18,7 +18,7 @@ $is_exTermin = $termin instanceof CourseExDate; </td> <? endif ?> - <td class="<?= $termin->getRoom() !== null ? 'green' : 'red' ?>"> + <td class="<?= !empty($termin->getRooms()) ? 'green' : 'red' ?>"> <? if ($is_exTermin) : ?> <span class="is_ex_termin"> <?= htmlReady($termin->getFullName(CourseDate::FORMAT_VERBOSE)) ?> @@ -26,7 +26,7 @@ $is_exTermin = $termin instanceof CourseExDate; <? elseif ($locked): ?> <?= htmlReady($termin->getFullName(CourseDate::FORMAT_VERBOSE)) ?> <? else: ?> - <a data-dialog + <a data-dialog="size=600" href="<?= $controller->url_for('course/timesrooms/editDate/' . $termin->termin_id, $linkAttributes) ?>"> <?= htmlReady($termin->getFullName(CourseDate::FORMAT_VERBOSE)) ?> </a> @@ -65,17 +65,18 @@ $is_exTermin = $termin instanceof CourseExDate; <?= tooltipIcon($termin->content) ?> <? elseif ($name = SemesterHoliday::isHoliday($termin->date, false) && $is_exTermin): ?> <?= $room_holiday ?> - <? elseif ($room = $termin->getRoom()) : ?> - <a href="<?= $room->getActionLink( - 'booking_plan', - [ - 'defaultDate' => date('Y-m-d', $termin->date) - ] - ) ?>" data-dialog="size=big"> - <?= htmlReady($room->getFullName()) ?> - </a> + <? elseif ($rooms = $termin->getRooms()) : ?> + <? foreach ($rooms as $room) : ?> + <span class="no-break"> + <a href="<?= $room->getActionLink('booking_plan', ['defaultDate' => date('Y-m-d', $termin->date)]) ?>" + data-dialog="size=big"> + <?= Icon::create('link-intern')->asImg(16, ['class' => 'text-bottom']) ?> + <?= htmlReady($room->getFullName()) ?> + </a> + </span> + <? endforeach ?> <?= $room_holiday ?: '' ?> - <? elseif ($freeTextRoom = $termin->getRoomName()) : ?> + <? elseif ($freeTextRoom = $termin->getRoomNames()) : ?> <?= sprintf('(%s)', formatLinks($freeTextRoom)) ?> <? else : ?> <?= _('Keine Raumangabe') ?> @@ -124,7 +125,7 @@ $is_exTermin = $termin instanceof CourseExDate; $controller->url_for('course/timesrooms/editDate/' . $termin->id, $linkAttributes), _('Termin bearbeiten'), Icon::create('edit'), - ['data-dialog' => ''] + ['data-dialog' => 'width=430;height=720'] ) ?> <? if (!$termin->metadate_id): ?> <? $actionMenu->addLink( diff --git a/app/views/course/timesrooms/_irregularEvents.php b/app/views/course/timesrooms/_irregularEvents.php index 8ab45f3..b123877 100644 --- a/app/views/course/timesrooms/_irregularEvents.php +++ b/app/views/course/timesrooms/_irregularEvents.php @@ -19,7 +19,7 @@ $room_request_filter = function ($date) { $controller->url_for('course/timesrooms/createSingleDate/' . $course->id, $linkAttributes), _('Einzeltermin hinzufügen'), Icon::create('date', Icon::ROLE_CLICKABLE, ['title' => _('Einzeltermin hinzufügen')]), - ['data-dialog' => 'size=600'] + ['data-dialog' => 'width=430;height=720'] ) ?> <? $actionMenu->addLink( diff --git a/app/views/course/timesrooms/createSingleDate.php b/app/views/course/timesrooms/createSingleDate.php index f8e08d5..1dc080a 100644 --- a/app/views/course/timesrooms/createSingleDate.php +++ b/app/views/course/timesrooms/createSingleDate.php @@ -1,105 +1,37 @@ -<form action="<?= $controller->url_for('course/timesrooms/saveSingleDate') ?>" method="post"
- class="default" <?= Request::int('fromDialog') ? 'data-dialog="size=big"' : '' ?>>
- <?= CSRFProtection::tokenTag() ?>
- <fieldset>
- <legend><?= _('Einzeltermin anlegen') ?></legend>
-
- <label class="col-2">
- <?= _('Datum') ?>
- <input class="has-date-picker size-s" type="text" name="date"
- value="<?= htmlReady(Request::get('date')) ?>" required>
- </label>
- <label class="col-2">
- <?= _('Startzeit') ?>
- <input class="studip-timepicker size-s" type="text" name="start_time"
- value="<?= htmlReady(Request::get('start_time')) ?>" required placeholder="HH:mm">
- </label>
- <label class="col-2">
- <?= _('Endzeit') ?>
- <input class="studip-timepicker size-s" type="text" name="end_time"
- value="<?= htmlReady(Request::get('end_time')) ?>" required placeholder="HH:mm">
- </label>
-
- <label for="dateType">
- <?= _('Art') ?>
- <select id="dateType" name="dateType">
- <? foreach ($GLOBALS['TERMIN_TYP'] as $key => $val) : ?>
- <option <?= Request::get('dateType') == $key ? 'selected' : '' ?>
- value="<?= $key ?>"><?= htmlReady($val['name']) ?></option>
- <? endforeach ?>
- </select>
- </label>
-
- <? if (Config::get()->RESOURCES_ENABLE
- && ($selectable_rooms || $room_search)): ?>
- <label>
- <?= _('Raum') ?>
- <? if ($room_search): ?>
- <?= $room_search->render() ?>
- <? else: ?>
- <select name="room_id" style="width: calc(100% - 23px);">
- <option value=""><?= _('<em>Keinen</em> Raum buchen') ?></option>
- <? foreach ($selectable_rooms as $room): ?>
- <option value="<?= htmlReady($room->id) ?>">
- <?= htmlReady($room->name) ?>
- <? if ($room->seats > 1) : ?>
- <?= sprintf(_('(%d Sitzplätze)'), $room->seats) ?>
- <? endif ?>
- </option>
- <? endforeach ?>
- </select>
- <? endif ?>
- </label>
- <? endif ?>
-
- <label for="freeRoomText">
- <?= _('Freie Ortsangabe') ?>
- <input value="<?= htmlReady(Request::get('freeRoomText')) ?>" id="freeRoomText"
- name="freeRoomText" type="text" maxlength="255">
- <? if (Config::get()->RESOURCES_ENABLE) : ?>
- <small style="display: block"><?= _('(führt <em>nicht</em> zu einer Raumbuchung)') ?></small>
- <? endif ?>
- </label>
-
- <? if (count($teachers)) : ?>
- <label for="related_teachers"><?= _('Durchführende Lehrende') ?>
- <? if (count($teachers) > 1) : ?>
- <select id="related_teachers" name="related_teachers[]" multiple class="multiple">
- <? foreach ($teachers as $dozent) : ?>
- <option <?= in_array($dozent['user_id'], Request::getArray('related_teachers')) ? 'selected' : '' ?>
- value="<?= $dozent['user_id'] ?>"><?= htmlReady($dozent->user->getFullName()) ?></option>
- <? endforeach ?>
- </select>
- <? else : ?>
- <p style="margin-left: 15px">
- <? $dozent = array_pop($teachers) ?>
- <?= htmlReady($dozent->getUserFullname()) ?>
- </p>
- <? endif ?>
- </label>
- <? endif ?>
-
-
- <? if (count($groups) > 0) : ?>
- <label for="related_statusgruppen"><?= _('Beteiligte Gruppen') ?>
- <select id="related_statusgruppen" name="related_statusgruppen[]" multiple class="multiple">
- <? foreach ($groups as $group) : ?>
- <option <?= in_array($group->getId(), Request::getArray('related_statusgruppen')) ? 'selected' : '' ?>
- value="<?= $group->getId() ?>"><?= htmlReady($group['name']) ?></option>
- <? endforeach ?>
- </select>
- </label>
- <? endif ?>
- </fieldset>
-
- <footer data-dialog-button>
- <?= Studip\Button::createAccept(_('Speichern'), 'save', ['data-dialog' => 'size=600']) ?>
- <? if (Request::get('fromDialog')) : ?>
- <?= Studip\LinkButton::create(
- _('Zurück zur Übersicht'),
- $controller->url_for('course/timesrooms/index'),
- ['data-dialog' => 'size=big']
- ) ?>
- <? endif ?>
- </footer>
-</form>
+<?php +/** + * @var Course_TimesroomsController $controller + * @var array $date_types + * @var array $available_lecturers + * @var array $available_groups + * @var bool $allow_multiple_room_bookings + */ +?> +<form class="default" method="post" + action="<?= $controller->link_for('course/timesrooms/saveDate') ?>"> + <?= CSRFProtection::tokenTag() ?> + <?= Studip\VueApp::create('CourseDateFormContent') + ->withProps([ + 'course_date' => null, + 'preparation_time' => 0, + 'subsequent_time' => 0, + 'date_types' => $date_types ?? [], + 'room_management_enabled' => Config::get()->RESOURCES_ENABLE, + 'available_lecturers' => $available_lecturers ?? [], + 'available_groups' => $available_groups ?? [], + 'allow_multiple_room_bookings' => $allow_multiple_room_bookings + ]) ?> + <footer data-dialog-button> + <?= \Studip\Button::createAccept(_('Speichern'), 'save') ?> + <? if (Request::bool('fromDialog')) : ?> + <?= Studip\LinkButton::create( + _('Zurück zur Übersicht'), + $controller->url_for( + 'course/timesrooms', + ['fromDialog' => 1] + ), + ['data-dialog' => 'size=big']) ?> + <? endif ?> + <?= \Studip\Button::createCancel(_('Abbrechen'), 'abort') ?> + </footer> +</form> diff --git a/app/views/course/timesrooms/editDate.php b/app/views/course/timesrooms/editDate.php index f17c52a..30ab615 100644 --- a/app/views/course/timesrooms/editDate.php +++ b/app/views/course/timesrooms/editDate.php @@ -2,256 +2,46 @@ /** * @var Course_TimesroomsController $controller * @var CourseDate $date - * @var Room[] $selectable_rooms - * @var QuickSearch|null $room_search - * @var bool $only_bookable_rooms * @var int $preparation_time + * @var int $subsequent_time * @var int $max_preparation_time - * @var CourseMember[] $teachers - * @var User[] $assigned_teachers - * @var Statusgruppen[] $groups - * @var Statusgruppen[] $assigned_groups + * @var array $selected_room_ids + * @var array $available_lecturers + * @var array $available_groups + * @var array $assigned_lecturers + * @var array $assigned_groups + * @var bool $allow_multiple_room_bookings */ ?> -<form action="<?= $controller->link_for('course/timesrooms/saveDate/' . $date->termin_id) ?>" - method="post" class="default collapsable" <?= Request::int('fromDialog') ? 'data-dialog="size=big"' : '' ?>> +<form class="default" method="post" + action="<?= $controller->link_for('course/timesrooms/saveDate/' . $date->termin_id) ?>"> <?= CSRFProtection::tokenTag() ?> - <fieldset style="margin-top: 1ex"> - <legend><?= _('Zeitangaben') ?></legend> - <label id="course_type" class=col-6> - <?= _('Art') ?> - <select name="course_type" id="course_type" class="size-s"> - <? foreach ($GLOBALS['TERMIN_TYP'] as $id => $value) : ?> - <option value="<?= $id ?>" - <?= $date->date_typ == $id ? 'selected' : '' ?>> - <?= htmlReady($value['name']) ?> - </option> - <? endforeach ?> - </select> - </label> - <label class="col-2"> - <?= _('Datum') ?> - <input class="has-date-picker size-s" type="text" name="date" required - value="<?= $date->date ? strftime('%d.%m.%Y', $date->date) : '' ?>"> - </label> - <label class="col-2"> - <?= _('Startzeit') ?> - <input class="studip-timepicker size-s" type="text" name="start_time" required placeholder="HH:mm" - value="<?= $date->date ? strftime('%H:%M', $date->date) : '' ?>"> - </label> - <label class="col-2"> - <?= _('Endzeit') ?> - <input class="studip-timepicker size-s" type="text" name="end_time" required placeholder="HH:mm" - value="<?= $date->end_time ? strftime('%H:%M', $date->end_time) : '' ?>"> - </label> - </fieldset> - <fieldset> - <legend><?= _('Raumangaben') ?></legend> - <? if (Config::get()->RESOURCES_ENABLE - && ($selectable_rooms || $room_search)): ?> - <label> - <input style="display: inline;" type="radio" name="room" value="room" - id="room" <? if ($date->room_booking) echo 'checked'; ?> - data-activates="input.preparation-time[name='preparation_time'],input.preparation-time[name='subsequent_time']"> - <?= _('Raum direkt buchen') ?> - <span class="flex-row"> - <? if ($room_search && !$only_bookable_rooms): ?> - <?= $room_search - ->setAttributes(['onFocus' => "jQuery('input[type=radio][name=room][value=room]').prop('checked', 'checked')"]) - ->setMinLength(2) - ->render() ?> - <? else: ?> - <? $selected_room_id = $date->room_booking->resource_id ?? ''; ?> - <select name="room_id" onFocus="jQuery('input[type=radio][name=room][value=room]').prop('checked', 'checked')"> - <? foreach ($selectable_rooms as $room): ?> - <option value="<?= htmlReady($room->id) ?>" - <?= $selected_room_id == $room->id - ? 'selected="selected"' - : '' ?>> - <?= htmlReady($room->name) ?> - <? if ($room->seats > 1) : ?> - <?= sprintf(_('(%d Sitzplätze)'), $room->seats) ?> - <? endif ?> - </option> - <? endforeach ?> - </select> - <? endif ?> - <? if (!$only_bookable_rooms) : ?> - <a href="<?= $controller->url_for( - 'course/timesrooms/editDate/' . $date->termin_id, - ['only_bookable_rooms' => '1'] - ) ?>" <?= Request::isDialog() ? 'data-dialog="size=normal"' : '' ?> - title="<?= _('Nur buchbare Räume anzeigen') ?>"> - <?= Icon::create('room-request')->asSvg([ - 'class' => 'text-bottom', - 'style' => 'margin-left: 0.2em; margin-top: 0.6em;', - ]) ?> - </a> - <? endif ?> - </span> - </label> - <section class="indented"> - <label class="col-2"> - <?= _('Rüstzeit vor dem Termin (in Minuten)') ?> - <input type="number" name="preparation_time" - class="preparation-time" - value="<?= htmlReady($preparation_time) ?>" - min="0" max="<?= htmlReady($max_preparation_time) ?>"> - </label> - <label class="col-2"> - <?= _('Rüstzeit nach dem Termin (in Minuten)') ?> - <input type="number" name="subsequent_time" - class="preparation-time" - value="<?= htmlReady($subsequent_time) ?>" - min="0" max="<?= htmlReady($max_preparation_time) ?>"> - </label> - </section> - <? endif ?> - <label class="horizontal"> - <input type="radio" name="room" value="freetext" <?= $date->raum ? 'checked' : '' ?> - data-deactivates="input.preparation-time[name='preparation_time'],input.preparation-time[name='subsequent_time']"> - <?= _('Freie Ortsangabe (keine Raumbuchung)') ?> - <input type="text" - name="freeRoomText_sd" - placeholder="<?= _('Freie Ortsangabe (keine Raumbuchung)') ?>" - value="<?= $date->raum ? htmlReady($date->raum) : '' ?>"> - </label> - - <label> - <input type="radio" name="room" value="noroom" - <?= (!empty($date->room_booking->resource_id) || !empty($date->raum) ? '' : 'checked') ?> - data-deactivates="input.preparation-time[name='preparation_time'],input.preparation-time[name='subsequent_time']"> - <span style="display: inline-block;"><?= _('Kein Raum') ?></span> - </label> - <label> - <input type="radio" name="room" value="nochange" checked="checked" - data-deactivates="input.preparation-time[name='preparation_time'],input.preparation-time[name='subsequent_time']"> - <?= _('Keine Änderungen an den Raumangaben vornehmen') ?> - <? if ($date->room_booking) :?> - <?=sprintf(_('(gebucht: %s)'), htmlReady($date->room_booking->room_name))?> - <? endif ?> - </label> - - </fieldset> - -<? if (count($teachers) > 1): ?> - <fieldset class="collapsed"> - <legend><?= _('Durchführende Lehrende') ?></legend> - - <div class="studip-selection" data-attribute-name="assigned_teachers"> - <section class="studip-selection-selected"> - <p><strong><?= _('Zugewiesene Lehrende') ?></strong></p> - <ul> - <? foreach ($assigned_teachers as $teacher): ?> - <li data-selection-id="<?= htmlReady($teacher->user_id) ?>"> - <input type="hidden" name="assigned_teachers[]" - value="<?= htmlReady($teacher->user_id) ?>"> - - <span class="studip-selection-image"> - <?= Avatar::getAvatar($teacher->user_id)->getImageTag(Avatar::SMALL) ?> - </span> - <span class="studip-selection-label"> - <?= htmlReady($teacher->getFullName()) ?> - </span> - </li> - <? endforeach ?> - <li class="empty-placeholder"> - <?= _('Kein spezieller Lehrender zugewiesen') ?> - </li> - </ul> - </section> - - <section class="studip-selection-selectable"> - <p><strong><?= _('Lehrende der Veranstaltung') ?></strong></p> - <ul> - <? foreach ($teachers as $teacher): ?> - <? if (!$assigned_teachers->find($teacher->user_id)): ?> - <li data-selection-id="<?= htmlReady($teacher->user_id) ?>" > - <span class="studip-selection-image"> - <?= Avatar::getAvatar($teacher->user_id)->getImageTag(Avatar::SMALL) ?> - </span> - <span class="studip-selection-label"> - <?= htmlReady($teacher->getUserFullname()) ?> - </span> - </li> - <? endif ?> - <? endforeach ?> - <li class="empty-placeholder"> - <?= sprintf( - _('Ihre Auswahl entspricht dem Zustand "%s" und wird beim Speichern zurückgesetzt'), - _('Kein spezieller Lehrender zugewiesen') - ) ?> - </li> - </ul> - </section> - </div> - </fieldset> -<? endif ?> - -<? if (count($groups) > 0): ?> - <fieldset class="collapsed"> - <legend><?= _('Beteiligte Gruppen') ?></legend> - - <div class="studip-selection" data-attribute-name="assigned-groups"> - <section class="studip-selection-selected"> - <p><strong><?= _('Zugewiesene Gruppen') ?></strong></p> - <ul> - <? foreach ($assigned_groups as $group) : ?> - <li data-selection-id="<?= htmlReady($group->id) ?>"> - <input type="hidden" name="assigned-groups[]" - value="<?= htmlReady($group->id) ?>"> - - <span class="studip-selection-label"> - <?= htmlReady($group->name) ?> - </span> - </li> - <? endforeach ?> - <li class="empty-placeholder"> - <?= _('Keine spezielle Gruppe zugewiesen') ?> - </li> - </ul> - </section> - - <section class="studip-selection-selectable"> - <p><strong><?= _('Gruppen der Veranstaltung') ?></strong></p> - <ul> - <? foreach ($groups as $group): ?> - <? if (!$assigned_groups->find($group->id)): ?> - <li data-selection-id="<?= htmlReady($group->id) ?>" > - <span class="studip-selection-label"> - <?= htmlReady($group->name) ?> - </span> - </li> - <? endif ?> - <? endforeach ?> - <li class="empty-placeholder"> - <?= _('Alle Gruppen wurden dem Termin zugewiesen') ?> - </li> - </ul> - </section> - </div> - </fieldset> -<? endif ?> -<? if (Config::get()->ENABLE_NUMBER_OF_PARTICIPANTS) : ?> - <fieldset> - <legend><?= _('Teilnehmende') ?></legend> - <label> - <?=_('Anzahl der Teilnehmenden')?> - <input type="number" min="0" name="number_of_participants" value="<?= htmlReady($date->number_of_participants) ?>"> - </label> - </fieldset> -<? endif ?> - + <?= Studip\VueApp::create('CourseDateFormContent') + ->withProps([ + 'course_date' => $date->toRawArray(['termin_id', 'date', 'end_time', 'date_typ', 'raum', 'number_of_participants', 'content']), + 'initial_preparation_time' => $preparation_time, + 'initial_subsequent_time' => $subsequent_time, + 'max_preparation_time' => $max_preparation_time, + 'date_types' => $date_types ?? [], + 'room_management_enabled' => Config::get()->RESOURCES_ENABLE, + 'selected_rooms' => $selected_room_ids ?? [], + 'available_lecturers' => $available_lecturers ?? [], + 'available_groups' => $available_groups ?? [], + 'selected_lecturers' => $assigned_lecturers, + 'selected_groups' => $assigned_groups, + 'allow_multiple_room_bookings' => $allow_multiple_room_bookings + ]) ?> <footer data-dialog-button> - <?= Studip\Button::createAccept(_('Speichern'), 'save_dates') ?> - <? if (Request::int('fromDialog')) : ?> + <?= \Studip\Button::createAccept(_('Speichern'), 'save') ?> + <? if (Request::bool('fromDialog')) : ?> <?= Studip\LinkButton::create( _('Zurück zur Übersicht'), $controller->url_for( 'course/timesrooms', ['fromDialog' => 1, 'contentbox_open' => $date->metadate_id] ), - ['data-dialog' => 'size=big']) ?> + ['data-dialog' => 'size=big']) ?> <? endif ?> + <?= \Studip\Button::createCancel(_('Abbrechen'), 'abort') ?> </footer> </form> diff --git a/app/views/course/timesrooms/editStack.php b/app/views/course/timesrooms/editStack.php index 11695fc..9d072bd 100644 --- a/app/views/course/timesrooms/editStack.php +++ b/app/views/course/timesrooms/editStack.php @@ -4,14 +4,16 @@ * @var string $cycle_id * @var array $linkAttributes * @var array $checked_dates - * @var array $selectable_rooms * @var QuickSearch $room_search - * @var array $only_bookable_rooms * @var array $teachers * @var array $gruppen + * @var array $time_ranges + * @var bool $allow_multiple_room_bookings * @var int $preparation_time + * @var int $subsequent_time * @var int $max_preparation_time * @var string[] $selected_lecturer_ids + * @var string[] $selected_room_ids */ ?> <form method="post" action="<?= $controller->link_for('course/timesrooms/saveStack/' . $cycle_id, $linkAttributes ?? []) ?>" @@ -20,80 +22,19 @@ <input type="hidden" name="method" value="edit"> <input type="hidden" name="checked_dates" value="<?= implode(',', $checked_dates) ?>"> - <fieldset> - <legend><?= _('Raumangaben') ?></legend> - <? if (Config::get()->RESOURCES_ENABLE && (!empty($room_search) || !empty($selectable_rooms))): ?> - <section> - <label> - <input type="radio" name="action" value="room" id="room" data-activates="input.preparation-time[name='preparation_time']"> - <?= _('Raum direkt buchen') ?> - </label> - <? if (!empty($room_search)) : ?> - <label> - <?= _('Raumsuche') ?> - <span class="flex-row"></span> - <?= $room_search - ->setAttributes(['onFocus' => "jQuery('input[type=radio][name=action][value=room]').prop('checked', true)"]) - ->setMinLength(2) - ->render() ?> - <? if (!$only_bookable_rooms) : ?> - <?= $this->render_partial('course/timesrooms/_bookable_rooms_icon.php') ?> - <? endif ?> - </label> - <? else : ?> - <label> - <?= _('Raum auswählen') ?> - <span class="flex-row"> - <select name="room_id" onFocus="jQuery('input[type=radio][name=action][value=room]').prop('checked', 'checked')"> - <option value="0"><?= _('Auswählen') ?></option> - <? foreach ($selectable_rooms as $room): ?> - <option value="<?= htmlReady($room->id)?>" - <?= $room->id === $selected_room_id ? 'selected' : '' ?>> - <?= htmlReady($room->name) ?> - </option> - <? endforeach ?> - </select> - <? if (!$only_bookable_rooms) : ?> - <?= $this->render_partial('course/timesrooms/_bookable_rooms_icon.php') ?> - <? endif ?> - </span> - </label> - <? endif ?> - <label> - <?= _('Rüstzeit (in Minuten)') ?> - <input type="number" name="preparation_time" - class="preparation-time" - value="<?= htmlReady($preparation_time) ?>" - min="0" max="<?= htmlReady($max_preparation_time) ?>"> - </label> - </section> - <? $placerholder = _('Freie Ortsangabe (keine Raumbuchung)') ?> - <? else : ?> - <? $placerholder = _('Freie Ortsangabe') ?> - <? endif ?> - <section> - <label> - <input type="radio" name="action" value="freetext" data-deactivates="input.preparation-time[name='preparation_time']"> - <?= $placerholder ?> - </label> - <label> - <input type="text" name="freeRoomText" value="<?= htmlReady($selected_room_name) ?>" - placeholder="<?= $placerholder ?>" - onFocus="jQuery('input[type=radio][name=action][value=freetext]').prop('checked', 'checked')"> - </label> - </section> - <? if (Config::get()->RESOURCES_ENABLE) : ?> - <label> - <input type="radio" name="action" value="noroom" data-deactivates="input.preparation-time[name='preparation_time']"> - <?= _('Kein Raum') ?> - </label> - <? endif ?> - - <label> - <input type="radio" name="action" value="nochange" checked="checked" data-deactivates="input.preparation-time[name='preparation_time']"> - <?= _('Keine Änderungen an den Raumangaben vornehmen') ?> - </label> - </fieldset> + <section id="room-fieldset"> + <course-date-room-fieldset + :time_ranges="<?= htmlReady(json_encode($time_ranges)) ?>" + :course_date_ids="<?= htmlReady(json_encode($checked_dates)) ?>" + :room_management_enabled="<?= Config::get()->RESOURCES_ENABLE ? 'true' : 'false' ?>" + :allow_multiple_room_bookings="<?= $allow_multiple_room_bookings ? 'true' : 'false' ?>" + :initial_preparation_time="<?= $preparation_time ?>" + :initial_subsequent_time="<?= $subsequent_time ?>" + :max_preparation_time="<?= $max_preparation_time ?>" + :selected_rooms="<?= htmlReady(json_encode($selected_room_ids ?? [])) ?>" + :show_nochange_option="true" + ></course-date-room-fieldset> + </section> <fieldset class="collapsed"> <legend><?= _('Terminangaben') ?></legend> @@ -122,7 +63,7 @@ <label> <?= _('Aktion auswählen') ?> <select name="related_persons_action" id="related_persons_action"> - <option value=""><?= _('Bitte wählen') ?></option> + <option value=""><?= _('-- Keine Änderung --') ?></option> <option value="add"><?= _('Lehrende hinzufügen') ?></option> <option value="delete"><?= _('Lehrende entfernen') ?></option> </select> @@ -148,7 +89,7 @@ <label> <?= _('Aktion auswählen') ?> <select name="related_groups_action" id="related_groups_action"> - <option value=""><?= _('Bitte wählen') ?></option> + <option value=""><?= _('-- Keine Änderung --') ?></option> <option value="add"><?= _('Gruppen hinzufügen') ?></option> <option value="delete"><?= _('Gruppen entfernen') ?></option> </select> @@ -177,3 +118,10 @@ <? endif ?> </footer> </form> +<script> + STUDIP.Vue.load().then(({createApp}) => { + STUDIP.editStackRoomFieldset = createApp({ + el: "#room-fieldset" + }); + }); +</script> diff --git a/app/views/resources/admin/edit_separable_room.php b/app/views/resources/admin/edit_separable_room.php new file mode 100644 index 0000000..b6b5d31 --- /dev/null +++ b/app/views/resources/admin/edit_separable_room.php @@ -0,0 +1,25 @@ +<?php +/** + * @var Resources_AdminController $controller + * @var ?SeparableRoom $separable_room + */ +?> +<form class="default" method="post" data-dialog="reload-on-close" + action="<?= $controller->link_for('resources/admin/save_separable_room/'. ($separable_room->id ?? '')) ?>"> + <?= CSRFProtection::tokenTag() ?> + <fieldset> + <legend><?= _('Grunddaten') ?></legend> + <label> + <?= _('Name') ?> + <input type="text" name="name" value="<?= htmlReady($separable_room->name) ?>" maxlength="256"> + </label> + <label> + <?= _('Beschreibung') ?> + <textarea name="description"><?= htmlReady($separable_room->description) ?></textarea> + </label> + </fieldset> + <div data-dialog-button> + <?= \Studip\Button::createAccept(_('Speichen'), 'save') ?> + <?= \Studip\Button::createCancel(_('Abbrechen')) ?> + </div> +</form> diff --git a/app/views/resources/admin/separable_rooms.php b/app/views/resources/admin/separable_rooms.php index ec87f40..5bd7ed5 100644 --- a/app/views/resources/admin/separable_rooms.php +++ b/app/views/resources/admin/separable_rooms.php @@ -68,6 +68,10 @@ <td><?= htmlReady($separable_room->name) ?></td> <td></td> <td class="actions"> + <a href="<?= $controller->link_for('resources/admin/edit_separable_room/' . $separable_room->id) ?>" + data-dialog="size=auto"> + <?= Icon::create('edit')->asImg(['class' => 'text-bottom', 'aria-label' => _('Bearbeiten')]) ?> + </a> <?= Icon::create('trash')->asInput( [ 'name' => 'delete_separable_room[' diff --git a/app/views/resources/room_request/resolve.php b/app/views/resources/room_request/resolve.php index a6c9ec6..8156dc2 100644 --- a/app/views/resources/room_request/resolve.php +++ b/app/views/resources/room_request/resolve.php @@ -353,7 +353,7 @@ <input type="radio" name="<?= htmlReady($room_radio_name) ?>" class="radio-null text-bottom" value="" - <?= empty($selected_rooms[$range_index]) && empty($interval['booked_room']) + <?= empty($selected_rooms[$range_index]) && empty($interval['booked_rooms']) ? 'checked="checked"' : '' ?>> </td> diff --git a/app/views/resources/room_request/resolve_room_tr.php b/app/views/resources/room_request/resolve_room_tr.php index a7f003a..b0aa178 100644 --- a/app/views/resources/room_request/resolve_room_tr.php +++ b/app/views/resources/room_request/resolve_room_tr.php @@ -90,7 +90,7 @@ )) ?> <? endif ?> <? $stats = 0; array_walk($data['intervals'], function($item, $key, $room_id) use (&$stats) { - if ($item['booked_room'] === $room_id) { + if (in_array($room_id, $item['booked_rooms'])) { $stats++; } }, $room->id) ?> @@ -111,12 +111,12 @@ $room_radio_name = 'selected_rooms[' . $range_index . ']'; ?> <td> - <? if ($available || (!empty($interval['booked_room']) && $interval['booked_room'] == $room->id)): ?> + <? if ($available || (!empty($interval['booked_rooms']) && in_array($room->id, $interval['booked_rooms']))): ?> <input type="radio" name="<?= htmlReady($room_radio_name) ?>" class="text-bottom radio-<?= htmlReady($room->id) ?>" value="<?= htmlReady($room->id) ?>" <?= (!empty($selected_rooms[$range_index]) && $selected_rooms[$range_index] === $room->id - || (!empty($interval['booked_room']) && $interval['booked_room'] === $room->id)) + || (!empty($interval['booked_rooms']) && in_array($room->id, $interval['booked_rooms']))) ? 'checked="checked"' : ''?>> <?= Icon::create('check-circle', Icon::ROLE_STATUS_GREEN)->asSvg(['class' => 'text-bottom']) ?> diff --git a/app/views/room_management/overview/rooms.php b/app/views/room_management/overview/rooms.php index cdec83e..08b5d0e 100644 --- a/app/views/room_management/overview/rooms.php +++ b/app/views/room_management/overview/rooms.php @@ -16,10 +16,21 @@ </thead> <tbody> <? foreach ($rooms as $room): ?> + <? + $room_tooltip = null; + $separable_room = SeparableRoom::findByRoomPart($room); + if ($separable_room) { + $room_tooltip = studip_interpolate( + _('Dieser Raum gehört zum teilbaren Raum %{room_name}.'), + ['room_name' => $separable_room->name] + ); + } + ?> <?= $this->render_partial( 'resources/_common/_room_tr.php', [ 'room' => $room, + 'room_tooltip' => $room_tooltip, 'show_global_admin_actions' => $show_global_admin_actions, 'show_admin_actions' => $room->userHasPermission( $user, diff --git a/db/migrations/6.2.6_multiple_rooms_per_course_date.php b/db/migrations/6.2.6_multiple_rooms_per_course_date.php new file mode 100644 index 0000000..9187c78 --- /dev/null +++ b/db/migrations/6.2.6_multiple_rooms_per_course_date.php @@ -0,0 +1,66 @@ +<?php + + +class MultipleRoomsPerCourseDate extends Migration +{ + public function description() + { + return 'Changes the database schema to allow booking multiple rooms for a course date.'; + } + + protected function up() + { + $db = DBManager::get(); + + $db->exec( + "INSERT INTO `config` + (`field`, `value`, `type`, `range`, `section`, `mkdate`, `chdate`, `description`) + VALUES + ( + 'ROOM_PERMISSIONS_FOR_MULTIPLE_BOOKINGS_PER_COURSE_DATE', + 'admin', + 'string', + 'global', + 'resources', + UNIX_TIMESTAMP(), + UNIX_TIMESTAMP(), + 'Ab welcher Raumverwaltungs-Rechtestufe soll es möglich sein, mehrere Räume für einen Veranstaltungstermin zu buchen?' + )" + ); + + $db->exec( + "CREATE TABLE IF NOT EXISTS ex_termin_rooms + ( + ex_termin_id CHAR(32) NOT NULL, + room_id CHAR(32) NOT NULL, + mkdate INT(10) UNSIGNED NOT NULL DEFAULT '0', + chdate INT(10) UNSIGNED NOT NULL DEFAULT '0', + PRIMARY KEY (ex_termin_id, room_id) + )" + ); + + $db->exec( + "INSERT INTO `ex_termin_rooms` (`ex_termin_id`, `room_id`, `mkdate`, `chdate`) + SELECT `termin_id` AS ex_termin_id, `resource_id` AS room_id, `chdate` AS mkdate, `chdate` AS chdate + FROM `ex_termine` + WHERE `resource_id` <> ''" + ); + + $db->exec("ALTER TABLE `ex_termine` DROP COLUMN `resource_id`"); + + $db->exec("ALTER TABLE `separable_rooms` ADD COLUMN `description` TEXT NOT NULL"); + } + + protected function down() + { + $db = DBManager::get(); + + $db->exec("ALTER TABLE `separable_rooms` DROP COLUMN `description`"); + $db->exec("ALTER TABLE `ex_termine` ADD COLUMN `resource_id` CHAR(32) NOT NULL"); + $db->exec("DROP TABLE IF EXISTS ex_termin_rooms"); + $db->exec( + "DELETE FROM `config` + WHERE `field` = 'ROOM_PERMISSIONS_FOR_MULTIPLE_BOOKINGS_PER_COURSE_DATE'" + ); + } +} diff --git a/lib/classes/CourseDateList.php b/lib/classes/CourseDateList.php index 8a6be35..5aa1a02 100644 --- a/lib/classes/CourseDateList.php +++ b/lib/classes/CourseDateList.php @@ -204,12 +204,12 @@ class CourseDateList implements Stringable } } foreach ($this->single_dates as $date) { - $room_name = $date->getRoomName(); - if ($room_name) { - if (!array_key_exists($room_name, $grouped_dates)) { - $grouped_dates[$room_name] = new CourseDateList(); + $room_names = $date->getRoomNames(); + if ($room_names) { + if (!array_key_exists($room_names, $grouped_dates)) { + $grouped_dates[$room_names] = new CourseDateList(); } - $grouped_dates[$room_name]->addSingleDate($date); + $grouped_dates[$room_names]->addSingleDate($date); } else { if (!array_key_exists(_('Ohne Raum'), $grouped_dates)) { $grouped_dates[_('Ohne Raum')] = new CourseDateList(); @@ -269,16 +269,16 @@ class CourseDateList implements Stringable foreach ($this->single_dates as $single_date) { $date_line = $single_date->getFullName($with_room_names ? 'long-include-room' : 'long'); if ($group_by_rooms) { - $room_name = _('Kein Raum'); - if ($single_date->room_booking) { - $room_name = $single_date->room_booking->room_name; + $room_names = _('Kein Raum'); + if ($single_date->room_bookingn) { + $room_names = $single_date->getRoomNames(); } elseif ($single_date->raum) { - $room_name = $single_date->raum; + $room_names = $single_date->raum; } - if (!isset($output[$room_name])) { - $output[$room_name] = []; + if (!isset($output[$room_names])) { + $output[$room_names] = []; } - $output[$room_name][] = $date_line; + $output[$room_names][] = $date_line; } else { $output[] = $date_line; } diff --git a/lib/classes/InstituteCalendarHelper.php b/lib/classes/InstituteCalendarHelper.php index 13677ea..54393fa 100644 --- a/lib/classes/InstituteCalendarHelper.php +++ b/lib/classes/InstituteCalendarHelper.php @@ -438,7 +438,7 @@ class InstituteCalendarHelper $next_single_date = CourseDate::getNextDateByMetadate($cycle_date->metadate_id); if ($next_single_date) { - $room_name = $next_single_date->getRoomName() ?: _('ohne Raumangabe'); + $room_name = $next_single_date->getRoomNames() ?: _('ohne Raumangabe'); } $fields = [ @@ -618,9 +618,11 @@ class InstituteCalendarHelper $rooms = []; foreach ($cycle_date->getAllDates() as $course_date) { - $room = $course_date->getRoom(); - if ($room) { - $rooms[$room->id] = $room->name; + $course_date_rooms = $course_date->getRooms(); + if ($course_date_rooms) { + foreach ($course_date_rooms as $room) { + $rooms[$room->id] = $room->name; + } } } if ($rooms) { diff --git a/lib/classes/JsonApi/RouteMap.php b/lib/classes/JsonApi/RouteMap.php index f3e97c0..bd5a58a 100644 --- a/lib/classes/JsonApi/RouteMap.php +++ b/lib/classes/JsonApi/RouteMap.php @@ -9,6 +9,7 @@ use JsonApi\Routes\Consultations\SlotCreationCount; use JsonApi\Routes\Holidays\HolidaysShow; use JsonApi\Routes\Vacations\VacationsShow; use Slim\Routing\RouteCollectorProxy; +use Studip\JsonApi\Routes\AvailableRooms; /** * Diese Klasse ist die JSON-API-Routemap, in der alle Routen @@ -116,6 +117,8 @@ class RouteMap $group->get('/status-groups/{id}', Routes\StatusgroupShow::class); + $group->get('/available-rooms', AvailableRooms::class); + $this->addAuthenticatedAdmissionRoutes($group); $this->addAuthenticatedBlubberRoutes($group); $this->addAuthenticatedClipboardRoutes($group); diff --git a/lib/classes/JsonApi/Routes/AvailableRooms.php b/lib/classes/JsonApi/Routes/AvailableRooms.php new file mode 100644 index 0000000..4600db3 --- /dev/null +++ b/lib/classes/JsonApi/Routes/AvailableRooms.php @@ -0,0 +1,201 @@ +<?php + +namespace Studip\JsonApi\Routes; + +use JsonApi\NonJsonApiController; +use Psr\Http\Message\ServerRequestInterface as Request; +use Psr\Http\Message\ResponseInterface as Response; + +class AvailableRooms extends NonJsonApiController +{ + public function __invoke(Request $request, Response $response, $args) + { + $raw_time_ranges = \Request::get('time_ranges'); + if ($raw_time_ranges) { + $raw_time_ranges = json_decode($raw_time_ranges, true); + } else { + $raw_time_ranges = []; + } + $course_date_ids = \Request::get('course_date_ids'); + if ($course_date_ids) { + $course_date_ids = explode(',', $course_date_ids); + } + $current_user = \User::findCurrent(); + if (empty($raw_time_ranges)) { + //No time ranges given. + return $response->withStatus(400, 'No time ranges given.'); + } + //Convert the time ranges to the appropriate format: + $time_ranges = []; + foreach ($raw_time_ranges as $raw_time_range) { + $start_str = $raw_time_range['start'] ?? ''; + $end_str = $raw_time_range['end'] ?? ''; + if (!$start_str || !$end_str) { + //Invalid time range. + return $response->withStatus(400, 'Invalid time range.'); + } + //The timestamps are either in the extended RFC3339 format or in unix timestamps. + $timezone = new \DateTime(); + $start_datetime = \DateTime::createFromFormat(\DateTimeInterface::RFC3339_EXTENDED, $start_str, $timezone->getTimezone()); + $end_datetime = \DateTime::createFromFormat(\DateTimeInterface::RFC3339_EXTENDED, $end_str, $timezone->getTimezone()); + if (!$start_datetime || !$end_datetime) { + //Try unix timestamps as fallback. + $start_datetime = \DateTime::createFromFormat('U', $start_str, $timezone->getTimezone()); + $end_datetime = \DateTime::createFromFormat('U', $end_str, $timezone->getTimezone()); + } + if (!$start_datetime || !$end_datetime) { + //Invalid date or time format. + return $response->withStatus(400, 'Invalid date or time format.'); + } + $time_ranges[] = [ + 'begin' => $start_datetime, + 'end' => $end_datetime + ]; + } + + if (!\ResourceManager::userHasGlobalPermission($current_user, 'autor') + && !\ResourceManager::userHasResourcePermissions($current_user, 'autor')) { + //The user must not book any room. + throw new \AccessDeniedException(); + } + + //Collect the booking-IDs for the course date: + $booking_ids = []; + if (!empty($course_date_ids)) { + $course_dates = \CourseDate::findMany($course_date_ids); + if ($course_dates) { + foreach ($course_dates as $course_date) { + foreach ($course_date->room_bookings as $booking) { + $booking_ids[] = $booking->id; + } + } + } + } + + $available_rooms = \RoomManager::findRooms( + '', + null, + null, + [], + $time_ranges, + 'name ASC', + false, + [], + true, + $booking_ids + ); + + $bookable_rooms = []; + foreach ($available_rooms as $room) { + $all_ranges_bookable = true; + foreach ($time_ranges as $time_range) { + if (empty($time_range['begin']) || empty($time_range['end'])) { + //Invalid time range. We cannot continue. + return $response->withStatus(400, 'Invalid time range.'); + } + if (!$room->userHasBookingRights($current_user, $time_range['begin'], $time_range['end'])) { + $all_ranges_bookable = false; + break; + } + } + if ($all_ranges_bookable) { + $bookable_rooms[$room->id] = $room; + } + } + + $separable_rooms = \SeparableRoom::findBySQL( + "JOIN `separable_room_parts` srp + ON `separable_rooms`.`id` = `srp`.`separable_room_id` + WHERE `srp`.`room_id` IN ( :room_ids ) + GROUP BY `separable_rooms`.`id`", + [ + 'room_ids' => array_keys($bookable_rooms) + ] + ); + + $selectable_room_data = []; + + foreach ($separable_rooms as $separable_room) { + //Check if all the room parts are available and bookable. If so, include the separable room + //in the $selectable_room_data array. + + $unavailable_parts = \SeparableRoomPart::countBySQL( + "`separable_room_id` = :separable_room_id + AND `room_id` NOT IN ( :room_ids )", + [ + 'separable_room_id' => $separable_room->id, + 'room_ids' => array_keys($bookable_rooms) + ] + ) > 0; + + if (!$unavailable_parts) { + //The separable room is fully available. Include it in the list of bookable rooms + //before its room parts. + $selectable_room_data[] = [ + 'id' => sprintf('separable_room-%s', $separable_room->id), + 'name' => 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) + ]; + } + + //Add the room parts from the $bookable_rooms array: + $room_part_data = []; + foreach ($separable_room->parts as $part) { + if (in_array($part->room_id, array_keys($bookable_rooms))) { + $room = $bookable_rooms[$part->room_id]; + + $room_part_data[] = [ + 'id' => sprintf('room-%s', $room->id), + 'name' => studip_interpolate( + _('%{room_name} (%{seats} Sitzplätze) [Teil von %{separable_room_name}]'), + [ + 'room_name' => $room->getFullName(), + 'seats' => $room->seats, + 'separable_room_name' => $separable_room->name + ] + ), + 'separable_room_id' => $separable_room->id, + 'info_text' => sprintf('%1$s: %2$s', $separable_room->name, $separable_room->description) + ]; + + unset($bookable_rooms[$part->room_id]); + } + } + + //Sort $room_part_data by name: + uasort($room_part_data, function ($a, $b) { + if ($a['name'] > $b['name']) { + return 1; + } elseif ($a['name'] < $b['name']) { + return -1; + } else { + return 0; + } + }); + + $selectable_room_data = array_merge($selectable_room_data, $room_part_data); + } + + //Add all remaining rooms: + foreach ($bookable_rooms as $room) { + $selectable_room_data[] = [ + 'id' => sprintf('room-%s', $room->id), + 'name' => studip_interpolate( + _('%{room_name} (%{seats} Sitzplätze)'), + [ + 'room_name' => $room->getFullName(), + 'seats' => $room->seats + ] + ) + ]; + } + + $response->getBody()->write(json_encode($selectable_room_data)); + return $response; + } +} diff --git a/lib/classes/JsonApi/SchemaMap.php b/lib/classes/JsonApi/SchemaMap.php index 7432abc..5923c48 100644 --- a/lib/classes/JsonApi/SchemaMap.php +++ b/lib/classes/JsonApi/SchemaMap.php @@ -3,6 +3,7 @@ namespace JsonApi; use JsonApi\Schemas\ShortUrl; +use JsonApi\Schemas\Room; /** * @SuppressWarnings(PHPMD.CouplingBetweenObjects) @@ -104,7 +105,6 @@ class SchemaMap \Courseware\Unit::class => Schemas\Courseware\Unit::class, \Courseware\UserDataField::class => Schemas\Courseware\UserDataField::class, \Courseware\UserProgress::class => Schemas\Courseware\UserProgress::class, - \ShortUrl::class => Schemas\ShortUrl::class, ]; } } diff --git a/lib/classes/calendar/ICalendarExport.php b/lib/classes/calendar/ICalendarExport.php index 21bb47f..a2dada9 100644 --- a/lib/classes/calendar/ICalendarExport.php +++ b/lib/classes/calendar/ICalendarExport.php @@ -147,7 +147,7 @@ class ICalendarExport return [ 'SUMMARY' => $summary, 'DESCRIPTION' => $description, - 'LOCATION' => $date->getRoomName(), + 'LOCATION' => $date->getRoomNames(), 'CATEGORIES' => $categories, 'LAST-MODIFIED' => $date->chdate, 'CREATED' => $date->mkdate, diff --git a/lib/dates.inc.php b/lib/dates.inc.php index 5ebd84d..93def75 100644 --- a/lib/dates.inc.php +++ b/lib/dates.inc.php @@ -200,18 +200,22 @@ function vorbesprechung(string $seminar_id, string $type = 'standard'): false|st $termin = new CourseDate($termin_id); $ret = (string) $termin; - if (!empty($termin->room_booking->resource)) { + if (!empty($termin->room_bookings)) { $ret .= ', '._("Ort:").' '; switch ($type) { case 'export': - $ret .= $termin->room_booking->resource->name; + $ret .= $termin->getRoomNames(); break; case 'standard': - default: - $ret .= '<a href="' . $termin->room_booking->resource->getActionLink('show') . '" data-dialog>' - . htmlReady($termin->room_booking->resource->name) . '</a>'; + default: { + $rooms = $termin->getRooms(); + foreach ($rooms as $room) { + $ret .= '<a href="' . $room->getActionLink('show') . '" data-dialog>' + . htmlReady($room->name) . '</a>'; + } break; + } } } return $ret; diff --git a/lib/extern/ExternPageTimetable.php b/lib/extern/ExternPageTimetable.php index 1e0e66a..6823001 100644 --- a/lib/extern/ExternPageTimetable.php +++ b/lib/extern/ExternPageTimetable.php @@ -259,7 +259,7 @@ class ExternPageTimetable extends ExternPage 'TYPE' => $GLOBALS['TERMIN_TYP'][$date->date_typ]['name'], 'LECTURERS' => $this->getLecturers($date), 'TOPICS' => $this->getTopics($date), - 'BOOKED_ROOM' => $this->getBookedRoomData($date) + 'BOOKED_ROOMS' => $this->getBookedRoomData($date) ]; return $date_content; } @@ -312,11 +312,18 @@ class ExternPageTimetable extends ExternPage private function getBookedRoomData(CourseDate $date): array { - if ($date->room_booking) { - return [ - 'NAME' => $date->room_booking->resource->name, - 'ID' => $date->room_booking->resource->id - ]; + if ($date->room_bookings) { + $booking_data = []; + foreach ($date->room_bookings as $booking) { + $booking_data[] = [ + 'NAME' => $booking->resource->name ?? '', + 'ID' => $booking->resource->id ?? '' + ]; + } + uasort($booking_data, function($a, $b) { + return strnatcmp($a['NAME'], $b['NAME']); + }); + return $booking_data; } return []; } diff --git a/lib/models/Course.php b/lib/models/Course.php index 831fe07..d35c6c3 100644 --- a/lib/models/Course.php +++ b/lib/models/Course.php @@ -1997,7 +1997,7 @@ class Course extends SimpleORMap implements Range, PrivacyObject, StudipItem, Fe } $dates->uasort(function($a, $b) { return $a->date - $b->date - ?: strnatcasecmp($a->getRoomName(), $b->getRoomName()); + ?: strnatcasecmp($a->getRoomNames(), $b->getRoomNames()); }); return $dates; } diff --git a/lib/models/CourseDate.php b/lib/models/CourseDate.php index 038a120..87a56e8 100644 --- a/lib/models/CourseDate.php +++ b/lib/models/CourseDate.php @@ -29,7 +29,7 @@ * @property User $author belongs_to User * @property Course $course belongs_to Course * @property SeminarCycleDate|null $cycle belongs_to SeminarCycleDate - * @property ResourceBooking $room_booking has_one ResourceBooking + * @property ResourceBooking[] $room_bookings has_many ResourceBooking * @property SimpleORMapCollection<CourseTopic> $topics has_and_belongs_to_many CourseTopic * @property SimpleORMapCollection<Statusgruppen> $statusgruppen has_and_belongs_to_many Statusgruppen * @property SimpleORMapCollection<User> $dozenten has_and_belongs_to_many User @@ -89,7 +89,7 @@ class CourseDate extends SimpleORMap implements PrivacyObject, Event 'class_name' => SeminarCycleDate::class, 'foreign_key' => 'metadate_id' ]; - $config['has_one']['room_booking'] = [ + $config['has_many']['room_bookings'] = [ 'class_name' => ResourceBooking::class, 'foreign_key' => 'termin_id', 'assoc_foreign_key' => 'range_id', @@ -225,25 +225,49 @@ class CourseDate extends SimpleORMap implements PrivacyObject, Event * * @return String containing the room name */ - public function getRoomName() + public function getRoomNames() { - if (Config::get()->RESOURCES_ENABLE && isset($this->room_booking->resource)) { - return $this->getRoom()->name; + if (Config::get()->RESOURCES_ENABLE) { + $room_names = []; + foreach ($this->getRooms() as $room) { + $room_names[] = $room->getFullName(); + } + if (!empty($room_names)) { + return implode(', ', $room_names); + } } + //The room management is disabled or there are no room bookings for this course date. + //Maybe a freetext room name exists. return $this->raum ?? ''; } /** - * Returns the assigned room for this date as an object. + * Returns the assigned rooms for this date as objects. * - * @return Resource Either the object or null if no room is assigned + * @return Room[] Either the room objects or an empty array if no rooms are assigned. */ - public function getRoom() - { - if (Config::get()->RESOURCES_ENABLE && !empty($this->room_booking->resource)) { - return $this->room_booking->resource->getDerivedClassInstance(); + public function getRooms() : array + { + if (Config::get()->RESOURCES_ENABLE) { + $rooms = []; + foreach ($this->room_bookings as $booking) { + $room = $booking->resource->getDerivedClassInstance(); + if ($room instanceof Room) { + $rooms[] = $room; + } + } + uasort($rooms, function ($a, $b) { + if ($a->name < $b->name) { + return -1; + } elseif ($a->name > $b->name) { + return 1; + } else { + return 0; + } + }); + return $rooms; } - return null; + return []; } /** @@ -271,32 +295,21 @@ class CourseDate extends SimpleORMap implements PrivacyObject, Event $this->raum = ''; $this->store(); - //If there is already a room assigned, "change" the booking. - //Otherwise, create a new one. - if ($this->room_booking instanceof ResourceBooking) { - $this->room_booking->begin = $this->date; - $this->room_booking->end = $this->end_time; - $this->room_booking->resource_id = $room->id; - $this->room_booking->preparation_time = $preparation_time * 60; - $this->room_booking->subsequent_time = $subsequent_time * 60; - $this->room_booking->store(); - } else { - $room->createBooking( - User::findCurrent(), - $this->id, - [['begin' => $this->date, 'end' => $this->end_time]], - null, - 0, - null, - $preparation_time * 60, - '', - '', - ResourceBooking::TYPE_NORMAL, - false, - '', - $subsequent_time * 60 - ); - } + $room->createBooking( + User::findCurrent(), + $this->id, + [['begin' => $this->date, 'end' => $this->end_time]], + null, + 0, + null, + $preparation_time, + '', + '', + ResourceBooking::TYPE_NORMAL, + false, + '', + $subsequent_time + ); return true; } @@ -354,13 +367,15 @@ class CourseDate extends SimpleORMap implements PrivacyObject, Event } if (in_array($format, ['include-room', 'long-include-room'])) { - $room = $this->getRoom(); - if ($room) { - $string = sprintf('%s <a href="%s" target="_blank">%s</a>', - $string, - $room->getActionURL('booking_plan'), - htmlReady($room->name) - ); + $rooms = $this->getRooms(); + if ($rooms) { + foreach ($rooms as $room) { + $string = sprintf('%s <a href="%s" target="_blank">%s</a>', + $string, + $room->getActionURL('booking_plan'), + htmlReady($room->name) + ); + } } elseif ($this->raum) { //Use the freetext room name: $string .= ' ' . $this->raum; @@ -402,10 +417,18 @@ class CourseDate extends SimpleORMap implements PrivacyObject, Event $ex_date = new CourseExDate(); $ex_date->setData($date); - if ($room = $this->getRoom()) { - $ex_date['resource_id'] = $room->getId(); - } $ex_date->setId($ex_date->getNewId()); + if ($rooms = $this->getRooms()) { + $db = DBManager::get(); + $stmt = $db->prepare( + "INSERT INTO `ex_termin_rooms` (`ex_termin_id`, `room_id`, `mkdate`, `chdate`) + VALUES (:date_id, :room_id, UNIX_TIMESTAMP(), UNIX_TIMESTAMP())" + ); + foreach ($rooms as $room) { + $stmt->execute(['date_id' => $ex_date->id, 'room_id' => $room->id]); + } + } + if ($ex_date->store()) { //Update some (but not all) relations to the date so that they @@ -441,7 +464,7 @@ class CourseDate extends SimpleORMap implements PrivacyObject, Event public function store() { // load room-booking, if any - $this->room_booking; + $this->room_bookings; $cache = \Studip\Cache\Factory::getCache(); $cache->expire('course/undecorated_data/'. $this->range_id); @@ -489,8 +512,8 @@ class CourseDate extends SimpleORMap implements PrivacyObject, Event $warnings[] = _('Diesem Termin ist ein Thema zugeordnet.'); } - if (Config::get()->RESOURCES_ENABLE && $this->getRoom()) { - $warnings[] = _('Dieser Termin hat eine Raumbuchung, welche mit dem Termin gelöscht wird.'); + if (Config::get()->RESOURCES_ENABLE && $this->getRooms()) { + $warnings[] = _('Dieser Termin hat Raumbuchungen, die mit dem Termin gelöscht werden.'); } return $warnings; @@ -628,7 +651,7 @@ class CourseDate extends SimpleORMap implements PrivacyObject, Event public function getLocation(): string { - return $this->getRoomName(); + return $this->getRoomNames(); } public function getUniqueId(): string diff --git a/lib/models/CourseExDate.php b/lib/models/CourseExDate.php index ee38f2f..59a7947 100644 --- a/lib/models/CourseExDate.php +++ b/lib/models/CourseExDate.php @@ -97,7 +97,7 @@ class CourseExDate extends SimpleORMap implements PrivacyObject, Event * * @return String that is always empty */ - public function getRoomName() + public function getRoomNames() { return ''; } @@ -107,9 +107,9 @@ class CourseExDate extends SimpleORMap implements PrivacyObject, Event * * @return null. always. canceled dates need no room. */ - public function getRoom() + public function getRooms() { - return null; + return []; } /** @@ -339,7 +339,7 @@ class CourseExDate extends SimpleORMap implements PrivacyObject, Event public function getLocation(): string { - return $this->getRoomName(); + return $this->getRoomNames(); } public function getUniqueId(): string diff --git a/lib/models/SeminarCycleDate.php b/lib/models/SeminarCycleDate.php index 4f7b6dc..e400665 100644 --- a/lib/models/SeminarCycleDate.php +++ b/lib/models/SeminarCycleDate.php @@ -539,17 +539,19 @@ class SeminarCycleDate extends SimpleORMap $date->date = mktime(date('G', strtotime($this->start_time)), date('i', strtotime($this->start_time)), 0, date('m', $tos), date('d', $tos), date('Y', $tos)) + $day * 24 * 60 * 60; $date->end_time = mktime(date('G', strtotime($this->end_time)), date('i', strtotime($this->end_time)), 0, date('m', $toe), date('d', $toe), date('Y', $toe)) + $day * 24 * 60 * 60; - if ($date instanceof CourseDate && !is_null($date->room_booking)) { + if ($date instanceof CourseDate && !empty($date->room_bookings)) { //Check if the time range of the date has decreased and did not exceed the - //boundaries of the existing room booking. In that case, the room booking is shortened. - if ($date->date < $tos || $date->end_time > $toe) { - //The room booking must be deleted. - $date->room_booking->delete(); - } else { - //The room booking must be shortened. - $date->room_booking->begin = $date->date; - $date->room_booking->end = $date->end_time; - $date->room_booking->store(); + //boundaries of the existing room booking. In that case, the room bookings are shortened. + foreach ($date->room_bookings as $room_booking) { + if ($date->date < $tos || $date->end_time > $toe) { + //The room booking must be deleted. + $room_booking->delete(); + } else { + //The room booking must be shortened. + $room_booking->begin = $date->date; + $room_booking->end = $date->end_time; + $room_booking->store(); + } } } @@ -1037,7 +1039,7 @@ class SeminarCycleDate extends SimpleORMap $booked_c = 0; foreach ($this->dates as $course_date) { - if ($course_date->room_booking) { + if (count($course_date->room_bookings)) { $booked_c++; } } @@ -1088,7 +1090,7 @@ class SeminarCycleDate extends SimpleORMap //List the dates that have no room booking: $unbooked_dates = []; foreach ($this->dates as $course_date) { - if (!$course_date->room_booking) { + if (empty($course_date->room_bookings)) { $unbooked_dates[] = $course_date; } } diff --git a/lib/models/resources/Resource.php b/lib/models/resources/Resource.php index 8a7fd06..9730128 100644 --- a/lib/models/resources/Resource.php +++ b/lib/models/resources/Resource.php @@ -93,6 +93,13 @@ class Resource extends SimpleORMap implements StudipItem 'foreign_key' => 'parent_id' ]; + $config['has_and_belongs_to_many']['separable_room'] = [ + 'class_name' => SeparableRoom::class, + 'thru_table' => 'separable_room_parts', + 'thru_key' => 'room_id', + 'thru_assoc_key' => 'separable_room_id' + ]; + $config['i18n_fields']['description'] = true; $config['additional_fields']['class_name'] = ['category', 'class_name']; diff --git a/lib/models/resources/ResourceBooking.php b/lib/models/resources/ResourceBooking.php index 805cff0..ef98e71 100644 --- a/lib/models/resources/ResourceBooking.php +++ b/lib/models/resources/ResourceBooking.php @@ -435,24 +435,7 @@ class ResourceBooking extends SimpleORMap implements PrivacyObject, Studip\Calen } $this->deleteOverlappingReservations(); - if (parent::store()) { - //Check if the booking is bound to a course date. - //If this is the case, check for existing bookings - //and delete them, so that there is only one booking - //for a course date: - $course_date_exists = CourseDate::exists($this->range_id); - if ($course_date_exists) { - self::deleteBySql( - 'range_id = :range_id AND id <> :this_id', - [ - 'this_id' => $this->id, - 'range_id' => $this->range_id - ] - ); - } - return true; - } - return false; + return parent::store(); } diff --git a/lib/models/resources/ResourceRequest.php b/lib/models/resources/ResourceRequest.php index 2d08ee7..12a1e41 100644 --- a/lib/models/resources/ResourceRequest.php +++ b/lib/models/resources/ResourceRequest.php @@ -983,11 +983,17 @@ class ResourceRequest extends SimpleORMap implements PrivacyObject, Studip\Calen ]; } - $date = CourseDate::find($appointment->appointment_id); + $date = CourseDate::find($appointment->appointment_id); + $booked_rooms = []; + $booking_ids = []; + foreach ($date->room_bookings as $booking) { + $booked_rooms[] = $booking->resource_id; + $booking_ids[] = $booking->id; + } $interval['range'] = 'CourseDate'; $interval['range_id'] = $appointment->appointment_id; - $interval['booked_room'] = $date->room_booking->resource_id ?? ''; - $interval['booking_id'] = $date->room_booking->id ?? ''; + $interval['booked_rooms'] = $booked_rooms; + $interval['booking_ids'] = $booking_ids; $time_intervals['']['intervals'][] = $interval; } @@ -1012,11 +1018,17 @@ class ResourceRequest extends SimpleORMap implements PrivacyObject, Studip\Calen ]; } - $date = CourseDate::find($this->termin_id); - $interval['range'] = 'CourseDate'; - $interval['range_id'] = $this->termin_id; - $interval['booked_room'] = $date->room_booking->resource_id ?? null; - $interval['booking_id'] = $date->room_booking->id ?? null; + $date = CourseDate::find($this->termin_id); + $booked_rooms = []; + $booking_ids = []; + foreach ($date->room_bookings as $booking) { + $booked_rooms[] = $booking->resource_id; + $booking_ids[] = $booking->id; + } + $interval['range'] = 'CourseDate'; + $interval['range_id'] = $this->termin_id; + $interval['booked_rooms'] = $booked_rooms; + $interval['booking_ids'] = $booking_ids; if (!empty($interval)) { return [ @@ -1050,10 +1062,16 @@ class ResourceRequest extends SimpleORMap implements PrivacyObject, Studip\Calen 'end' => $date->end_time ]; } + $booked_rooms = []; + $booking_ids = []; + foreach ($date->room_bookings as $booking) { + $booked_rooms[] = $booking->resource_id; + $booking_ids[] = $booking->id; + } $interval['range'] = 'CourseDate'; $interval['range_id'] = $date->id; - $interval['booked_room'] = $date->room_booking->resource_id ?? null; - $interval['booking_id'] = $date->room_booking->id ?? null; + $interval['booked_rooms'] = $booked_rooms; + $interval['booking_ids'] = $booking_ids; $time_intervals[$this->metadate_id]['intervals'][] = $interval; } return $time_intervals; @@ -1081,10 +1099,16 @@ class ResourceRequest extends SimpleORMap implements PrivacyObject, Studip\Calen 'end' => $date->end_time ]; } + $booked_rooms = []; + $booking_ids = []; + foreach ($date->room_bookings as $booking) { + $booked_rooms[] = $booking->resource_id; + $booking_ids[] = $booking->id; + } $interval['range'] = CourseDate::class; - $interval['range_id'] = $date->id; - $interval['booked_room'] = $date->room_booking->resource_id ?? null; - $interval['booking_id'] = $date->room_booking->id ?? null; + $interval['range_id'] = $date->id; + $interval['booked_rooms'] = $booked_rooms; + $interval['booking_ids'] = $booking_ids; $time_intervals[$cycle->id]['intervals'][] = $interval; } } @@ -1114,10 +1138,17 @@ class ResourceRequest extends SimpleORMap implements PrivacyObject, Studip\Calen 'end' => $date->end_time ]; } + + $booked_rooms = []; + $booking_ids = []; + foreach ($date->room_bookings as $booking) { + $booked_rooms[] = $booking->resource_id; + $booking_ids[] = $booking->id; + } $interval['range'] = 'CourseDate'; $interval['range_id'] = $date->id; - $interval['booked_room'] = $date->room_booking->resource_id ?? null; - $interval['booking_id'] = $date->room_booking->id ?? null; + $interval['booked_rooms'] = $booked_rooms; + $interval['booking_ids'] = $booking_ids; $time_intervals['']['intervals'][] = $interval; } @@ -1141,8 +1172,10 @@ class ResourceRequest extends SimpleORMap implements PrivacyObject, Studip\Calen 'end' => $this->end ]; } - $interval['range'] = 'User'; - $interval['range_id'] = $this->user_id; + $interval['range'] = 'User'; + $interval['range_id'] = $this->user_id; + $interval['booked_rooms'] = []; + $interval['booking_ids'] = []; return [ '' => [ @@ -1209,11 +1242,16 @@ class ResourceRequest extends SimpleORMap implements PrivacyObject, Studip\Calen } if ($with_range) { $date = CourseDate::find($appointment->appointment_id); - - $interval['range'] = ResourceRequestAppointment::class; - $interval['range_id'] = $appointment->appointment_id; - $interval['booked_room'] = $date->room_booking->resource_id ?? null; - $interval['booking_id'] = $date->room_booking->id ?? null; + $booked_rooms = []; + $booking_ids = []; + foreach ($date->room_bookings as $booking) { + $booked_rooms[] = $booking->resource_id; + $booking_ids[] = $booking->id; + } + $interval['range'] = ResourceRequestAppointment::class; + $interval['range_id'] = $appointment->appointment_id; + $interval['booked_rooms'] = $booked_rooms; + $interval['booking_ids'] = $booking_ids; } $time_intervals[] = $interval; @@ -1235,10 +1273,18 @@ class ResourceRequest extends SimpleORMap implements PrivacyObject, Studip\Calen ]; } if ($with_range) { - $interval['range'] = CourseDate::class; - $interval['range_id'] = $this->termin_id; - $interval['booked_room'] = $this->date->room_booking->resource_id ?? null; - $interval['booking_id'] = $this->date->room_booking->id ?? null; + if ($this->date->room_bookings) { + $booked_rooms = []; + $booking_ids = []; + foreach ($this->date->room_bookings as $booking) { + $booked_rooms[] = $booking->resource_id; + $booking_ids[] = $booking->id; + } + } + $interval['range'] = CourseDate::class; + $interval['range_id'] = $this->termin_id; + $interval['booked_rooms'] = $booked_rooms; + $interval['booking_ids'] = $booking_ids; } return [$interval]; } elseif ($this->metadate_id) { @@ -1259,10 +1305,19 @@ class ResourceRequest extends SimpleORMap implements PrivacyObject, Studip\Calen ]; } if ($with_range) { - $interval['range'] = CourseDate::class; - $interval['range_id'] = $date->id; - $interval['booked_room'] = $date->room_booking->resource_id ?? null; - $interval['booking_id'] = $date->room_booking->id ?? null; + if ($this->date->room_bookings) { + $booked_rooms = []; + $booking_ids = []; + foreach ($this->date->room_bookings as $booking) { + $booked_rooms[] = $booking->resource_id; + $booking_ids[] = $booking->id; + } + } + + $interval['range'] = CourseDate::class; + $interval['range_id'] = $date->id; + $interval['booked_rooms'] = $booked_rooms; + $interval['booking_ids'] = $booking_ids; } $time_intervals[] = $interval; } @@ -1286,10 +1341,19 @@ class ResourceRequest extends SimpleORMap implements PrivacyObject, Studip\Calen ]; } if ($with_range) { - $interval['range'] = CourseDate::class; - $interval['range_id'] = $date->id; - $interval['booked_room'] = $date->room_booking->resource_id ?? null; - $interval['booking_id'] = $date->room_booking->id ?? null; + if ($this->date->room_bookings) { + $booked_rooms = []; + $booking_ids = []; + foreach ($this->date->room_bookings as $booking) { + $booked_rooms[] = $booking->resource_id; + $booking_ids[] = $booking->id; + } + } + + $interval['range'] = CourseDate::class; + $interval['range_id'] = $date->id; + $interval['booked_rooms'] = $booked_rooms; + $interval['booking_ids'] = $booking_ids; } $time_intervals[] = $interval; } @@ -1333,11 +1397,14 @@ class ResourceRequest extends SimpleORMap implements PrivacyObject, Studip\Calen if ($interval['range'] === 'CourseDate') { $date = call_user_func([$interval['range'], 'find'], $interval['range_id']); - if ($date->room_booking) { - $room_obj = Room::find($date->room_booking->resource_id); - if ($room_obj) { - $room = $room_obj->name; + if ($date->room_bookings) { + $room_names = []; + foreach ($date->room_bookings as $room_booking) { + if (!empty($room_booking->resource)) { + $room_names[] = $room_booking->resource->name; + } } + $room = implode(', ', $room_names); } } diff --git a/lib/resources/RoomManager.php b/lib/resources/RoomManager.php index 9036c4e..5746115 100644 --- a/lib/resources/RoomManager.php +++ b/lib/resources/RoomManager.php @@ -527,6 +527,10 @@ class RoomManager * partially available in those time ranges (false). * Defaults to true. * + * @param string[] $excluded_booking_ids The IDs of bookings that shall be + * excluded when checking the availability of rooms in the specified + * time ranges. + * * @return array */ public static function findRooms( @@ -538,7 +542,8 @@ class RoomManager $order_by = null, $only_requestable_rooms = true, $excluded_room_ids = [], - $only_fully_available = true + $only_fully_available = true, + $excluded_booking_ids = [] ) { $sql = "INNER JOIN resource_categories rc @@ -649,7 +654,7 @@ class RoomManager $end = new DateTime(); $end->setTimestamp($time_range['end']); } - if ($room->isAvailable($begin, $end)) { + if ($room->isAvailable($begin, $end, $excluded_booking_ids)) { if (!$only_fully_available) { $room_is_available = true; break; diff --git a/resources/assets/javascripts/bootstrap/raumzeit.js b/resources/assets/javascripts/bootstrap/raumzeit.js index 76aec32..3a6414e 100644 --- a/resources/assets/javascripts/bootstrap/raumzeit.js +++ b/resources/assets/javascripts/bootstrap/raumzeit.js @@ -5,128 +5,6 @@ STUDIP.Dialog.handlers.header['X-Raumzeit-Update-Times'] = function(json) { $('.course-admin #course-' + info.course_id + ' .raumzeit').html(info.html); }; -STUDIP.ready(function () { - $('#block_appointments_days input').click(function() { - var clicked_id = parseInt(this.id.split('_').pop(), 10); - if (clicked_id === 0 || clicked_id === 1) { - $('#block_appointments_days input:checkbox').prop('checked', function(i) { - return i === clicked_id; - }); - } else { - $('#block_appointments_days_0').prop('checked', false); - $('#block_appointments_days_1').prop('checked', false); - } - }); -}); - -$(document).on('change', 'select[name=room_sd]', function() { - $('input[type=radio][name=room][value=room]').prop('checked', true); -}); - -$(document).on('focus', 'input[name=freeRoomText_sd]', function() { - $('input[type=radio][name=room][value=freetext]').prop('checked', true); -}); - -$(document).on('click', '.bookable_rooms_action', function(event) { - var select = $(this).prev('select')[0], - me = $(this); - if (select !== null && select !== undefined) { - if (me.data('state') === 'enabled') { - STUDIP.Raumzeit.disableBookableRooms(me); - } else { - if (me.data('options') === undefined) { - me.data( - 'options', - $(select) - .children('option') - .clone(true) - ); - } else { - $(select) - .empty() - .append(me.data('options').clone(true)); - } - - let singleDate; - if ($(this).parents('form').attr('action').split('saveDate/').length > 1) { - singleDate = $(this) - .parents('form') - .attr('action') - .split('saveDate/')[1] - .split('?')[0]; - } - - let checked_dates; - if ($("input[name='checked_dates']").length > 0) { - checked_dates = $("input[name='checked_dates']") - .val() - .split(','); - } else { - checked_dates = [singleDate]; - var startDate = $("input[name='date']").val(); - var start_time = $("input[name='start_time']") - .val() - .split(':'); - var end_time = $("input[name='end_time']") - .val() - .split(':'); - var date_obj = [ - { name: 'startDate', value: startDate }, - { name: 'start_stunde', value: start_time[0] }, - { name: 'start_minute', value: start_time[1] }, - { name: 'end_stunde', value: end_time[0] }, - { name: 'end_minute', value: end_time[1] } - ]; - } - - $.ajax({ - type: 'POST', - url: STUDIP.ABSOLUTE_URI_STUDIP + 'dispatch.php/resources/helpers/bookable_rooms', - data: { - rooms: _.map(select.options, 'value'), - selected_dates: checked_dates, - singleDateID: singleDate, - new_date: date_obj - }, - success: function(result) { - if ($.isArray(result)) { - if (result.length) { - var not_bookable_rooms = _.map(result, function(v) { - return $(select) - .children('option[value=' + v + ']') - .text() - .trim(); - }); - select.title = - $gettext('Nicht buchbare Räume:') + ' ' + not_bookable_rooms.join(', '); - } else { - select.title = ''; - } - _.each(result, function(v) { - $(select) - .children('option[value=' + v + ']') - .prop('disabled', true); - }); - } else { - select.title = ''; - } - me.attr('title', $gettext('Alle Räume anzeigen')); - me.data('state', 'enabled'); - } - }); - } - } - event.preventDefault(); -}); - -$(document).on('change', 'input[name="singledate[]"]', function() { - STUDIP.Raumzeit.disableBookableRooms($('.bookable_rooms_action')); -}); - -STUDIP.ready((event) => { - $('.bookable_rooms_action', event.target).show(); -}); - $(document).on('change', '.datesBulkActions', function() { var $button = $(this).next('button'); if ($(this).val() === 'delete') { diff --git a/resources/assets/stylesheets/scss/raumzeit.scss b/resources/assets/stylesheets/scss/raumzeit.scss index c680b26..f0fa3a5 100644 --- a/resources/assets/stylesheets/scss/raumzeit.scss +++ b/resources/assets/stylesheets/scss/raumzeit.scss @@ -111,7 +111,3 @@ div.at_least_one_teacher { font-weight: bold; text-decoration: underline; } - -.bookable_rooms_action { - cursor: pointer; -} diff --git a/resources/vue/apps/CourseBlockAppointments.vue b/resources/vue/apps/CourseBlockAppointments.vue new file mode 100644 index 0000000..35ae1c4 --- /dev/null +++ b/resources/vue/apps/CourseBlockAppointments.vue @@ -0,0 +1,266 @@ +<template> + <fieldset> + <legend>{{ $gettext('Grunddaten') }}</legend> + <section> + <label class="col-2"> + {{ $gettext('Startdatum') }} + <datepicker name="start_date" + v-model="start_date"></datepicker> + </label> + <label class="col-2"> + {{ $gettext('Enddatum') }} + <datepicker name="end_date" + v-model="end_date"></datepicker> + </label> + </section> + <section> + <label class="col-2"> + {{ $gettext('Beginn') }} + <timepicker name="start_time" + v-model="start_time_str"></timepicker> + </label> + <label class="col-2"> + {{ $gettext('Ende') }} + <timepicker name="end_time" + v-model="end_time_str"></timepicker> + </label> + </section> + <section id="block_appointment_days"> + <label>{{ $gettext('Die Termine finden an folgenden Tagen statt:') }}</label> + <label class="col-2"> + <input type="checkbox" name="dow[]" v-model="all_days_selected" value="all" + :checked="all_days_selected"> + {{ $gettext('Jeden Tag') }} + </label> + <label class="col-2"> + <input type="checkbox" name="dow[]" v-model="mon_fri_selected" value="mon_fri" + :checked="mon_fri_selected"> + {{ $gettext('Montag - Freitag') }} + </label> + + <label class="col-2"> + <input type="checkbox" name="dow[]" v-model="dow" :value="1" + :checked="dow.includes(1)"> + {{ $gettext('Montag') }} + </label> + <label class="col-2"> + <input type="checkbox" name="dow[]" v-model="dow" :value="2" + :checked="dow.includes(2)"> + {{ $gettext('Dienstag') }} + </label> + <label class="col-2"> + <input type="checkbox" name="dow[]" v-model="dow" :value="3" + :checked="dow.includes(3)"> + {{ $gettext('Mittwoch') }} + </label> + <label class="col-2"> + <input type="checkbox" name="dow[]" v-model="dow" :value="4" + :checked="dow.includes(4)"> + {{ $gettext('Donnerstag') }} + </label> + <label class="col-2"> + <input type="checkbox" name="dow[]" v-model="dow" :value="5" + :checked="dow.includes(5)"> + {{ $gettext('Freitag') }} + </label> + <label class="col-2"> + <input type="checkbox" name="dow[]" v-model="dow" :value="6" + :checked="dow.includes(6)"> + {{ $gettext('Samstag') }} + </label> + <label class="col-2"> + <input type="checkbox" name="dow[]" v-model="dow" :value="0" + :checked="dow.includes(0)"> + {{ $gettext('Sonntag') }} + </label> + </section> + <section> + <label> + {{ $gettext('Anzahl der Termine') }} + <input type="number" name="date_count" + min="1" :max="this.time_ranges.length" + v-model="this.date_count"> + </label> + <studip-message-box v-if="this.date_count > 50" + type="info" :hideClose="true" + :hideDetails="false"> + {{ $gettextInterpolate( + 'Sie legen %{count} Termine an. Bitte kontrollieren Sie Ihre Eingaben.', + {count: this.date_count} + ) }} + </studip-message-box> + </section> + </fieldset> + <CourseDateRoomFieldset + :time_ranges="time_ranges" + :room_management_enabled="room_management_enabled" + :initial_selected_room_option="'noroom'" + :allow_multiple_room_bookings="allow_multiple_room_bookings" + :initial_preparation_time="initial_preparation_time" + :initial_subsequent_time="initial_subsequent_time" + :max_preparation_time="max_preparation_time" + ></CourseDateRoomFieldset> + <fieldset> + <legend>{{ $gettext('Weitere Angaben') }}</legend> + <label> + {{ $gettext('Termintyp') }} + <select name="date_type" + v-model="selected_date_type"> + <option v-for="date_type in date_types" :value="date_type.id" :key="date_type.id"> + {{ date_type.name}} + </option> + </select> + </label> + <label> + {{ $gettext('Zugewiesene Lehrende') }} + <multiselect name="assigned_lecturers[]" + :options="available_lecturer_options" + v-model="selected_lecturer_list" + :no_options_text="$gettext('Keine Lehrenden auswählbar')" + :value="selected_lecturer_list" + ></multiselect> + <input type="hidden" name="assigned_lecturers[]" + v-for="item in selected_lecturer_list" + v-bind:key="item" :value="item"> + </label> + </fieldset> +</template> +<script> +import {$gettext} from "../../assets/javascripts/lib/gettext"; +import CourseDateRoomFieldset from "../components/CourseDateRoomFieldset.vue"; +import Timepicker from "../components/Timepicker.vue"; +import Datepicker from "../components/Datepicker.vue"; +import StudipMessageBox from "../components/StudipMessageBox.vue"; +export default { + name: 'CourseBlockAppointments', + components: {StudipMessageBox, CourseDateRoomFieldset, Timepicker, Datepicker}, + props: { + room_management_enabled: { + type: Boolean, + required: true, + default: false + }, + max_preparation_time: { + type: Number, + required: false, + default: 999 + }, + initial_preparation_time: { + type: Number, + required: false, + default: 0 + }, + initial_subsequent_time: { + type: Number, + required: false, + default: 0 + }, + allow_multiple_room_bookings: { + type: Boolean, + required: false, + default: false + }, + date_types: { + type: Array, + required: true + }, + available_lecturers: { + type: Array, + required: false, + default: () => [] + }, + selected_lecturers: { + type: Array, + required: false, + default: () => [] + } + }, + methods: { + $gettext, + }, + data() { + let now = new Date(); + //Use the next half hour as default: + let start_date = new Date(Math.ceil(now.getTime() / 1800000) * 1800000); + let end_date = new Date((Math.ceil(now.getTime() / 1800000) * 1800000) + 1800000); + let start_time_str = STUDIP.DateTime.pad(start_date.getHours()) + ':' + STUDIP.DateTime.pad(start_date.getMinutes()); + let end_time_str = STUDIP.DateTime.pad(end_date.getHours()) + ':' + STUDIP.DateTime.pad(end_date.getMinutes()); + let selected_date_type = ''; + if (this.date_types.length > 0) { + selected_date_type = this.date_types[0].id; + } + + return { + start_date: now.getTime() / 1000, + end_date: now.getTime() / 1000, + start_time_str, + end_time_str, + all_days_selected: true, + mon_fri_selected: false, + dow: [], + date_count: 0, + last_changed_start_date: new Date(), + last_changed_end_date: new Date(), + last_changed_start_time: new Date(), + last_changed_end_time: new Date(), + last_changed_dow: new Date(), + available_lecturer_options: this.available_lecturers !== undefined ? this.available_lecturers : [], + selected_lecturer_list: this.selected_lecturers !== undefined ? this.selected_lecturers : [], + selected_date_type + } + }, + computed: { + time_ranges() { + if (this.start_date > this.end_date) { + //Invalid time range selection. + return []; + } + let start_time_parts = this.start_time_str.split(':'); + let end_time_parts = this.end_time_str.split(':'); + if (start_time_parts.length !== 2 || end_time_parts.length !== 2) { + //Invalid time format. + return []; + } + let day_numbers = []; + if (this.all_days_selected) { + day_numbers = [0, 1, 2, 3, 4, 5, 6]; + } else if (this.mon_fri_selected) { + day_numbers = [1, 2, 3, 4, 5]; + } else { + day_numbers = this.dow; + } + if (day_numbers.length === 0) { + //No days selected. Nothing to do. + return []; + } + let current_start = new Date(this.start_date * 1000); + current_start.setHours(parseInt(start_time_parts[0]), parseInt(start_time_parts[1]), 0, 0); + let current_end = new Date(this.end_date * 1000); + current_end.setHours(parseInt(end_time_parts[0]), parseInt(end_time_parts[1]), 0, 0); + + let new_time_ranges = []; + while (current_start < current_end) { + let relevant_day = current_start.getDay(); + if (day_numbers.includes(relevant_day)) { + //Put the day into the time ranges. + let range_start = new Date(current_start.getTime()); + let range_end = new Date(current_start.getTime()); + range_end.setHours(parseInt(end_time_parts[0]), parseInt(end_time_parts[1]), 0, 0); + new_time_ranges.push({start: range_start, end: range_end}); + } + current_start.setDate(current_start.getDate() + 1); + } + return new_time_ranges; + } + }, + watch: { + time_ranges(newValue) { + if (newValue === undefined) { + this.date_count = 0; + } else { + this.date_count = newValue.length; + } + } + } +} +</script> diff --git a/resources/vue/apps/CourseDateFormContent.vue b/resources/vue/apps/CourseDateFormContent.vue new file mode 100644 index 0000000..0cf6b40 --- /dev/null +++ b/resources/vue/apps/CourseDateFormContent.vue @@ -0,0 +1,227 @@ +<template> + <fieldset> + <legend>{{ $gettext('Grunddaten') }}</legend> + <label class="col-2"> + {{ $gettext('Datum') }} + <datepicker name="date" + v-model="start_date"></datepicker> + </label> + <label class="col-2"> + {{ $gettext('Beginn') }} + <timepicker name="start_time" + v-model="start_time_str"></timepicker> + </label> + <label class="col-2"> + {{ $gettext('Ende') }} + <timepicker name="end_time" + v-model="end_time_str"></timepicker> + </label> + </fieldset> + <CourseDateRoomFieldset + :time_ranges="time_ranges" + :course_date_ids="course_date_ids" + :show_nochange_option="course_date_ids.length > 0" + :room_management_enabled="room_management_enabled" + :initial_selected_rooms="selected_rooms" + :initial_room_name="initial_room_name" + :allow_multiple_room_bookings="allow_multiple_room_bookings" + :initial_preparation_time="initial_preparation_time" + :initial_subsequent_time="initial_subsequent_time" + ></CourseDateRoomFieldset> + + <fieldset> + <legend>{{ $gettext('Weitere Angaben') }}</legend> + <label> + {{ $gettext('Termintyp') }} + <select name="date_type" + v-model="selected_date_type"> + <option v-for="date_type in date_types" :value="date_type.id" :key="date_type.id"> + {{date_type.name}} + </option> + </select> + </label> + + <label> + {{ $gettext('Zugewiesene Lehrende') }} + <multiselect name="assigned_lecturers[]" + :options="available_lecturer_options" + v-model="selected_lecturer_list" + :no_options_text="$gettext('Keine Lehrenden auswählbar')" + :value="selected_lecturer_list" + ></multiselect> + <input type="hidden" name="assigned_lecturers[]" + v-for="item in selected_lecturer_list" + v-bind:key="item" :value="item"> + </label> + <label> + {{ $gettext('Beteiligte Gruppen') }} + <multiselect name="assigned_groups[]" + :options="available_group_options" + v-model="selected_group_list" + :no_options_text="$gettext('Keine Gruppen auswählbar')" + :value="selected_group_list" + ></multiselect> + <input type="hidden" name="assigned_groups[]" + v-for="item in selected_group_list" + v-bind:key="item" :value="item"> + </label> + <label v-if="enable_number_of_participants"> + {{ $gettext('Anzahl der Teilnehmenden') }} + <input type="number" min="0" + name="number_of_participants"> + </label> + </fieldset> +</template> +<script> +import {$gettext} from "../../assets/javascripts/lib/gettext"; +import Datepicker from "../components/Datepicker.vue"; +import Timepicker from "../components/Timepicker.vue"; +import Multiselect from "../components/Multiselect.vue"; +import CourseDateRoomFieldset from "../components/CourseDateRoomFieldset.vue"; + +export default { + name: 'CourseDateFormContent', + components: {CourseDateRoomFieldset, Multiselect, Timepicker, Datepicker}, + props: { + course_date: { + type: Object, + required: false, + default: null + }, + date_types: { + type: Array, + required: true + }, + room_management_enabled: { + type: Boolean, + required: true, + default: false + }, + initial_preparation_time: { + type: Number, + required: false, + default: 0 + }, + initial_subsequent_time: { + type: Number, + required: false, + default: 0 + }, + max_preparation_time: { + type: Number, + required: false, + default: 999 + }, + allow_multiple_room_bookings: { + type: Boolean, + required: false, + default: false + }, + enable_number_of_participants: { + type: Boolean, + required: false, + default: false + }, + selected_rooms: { + type: Array, + required: false, + default: () => [] + }, + available_lecturers: { + type: Array, + required: false, + default: () => [] + }, + selected_lecturers: { + type: Array, + required: false, + default: () => [] + }, + available_groups: { + type: Array, + required: false, + default: () => [] + }, + selected_groups: { + type: Array, + required: false, + default: () => [] + }, + }, + data() { + let selected_date_type = ''; + let course_date_ids = []; + let start_date = null; + let end_date = null; + let initial_room_name = ''; + if (this.course_date) { + start_date = new Date(this.course_date.date * 1000); + end_date = new Date(this.course_date.end_time * 1000); + selected_date_type = this.course_date.date_typ; + course_date_ids.push(this.course_date.termin_id); + initial_room_name = this.course_date.raum; + } else { + start_date = new Date(); + if (this.date_types[0] !== undefined) { + selected_date_type = this.date_types[0].id; + } + //Round the time values to the next half hour: + start_date = new Date(Math.ceil(start_date.getTime() / 1800000) * 1800000); + end_date = new Date((Math.ceil(start_date.getTime() / 1800000) * 1800000) + 1800000); + } + let start_time_str = null; + let end_time_str = null; + if (start_date && end_date) { + start_time_str = STUDIP.DateTime.pad(start_date.getHours()) + ':' + STUDIP.DateTime.pad(start_date.getMinutes()); + end_time_str = STUDIP.DateTime.pad(end_date.getHours()) + ':' + STUDIP.DateTime.pad(end_date.getMinutes()); + } + + return { + start_date, + start_time_str, + end_time_str, + course_date_ids, + selected_date_type, + booking_selected: this.room_management_enabled && this.selected_rooms, + separable_room_name: '', + initial_room_name, + last_changed_date: new Date(), + last_changed_start_time: new Date(), + last_changed_end_time: new Date(), + available_lecturer_options: this.available_lecturers !== undefined ? this.available_lecturers : [], + selected_lecturer_list: this.selected_lecturers !== undefined ? this.selected_lecturers : [], + available_group_options: this.available_groups !== undefined ? this.available_groups : [], + selected_group_list: this.selected_groups !== undefined ? this.selected_groups : [] + }; + }, + methods: { + $gettext + }, + computed: { + time_ranges() { + let start = new Date(this.start_date); + let end = new Date(this.start_date); + if (typeof(this.start_date) === 'number') { + //The start date is not a date object but a timestamp. + start = new Date(this.start_date * 1000); + end = new Date(this.start_date * 1000); + } + let start_time_parts = this.start_time_str.split(':'); + if (start_time_parts.length !== 2) { + //Invalid time string. + return []; + } + start.setHours(parseInt(start_time_parts[0]), parseInt(start_time_parts[1]), 0); + + let end_time_parts = this.end_time_str.split(':'); + if (end_time_parts.length !== 2) { + //Invalid time string. + return []; + } + end.setHours(parseInt(end_time_parts[0]), parseInt(end_time_parts[1]), 0); + + return [{start, end}]; + } + } +} +</script> diff --git a/resources/vue/base-components.js b/resources/vue/base-components.js index 904afa1..dd6561e 100644 --- a/resources/vue/base-components.js +++ b/resources/vue/base-components.js @@ -3,6 +3,7 @@ import { defineAsyncComponent } from 'vue'; const BaseComponents = { CaptchaInput: defineAsyncComponent(() => import('./components/form_inputs/CaptchaInput.vue')), CalendarPermissionsTable: defineAsyncComponent(() => import('./components/form_inputs/CalendarPermissionsTable.vue')), + CourseDateRoomFieldset: defineAsyncComponent(() => import('./components/CourseDateRoomFieldset.vue')), DateListInput: defineAsyncComponent(() => import('./components/form_inputs/DateListInput.vue')), Datepicker: defineAsyncComponent(() => import('./components/Datepicker.vue')), Datetimepicker: defineAsyncComponent(() => import('./components/Datetimepicker.vue')), diff --git a/resources/vue/components/CourseDateRoomFieldset.vue b/resources/vue/components/CourseDateRoomFieldset.vue new file mode 100644 index 0000000..1bdec9a --- /dev/null +++ b/resources/vue/components/CourseDateRoomFieldset.vue @@ -0,0 +1,310 @@ +<template> + <fieldset> + <legend>{{ $gettext('Raumangaben') }}</legend> + + <section v-if="room_management_enabled"> + <studip-message-box v-if="selected_room_option === 'room' && available_rooms.length === 0 && searched_for_rooms" + hide-close="true"> + {{ $gettext('Im gewählten Zeitbereich sind keine buchbaren Räume verfügbar.') }} + </studip-message-box> + <label> + <input type="radio" name="room" + v-model="selected_room_option" + value="room"> + {{ $gettext('Gebuchte Räume') }} + </label> + <label v-if="selected_room_option === 'room' && available_rooms.length > 0" for="room_ids[]"> + {{ $gettext('Raum auswählen') }} + </label> + <span class="flex-row"> + <StudipSelect v-if="allow_multiple_room_bookings" + v-model="selected_room_list" + :no_options_text="$gettext('Kein Raum verfügbar')" + :options="available_rooms" + multiple + style="flex-grow: 2"> + <template #selected-option="{id, label}"> + <span>{{ label }}</span> + <input type="hidden" name="room_ids[]" :value="id"> + </template> + </StudipSelect> + <select v-if="!allow_multiple_room_bookings" + name="room_ids[]" + style="flex-grow: 2" + v-model="selected_room_list"> + <option v-for="room of available_rooms" :key="room.id" + :value="room.id" :selected="selected_room_list.includes(room.id)"> + {{ room.label }} + </option> + </select> + <studip-icon v-if="show_ajax_indicator" shape="reload" role="info"></studip-icon> + </span> + <section v-if="selected_room_option === 'room' && available_rooms.length > 0 && Object.keys(visible_info_texts).length > 0"> + <h3>{{ $gettext('Hinweise zu teilbaren Räumen') }}</h3> + <ul class="default"> + <li v-for="item in visible_info_texts" v-bind:key="item"> + {{ item }} + </li> + </ul> + </section> + <label v-if="selected_room_option === 'room' && available_rooms.length > 0"> + {{ $gettext('Rüstzeit vor dem Termin (in Minuten)') }} + <input type="number" name="preparation_time" + class="preparation-time" + v-model="preparation_time" + min="0" + :max="max_preparation_time"> + </label> + <label v-if="selected_room_option === 'room' && available_rooms.length > 0"> + {{ $gettext('Rüstzeit nach dem Termin (in Minuten)') }} + <input type="number" name="subsequent_time" + class="preparation-time" + v-model="subsequent_time" + min="0" + :max="max_preparation_time"> + </label> + </section> + + <label> + <input type="radio" name="room" + v-model="selected_room_option" + value="freetext"> + {{ $gettext('Freie Ortsangabe (keine Raumbuchung)') }} + </label> + <label v-if="selected_room_option === 'freetext'"> + <input type="text" name="room_name" + v-model="room_name" + :placeholder="$gettext('Freie Ortsangabe (keine Raumbuchung)')"> + </label> + + <label> + <input type="radio" name="room" + v-model="selected_room_option" + value="noroom"> + {{ $gettext('Kein Raum') }} + </label> + <label v-if="show_nochange_option"> + <input type="radio" name="room" + v-model="selected_room_option" + value="nochange"> + {{ $gettext('Keine Änderung') }} + </label> + </fieldset> +</template> +<script> +import {$gettext} from "../../assets/javascripts/lib/gettext"; +import StudipMessageBox from "../components/StudipMessageBox.vue"; +import StudipSelect from "../components/StudipSelect.vue"; +import StudipIcon from "../components/StudipIcon.vue"; +import {jsonapi} from "../../assets/javascripts/lib/jsonapi"; + +export default { + name: 'CourseDateRoomFieldset', + components: {StudipMessageBox, StudipSelect, StudipIcon}, + props: { + time_ranges: { + type: Array, + required: true, + default: () => [] + }, + course_date_ids: { + type: Array, + required: false, + default: () => [] + }, + room_management_enabled: { + type: Boolean, + required: true, + default: false + }, + allow_multiple_room_bookings: { + type: Boolean, + required: false, + default: false + }, + max_preparation_time: { + type: Number, + required: false, + default: 999 + }, + initial_selected_rooms: { + type: Array, + required: false, + default: () => [] + }, + initial_selected_room_option: { + type: String, + required: false, + default: 'nochange' + }, + show_nochange_option: { + type: Boolean, + required: false, + default: false + }, + initial_preparation_time: { + type: Number, + required: false, + default: 0 + }, + initial_subsequent_time: { + type: Number, + required: false, + default: 0 + }, + initial_room_name: { + type: String, + required: false, + default: '' + } + }, + data() { + let room_option = this.initial_selected_room_option; + if (!this.show_nochange_option) { + room_option = 'noroom'; + } + return { + searched_for_rooms: false, + available_rooms: [], + preparation_time: this.initial_preparation_time, + subsequent_time: this.initial_subsequent_time, + room_name: this.initial_room_name, + info_texts: [], + selected_room_list: this.initial_selected_rooms !== undefined ? this.initial_selected_rooms : [], + selected_room_option: room_option, + show_ajax_indicator: false + } + }, + methods: { + $gettext, + getAvailableRooms() { + if (this.selected_room_option !== 'room') { + //We don't need to look for available rooms. + return; + } + + //Reload the list of available rooms: + this.show_ajax_indicator = true; + try { + const options = { + method: 'GET', + data: { + time_ranges: JSON.stringify(this.time_ranges) + }, + async: true + }; + if (this.course_date_ids) { + options.data.course_date_ids = this.course_date_ids; + } + jsonapi.request('available-rooms', options).then((response) => { + const json = JSON.parse(response); + if (!json) { + //Error fetching the available rooms. + this.available_rooms = []; + this.searched_for_rooms = true; + } + //Change the format for the multiselect and strip the info text. + this.available_rooms = []; + let available_room_ids = []; + this.info_texts = {}; + let current_separable_room_id = ''; + for (let item of json) { + //$item is an object with the attributes id, name and info_text. + if (item.id.startsWith('separable_room-')) { + //It is a separable room. + this.available_rooms.push( + { + id: item.id, + label: item.name, + indented: false, + separable_room_id: item.separable_room_id + } + ); + available_room_ids.push(item.id); + current_separable_room_id = item.id.substring(15); + } else if (item.separable_room_id) { + //Indent the name of the room part of the separable room: + this.available_rooms.push( + { + id: item.id, + label: item.name, + indented: current_separable_room_id.length > 0, + separable_room_id: item.separable_room_id + } + ); + available_room_ids.push(item.id); + } else { + //A room that is not part of a separable room. + current_separable_room_id = ''; + this.available_rooms.push( + { + id: item.id, + label: item.name, + indented: false, + separable_room_id: null + } + ); + available_room_ids.push(item.id); + } + if (item.info_text && item.info_text.length > 0 && item.separable_room_id) { + this.info_texts[item.id] = { + separable_room_id: item.separable_room_id, + info_text: item.info_text + }; + } + } + this.searched_for_rooms = true; + //Update the selected rooms: If a room is not present in the list of available rooms, + //it shall be removed from the list. + let new_selected_rooms = []; + for (let selected_room of this.selected_room_list) { + if (available_room_ids.includes(selected_room.id)) { + new_selected_rooms.push(selected_room); + } + } + this.selected_room_list = new_selected_rooms; + }); + } catch (error) { + console.error(error); + //Clear the list of available rooms, since we cannot determine + //if the current list is accurate. + this.available_rooms = []; + this.searched_for_rooms = true; + } + this.show_ajax_indicator = false; + }, + }, + computed: { + visible_info_texts() { + let new_visible_info_texts = {}; + for (let item of this.selected_room_list) { + if (item.separable_room_id && this.info_texts[item.id]) { + let item_info_text = this.info_texts[item.id]; + new_visible_info_texts[item_info_text.separable_room_id] = item_info_text.info_text; + } + } + return new_visible_info_texts; + } + }, + watch: { + time_ranges(new_ranges, old_ranges) { + if (old_ranges === undefined || old_ranges === new_ranges) { + //Do nothing. + return; + } + if (this.selected_room_option === 'room') { + this.getAvailableRooms(); + } + }, + selected_room_option(new_options, old_options) { + if (old_options === undefined || old_options === new_options) { + //Do nothing. + return; + } + if (this.selected_room_option === 'room') { + this.getAvailableRooms(); + } + } + } +} +</script> diff --git a/resources/vue/components/Multiselect.vue b/resources/vue/components/Multiselect.vue index 7b70f22..ed1f23c 100644 --- a/resources/vue/components/Multiselect.vue +++ b/resources/vue/components/Multiselect.vue @@ -7,7 +7,10 @@ v-bind="$attrs" > <template v-slot:no-options> - {{ $gettext('Keine Auswahlmöglichkeiten') }} + {{ this.no_options_text }} + </template> + <template #open-indicator="{ selectAttributes }"> + <span v-bind="selectAttributes"><studip-icon shape="arr_1down" :size="10"/></span> </template> </v-select> </template> @@ -15,9 +18,12 @@ <script> import vSelect from 'vue-select'; import 'vue-select/dist/vue-select.css' +import {$gettext} from "../../assets/javascripts/lib/gettext"; +import StudipIcon from "./StudipIcon.vue"; export default { name: 'multiselect', components: { + StudipIcon, vSelect, }, emits: ['update:model-value'], @@ -36,6 +42,11 @@ export default { options: { type: Object, required: true + }, + no_options_text: { + type: String, + required: false, + default: $gettext('Keine Auswahlmöglichkeiten') } }, data () { diff --git a/resources/vue/components/StudipSelect.vue b/resources/vue/components/StudipSelect.vue index 1246aac..dca450a 100644 --- a/resources/vue/components/StudipSelect.vue +++ b/resources/vue/components/StudipSelect.vue @@ -110,3 +110,8 @@ export default { } }; </script> +<style> +.studip-v-select .vs__dropdown-toggle { + max-height: fit-content; +} +</style> |
