aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMoritz Strohm <strohm@data-quest.de>2026-01-14 10:29:35 +0000
committerMoritz Strohm <strohm@data-quest.de>2026-01-14 10:29:35 +0000
commit78e46de33b3f205aae375d1ea6d4fe088e0e5124 (patch)
tree4b305bf3f7b5d066ac28f011fe752e98901e714c
parentf637e7ae2d086941a11297ccc29ac273ad6759b0 (diff)
allow booking separable rooms in courses, closes #639
Closes #639 Merge request studip/studip!4039
-rw-r--r--app/controllers/course/block_appointments.php347
-rw-r--r--app/controllers/course/dates.php7
-rw-r--r--app/controllers/course/timesrooms.php858
-rw-r--r--app/controllers/resources/admin.php39
-rw-r--r--app/controllers/resources/room_request.php17
-rw-r--r--app/views/calendar/contentbox/_termin.php2
-rw-r--r--app/views/course/block_appointments/index.php178
-rw-r--r--app/views/course/dates/_date_row.php15
-rw-r--r--app/views/course/dates/current_day_dates.php12
-rw-r--r--app/views/course/timesrooms/_cycleRow.php27
-rw-r--r--app/views/course/timesrooms/_irregularEvents.php2
-rw-r--r--app/views/course/timesrooms/createSingleDate.php142
-rw-r--r--app/views/course/timesrooms/editDate.php266
-rw-r--r--app/views/course/timesrooms/editStack.php104
-rw-r--r--app/views/resources/admin/edit_separable_room.php25
-rw-r--r--app/views/resources/admin/separable_rooms.php4
-rw-r--r--app/views/resources/room_request/resolve.php2
-rw-r--r--app/views/resources/room_request/resolve_room_tr.php6
-rw-r--r--app/views/room_management/overview/rooms.php11
-rw-r--r--db/migrations/6.2.6_multiple_rooms_per_course_date.php66
-rw-r--r--lib/classes/CourseDateList.php24
-rw-r--r--lib/classes/InstituteCalendarHelper.php10
-rw-r--r--lib/classes/JsonApi/RouteMap.php3
-rw-r--r--lib/classes/JsonApi/Routes/AvailableRooms.php201
-rw-r--r--lib/classes/JsonApi/SchemaMap.php2
-rw-r--r--lib/classes/calendar/ICalendarExport.php2
-rw-r--r--lib/dates.inc.php14
-rw-r--r--lib/extern/ExternPageTimetable.php19
-rw-r--r--lib/models/Course.php2
-rw-r--r--lib/models/CourseDate.php127
-rw-r--r--lib/models/CourseExDate.php8
-rw-r--r--lib/models/SeminarCycleDate.php26
-rw-r--r--lib/models/resources/Resource.php7
-rw-r--r--lib/models/resources/ResourceBooking.php19
-rw-r--r--lib/models/resources/ResourceRequest.php143
-rw-r--r--lib/resources/RoomManager.php9
-rw-r--r--resources/assets/javascripts/bootstrap/raumzeit.js122
-rw-r--r--resources/assets/stylesheets/scss/raumzeit.scss4
-rw-r--r--resources/vue/apps/CourseBlockAppointments.vue266
-rw-r--r--resources/vue/apps/CourseDateFormContent.vue227
-rw-r--r--resources/vue/base-components.js1
-rw-r--r--resources/vue/components/CourseDateRoomFieldset.vue310
-rw-r--r--resources/vue/components/Multiselect.vue13
-rw-r--r--resources/vue/components/StudipSelect.vue5
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>