ILIAS  trunk Revision v12.0_alpha-1613-gae4c99ebb18
BookableItemWithScheduleTable.php
Go to the documentation of this file.
1<?php
2
19declare(strict_types=1);
20
22
23use DateTimeImmutable;
24use DateTimeZone;
29use ilDateTime;
31use Throwable;
32
34{
35 public const string ID = 'bkbiws';
36
37 // Hard limit for the number of weeks in the future to enumerate slots for
38 public const int MAX_WEEKS_IN_THE_FUTURE = 52;
39
40 private const WEEKDAYS_MAP = [
41 'mo' => 'monday',
42 'tu' => 'tuesday',
43 'we' => 'wednesday',
44 'th' => 'thursday',
45 'fr' => 'friday',
46 'sa' => 'saturday',
47 'su' => 'sunday',
48 ];
49
53 protected function getColumns(): array
54 {
55 $column_factory = $this->ui_factory->table()->column();
56 return [
57 'availability' => $column_factory->text($this->lng->txt('book_table_col_availability'))->withIsSortable(true),
58 'date_time' => $column_factory->text($this->lng->txt('book_table_col_datetime'))->withIsSortable(true),
59 'title' => $column_factory->text($this->lng->txt('title'))->withIsSortable(true),
60 'description' => $column_factory->text($this->lng->txt('description'))->withIsSortable(true),
61 ];
62 }
63
67 protected function getFilterInputs(): array
68 {
69 $bookable_items = [];
70 foreach (ilBookingObject::getList($this->pool->getId()) as $item) {
71 $bookable_items[(int) $item['booking_object_id']] = (string) $item['title'];
72 }
73
74 $field_factory = $this->ui_factory->input()->field();
75 return [
76 'title' => $field_factory->text($this->lng->txt('title')),
77 'description' => $field_factory->text($this->lng->txt('description')),
78 'objects' => $field_factory->multiSelect($this->lng->txt('book_filter_objects'), $bookable_items),
79 'period' => $field_factory->duration($this->lng->txt('book_filter_period'))
80 ->withUseTime(true)
81 ->withFormat($this->user->getDateTimeFormat()),
82 ];
83 }
84
88 protected function loadRecords(mixed $filter_data): array
89 {
90 $filter_data = is_array($filter_data) ? $filter_data : [];
91
92 $period_bounds = $this->resolvePeriod($filter_data);
93 $time_slot_filter = $this->stringFilter($filter_data, 'time_slot');
94
95 $booking_objects = $this->loadFilteredBookingObjects(
96 $this->stringFilter($filter_data, 'title'),
97 $this->stringFilter($filter_data, 'description'),
98 $this->arrayFilter($filter_data, 'objects')
99 );
100
101 $rows = [];
102
103 foreach ($booking_objects as $item) {
104 $schedule_id = (int) $item['schedule_id'];
105 if ($schedule_id === 0) {
106 continue;
107 }
108
109 $object_id = (int) $item['booking_object_id'];
110 $schedule = new ilBookingSchedule($schedule_id);
111 $is_all_day_schedule = $this->isAllDaySchedule($schedule);
112 $slots = $this->enumerateSlots(
113 $schedule,
114 $period_bounds[0] ?? null,
115 $period_bounds[1] ?? null
116 );
117 foreach ($slots as $slot) {
118 if (
119 $time_slot_filter !== null
120 && $this->getTimeSlotLabel($slot['from'], $slot['to'], $is_all_day_schedule) !== $time_slot_filter
121 ) {
122 continue;
123 }
124
125 $rows[] = $this->composeRow($item, $object_id, $slot['from'], $slot['to'], $is_all_day_schedule);
126 }
127 }
128
129 return $rows;
130 }
131
135 protected function buildRowCells(array $record): array
136 {
137 return [
138 'availability' => $this->buildAvailabilityCell((int) $record['available'], (int) $record['nr_items']),
139 'date_time' => $this->formatDateTime(
140 (int) $record['slot_from'],
141 (int) $record['slot_to'],
142 (bool) $record['is_all_day']
143 ),
144 'title' => (string) $record['title'],
145 'description' => nl2br((string) $record['description']),
146 ];
147 }
148
149 protected function getSortCallable(string $key, int $direction): ?callable
150 {
151 return $key === 'date_time'
152 ? static fn(array $a, array $b): int => ((int) $a['slot_from'] <=> (int) $b['slot_from']) * $direction
153 : parent::getSortCallable($key, $direction);
154 }
155
160 private function enumerateSlots(ilBookingSchedule $schedule, ?int $period_start = null, ?int $period_end = null): array
161 {
162 $definition = $schedule->getDefinition();
163 if ($definition === []) {
164 return [];
165 }
166
167 $now = time();
168 $deadline = $schedule->getDeadline();
169 $deadline_timestamp = $now + ($deadline * 3600);
170
171 $availability_from = $schedule->getAvailabilityFrom()?->get(IL_CAL_UNIX);
172 $availability_to = $schedule->getAvailabilityTo()?->get(IL_CAL_UNIX);
173
174 $slots = [];
175
176 foreach ($definition as $weekday_key => $day_slots) {
177 $next_week_day = new DateTimeImmutable('next ' . self::WEEKDAYS_MAP[$weekday_key]);
178
179 for ($i = 0; $i < self::MAX_WEEKS_IN_THE_FUTURE; $i++) {
180 foreach ($day_slots as $time_range) {
181 [$start_string, $end_string] = explode('-', $time_range);
182 [$start_hour, $start_minute] = explode(':', $start_string);
183 [$end_hour, $end_minute] = explode(':', $end_string);
184
185 $start_timestamp = $next_week_day->setTime((int) $start_hour, (int) $start_minute, 0)->getTimestamp();
186 $end_timestamp = $next_week_day->setTime((int) $end_hour, (int) $end_minute, 0)->getTimestamp();
187
188 if (
189 (is_int($availability_from) && $start_timestamp < $availability_from)
190 || (is_int($availability_to) && $end_timestamp > $availability_to)
191 ) {
192 continue;
193 }
194
195 if (
196 ($deadline === 0 && $start_timestamp < $now)
197 || ($deadline === -1 && $end_timestamp < $now)
198 || ($start_timestamp < $deadline_timestamp)
199 ) {
200 continue;
201 }
202
203 if (
204 (is_int($period_start) && $start_timestamp < $period_start)
205 || (is_int($period_end) && $end_timestamp > $period_end)
206 ) {
207 continue;
208 }
209
210 $slots[] = [
211 'from' => $start_timestamp,
212 'to' => $end_timestamp,
213 ];
214 }
215
216 $next_week_day = $next_week_day->modify('+1 week');
217 }
218 }
219
220 return $slots;
221 }
222
227 private function composeRow(
228 array $item,
229 int $object_id,
230 int $slot_from,
231 int $slot_to,
232 bool $is_all_day = false
233 ): array {
234 $nr_items = (int) $item['nr_items'];
235 $available = max($nr_items - $this->countActiveReservations($object_id, $slot_from, $slot_to), 0);
236
237 return [
238 'row_id' => "{$object_id}_{$slot_from}_{$slot_to}",
239 'booking_object_id' => $object_id,
240 'title' => (string) $item['title'],
241 'description' => (string) ($item['description'] ?? ''),
242 'nr_items' => $nr_items,
243 'available' => $available,
244 'is_available' => $available > 0,
245 'has_user_booking' => $this->hasReservation($object_id, $slot_from, $slot_to, $this->user->getId()),
246 'has_user_active_booking' => $this->hasActiveReservation($object_id, $slot_from, $slot_to, $this->user->getId()),
247 'has_reservations' => $this->hasActiveReservation($object_id, $slot_from, $slot_to),
248 'slot_from' => $slot_from,
249 'slot_to' => $slot_to,
250 'is_all_day' => $is_all_day,
251 'schedule_id' => (int) $item['schedule_id'],
252 'post_text' => (string) ($item['post_text'] ?? ''),
253 ];
254 }
255
256 private function hasReservation(int $object_id, int $slot_from, int $slot_to, ?int $user_id = null): bool
257 {
258 return array_any(
259 $this->getReservationsForObject($object_id),
260 static fn(array $reservation): bool =>
261 ($user_id === null || $reservation['user_id'] === $user_id)
262 && $reservation['date_from'] === $slot_from
263 && $reservation['date_to'] === $slot_to
264 );
265 }
266
267 private function hasActiveReservation(int $object_id, int $slot_from, int $slot_to, ?int $user_id = null): bool
268 {
269 return array_any(
270 $this->getReservationsForObject($object_id),
271 static fn(array $reservation): bool =>
272 ($user_id === null || $reservation['user_id'] === $user_id)
273 && $reservation['date_from'] === $slot_from
274 && $reservation['date_to'] === $slot_to
275 && $reservation['status'] !== ilBookingReservation::STATUS_CANCELLED
276 );
277 }
278
279 private function countReservations(int $object_id, int $slot_from, int $slot_to): int
280 {
281 return count(
282 array_filter(
283 $this->getReservationsForObject($object_id),
284 static fn(array $reservation): bool =>
285 $reservation['date_from'] === $slot_from
286 && $reservation['date_to'] === $slot_to
287 )
288 );
289 }
290
291 private function countActiveReservations(int $object_id, int $slot_from, int $slot_to): int
292 {
293 return count(
294 array_filter(
295 $this->getReservationsForObject($object_id),
296 static fn(array $reservation): bool =>
297 $reservation['date_from'] === $slot_from
298 && $reservation['date_to'] === $slot_to
299 && $reservation['status'] !== ilBookingReservation::STATUS_CANCELLED
300 )
301 );
302 }
303
304 public function formatDateTime(int $slot_from, int $slot_to, bool $is_all_day = false): string
305 {
306 $this->lng->loadLanguageModule('dateplaner');
307
308 if ($is_all_day) {
309 return ilDatePresentation::formatDate(new ilDateTime($slot_from, IL_CAL_UNIX, 'UTC'))
310 . ", {$this->lng->txt('book_all_day')}";
311 }
312
314 new ilDateTime($slot_from, IL_CAL_UNIX, 'UTC'),
315 new ilDateTime($slot_to, IL_CAL_UNIX, 'UTC')
316 );
317 }
318
319 private function isAllDaySchedule(ilBookingSchedule $schedule): bool
320 {
322 }
323
324 private function getTimeSlotLabel(int $slot_from, int $slot_to, bool $is_all_day): string
325 {
326 if ($is_all_day) {
327 return $this->lng->txt('book_all_day');
328 }
329
330 $from = new DateTimeImmutable("@{$slot_from}");
331 $to = new DateTimeImmutable("@{$slot_to}");
332
333 return "{$from->format('H:i')} - {$to->format('H:i')}";
334 }
335
336 private function stringFilter(array $filter_data, string $key): ?string
337 {
338 return trim((string) ($filter_data[$key] ?? '')) ?: null;
339 }
340
344 private function arrayFilter(array $filter_data, string $key): ?array
345 {
346 if (!isset($filter_data[$key]) || !is_array($filter_data[$key]) || $filter_data[$key] === []) {
347 return null;
348 }
349
350 return array_values(array_map('intval', $filter_data[$key]));
351 }
352
356 private function resolvePeriod(array $filter_data): ?array
357 {
358 $period = $filter_data['period'] ?? null;
359 if (!is_array($period) || count($period) < 2) {
360 return null;
361 }
362
363 $from = $this->parsePeriodEndpoint($period[0] ?? null);
364 $to = $this->parsePeriodEndpoint($period[1] ?? null);
365
366 if ($from === null && $to === null) {
367 return null;
368 }
369
370 $default_start = $this->defaultPeriodStart()->getTimestamp();
371
372 return [
373 $from ?? $default_start,
374 $to ?? PHP_INT_MAX,
375 ];
376 }
377
378 private function defaultPeriodStart(): DateTimeImmutable
379 {
380 return new DateTimeImmutable('today', new DateTimeZone($this->userTimeZoneId()));
381 }
382
383 private function userTimeZoneId(): string
384 {
385 return $this->user->getTimeZone() ?: date_default_timezone_get();
386 }
387
388 private function parsePeriodEndpoint(mixed $value): ?int
389 {
390 if ($value === null || $value === '') {
391 return null;
392 }
393
394 if ($value instanceof DateTimeImmutable) {
395 return $value->getTimestamp();
396 }
397
398 try {
399 return (new DateTimeImmutable((string) $value))->getTimestamp();
400 } catch (Throwable) {
401 return null;
402 }
403 }
404}
loadFilteredBookingObjects(?string $title_filter, ?string $description_filter, ?array $object_ids_filter)
hasActiveReservation(int $object_id, int $slot_from, int $slot_to, ?int $user_id=null)
composeRow(array $item, int $object_id, int $slot_from, int $slot_to, bool $is_all_day=false)
enumerateSlots(ilBookingSchedule $schedule, ?int $period_start=null, ?int $period_end=null)
hasReservation(int $object_id, int $slot_from, int $slot_to, ?int $user_id=null)
const IL_CAL_UNIX
This file is part of ILIAS, a powerful learning management system published by ILIAS open source e-Le...
static getList(int $a_pool_id, ?string $a_title=null)
Get list of booking objects.
This file is part of ILIAS, a powerful learning management system published by ILIAS open source e-Le...
This file is part of ILIAS, a powerful learning management system published by ILIAS open source e-Le...
Class for date presentation.
static formatDate(ilDateTime $date, bool $a_skip_day=false, bool $a_include_wd=false, bool $include_seconds=false, ?ilObjUser $user=null,)
static formatPeriod(ilDateTime $start, ilDateTime $end, bool $a_skip_starting_day=false, ?ilObjUser $user=null)
Format a period of two dates Shows: 14.
@classDescription Date and time handling
A Column describes the form of presentation for a certain aspect of data, i.e.
Definition: Column.php:28
$a
thx to https://mlocati.github.io/php-cs-fixer-configurator for the examples