aboutsummaryrefslogtreecommitdiff
path: root/lib/classes
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 /lib/classes
parentf637e7ae2d086941a11297ccc29ac273ad6759b0 (diff)
allow booking separable rooms in courses, closes #639
Closes #639 Merge request studip/studip!4039
Diffstat (limited to 'lib/classes')
-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
6 files changed, 224 insertions, 18 deletions
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,