From 24d8590e8b10ef3f95c23dacd95ab0a1f33801f3 Mon Sep 17 00:00:00 2001 From: Matheus Zych Date: Fri, 29 May 2026 11:28:19 +0200 Subject: [PATCH 1/8] BookingPool: Adds Booking Pool Common classes See: https://docu.ilias.de/go/wiki/wpage_7320_1357 Adds common classes, mainly shared by all the new Booking Pool tables. --- .../BookingManager/src/Common/HttpService.php | 104 +++++++++++ .../BookingManager/src/Common/Table/Table.php | 37 ++++ .../src/Common/Table/TableAction.php | 57 ++++++ .../Common/Table/TableActionExecutorTrait.php | 36 ++++ .../Common/Table/TableActionModalTrait.php | 163 ++++++++++++++++++ .../src/Common/Table/TableActions.php | 102 +++++++++++ .../src/Common/Table/TableActionsFactory.php | 26 +++ 7 files changed, 525 insertions(+) create mode 100644 components/ILIAS/BookingManager/src/Common/HttpService.php create mode 100644 components/ILIAS/BookingManager/src/Common/Table/Table.php create mode 100644 components/ILIAS/BookingManager/src/Common/Table/TableAction.php create mode 100644 components/ILIAS/BookingManager/src/Common/Table/TableActionExecutorTrait.php create mode 100644 components/ILIAS/BookingManager/src/Common/Table/TableActionModalTrait.php create mode 100644 components/ILIAS/BookingManager/src/Common/Table/TableActions.php create mode 100644 components/ILIAS/BookingManager/src/Common/Table/TableActionsFactory.php diff --git a/components/ILIAS/BookingManager/src/Common/HttpService.php b/components/ILIAS/BookingManager/src/Common/HttpService.php new file mode 100644 index 000000000000..ff7bf6267db6 --- /dev/null +++ b/components/ILIAS/BookingManager/src/Common/HttpService.php @@ -0,0 +1,104 @@ +http->request(); + } + + public function getRefId(): int + { + return $this->get(self::KEY_REF_ID, $this->refinery->kindlyTo()->int()); + } + + public function resolveRowParameter(string $key): string|int + { + return $this->get($key, $this->refinery->byTrying([ + $this->refinery->kindlyTo()->int(), + $this->refinery->kindlyTo()->string(), + $this->refinery->custom()->transformation(static fn(array $value): string|int => $value[0]) + ])); + } + + public function resolveRowParameters(string $key): array|string + { + return $this->get($key, $this->refinery->custom()->transformation( + static fn(array|string $value): array|string => $value === self::ALL_OBJECTS || $value[0] === self::ALL_OBJECTS + ? self::ALL_OBJECTS + : array_map( + static fn(string $value): string|int => count(explode('_', $value)) > 1 ? $value : (int) $value, + $value + ) + )) ?? []; + } + + public function get(string $key, Transformation $t): mixed + { + $wrapper = $this->http->wrapper(); + + return match(true) { + $wrapper->post()->has($key) => $wrapper->post()->retrieve($key, $t), + $wrapper->query()->has($key) => $wrapper->query()->retrieve($key, $t), + default => null, + }; + } + + /** + * @param Stream|string|mixed $response + */ + public function sendAsync(mixed $response): void + { + $response = match(true) { + is_string($response) => Streams::ofString($response), + is_resource($response) => Streams::ofResource($response), + default => $response, + }; + + $this->http->saveResponse($this->http->response()->withBody($response)); + $this->http->sendResponse(); + $this->http->close(); + } + + public function has(string $key): bool + { + return $this->http->wrapper()->query()->has($key) || $this->http->wrapper()->post()->has($key); + } +} diff --git a/components/ILIAS/BookingManager/src/Common/Table/Table.php b/components/ILIAS/BookingManager/src/Common/Table/Table.php new file mode 100644 index 000000000000..8bb8b894ff12 --- /dev/null +++ b/components/ILIAS/BookingManager/src/Common/Table/Table.php @@ -0,0 +1,37 @@ + + */ + public function getComponents(URLBuilder $url_builder): array; +} diff --git a/components/ILIAS/BookingManager/src/Common/Table/TableAction.php b/components/ILIAS/BookingManager/src/Common/Table/TableAction.php new file mode 100644 index 000000000000..889ed1c23d51 --- /dev/null +++ b/components/ILIAS/BookingManager/src/Common/Table/TableAction.php @@ -0,0 +1,57 @@ +getTableActions()->execute(...$this->acquireParameters($url_builder)); + } + + abstract protected function acquireParameters(URLBuilder $url_builder): array; + + abstract protected function getTableActions(): TableActions; +} diff --git a/components/ILIAS/BookingManager/src/Common/Table/TableActionModalTrait.php b/components/ILIAS/BookingManager/src/Common/Table/TableActionModalTrait.php new file mode 100644 index 000000000000..ff2eec8267c8 --- /dev/null +++ b/components/ILIAS/BookingManager/src/Common/Table/TableActionModalTrait.php @@ -0,0 +1,163 @@ +http->resolveRowParameter($action_type_token->getName())) { + self::SUBMIT_MODAL_ACTION => $this->submit($url_builder, $row_id_token, $action_token, $action_type_token), + default => $this->showModal($url_builder, $row_id_token, $action_token, $action_type_token), + }; + } + + protected function showModal( + URLBuilder $url_builder, + URLBuilderToken $row_id_token, + URLBuilderToken $action_token, + URLBuilderToken $action_type_token, + ): void { + $selected_ids = $this->http->resolveRowParameters($row_id_token->getName()); + $all_records_selected = $selected_ids === HttpService::ALL_OBJECTS; + + if ($all_records_selected) { + $selected_ids = null; + } else { + $selected_ids = is_string($selected_ids) ? [] : $selected_ids; + } + + $selected_records = array_filter( + $this->resolveRecords($selected_ids), + fn(mixed $record): bool => $this->allowActionForRecord($record) + ); + + $this->http->sendAsync( + $this->ui_renderer->renderAsync( + $this->getModal( + $url_builder + ->withParameter($row_id_token, $all_records_selected ? HttpService::ALL_OBJECTS : ($selected_ids ?? [])) + ->withParameter($action_token, $this->getActionId()) + ->withParameter($action_type_token, self::SUBMIT_MODAL_ACTION), + $selected_records, + $all_records_selected + ) + ) + ); + } + + protected function submit( + URLBuilder $url_builder, + URLBuilderToken $row_id_token, + URLBuilderToken $action_token, + URLBuilderToken $action_type_token, + ): ?Modal { + $selected_ids = $this->http->resolveRowParameters($row_id_token->getName()); + $all_records_selected = $selected_ids === HttpService::ALL_OBJECTS; + + if ($all_records_selected) { + $selected_ids = null; + } else { + $selected_ids = is_string($selected_ids) ? [] : $selected_ids; + } + + if (!$all_records_selected && $selected_ids === []) { + $this->showErrorMessage($this->getSelectionErrorMessage()); + return null; + } + + $selected_records = array_filter( + $this->resolveRecords($selected_ids), + fn(mixed $record): bool => $this->allowActionForRecord($record) + ); + + if ($selected_records === []) { + $this->showErrorMessage($this->getSelectionErrorMessage()); + return null; + } + + return $this->onSubmit( + $url_builder + ->withParameter($row_id_token, $all_records_selected ? HttpService::ALL_OBJECTS : $selected_ids) + ->withParameter($action_token, $this->getActionId()) + ->withParameter($action_type_token, self::SUBMIT_MODAL_ACTION), + $selected_records, + $all_records_selected + ); + } + + protected function showErrorMessage(string $message): void + { + $this->tpl->setOnScreenMessage(ilGlobalTemplateInterface::MESSAGE_TYPE_FAILURE, $message, true); + } + + protected function showSuccessMessage(string $message): void + { + $this->tpl->setOnScreenMessage(ilGlobalTemplateInterface::MESSAGE_TYPE_SUCCESS, $message, true); + } + + public function getSelectionErrorMessage(): ?string + { + return $this->lng->txt('no_valid_selection'); + } + + /** + * @param list $selected_records + */ + abstract protected function getModal( + URLBuilder $url_builder, + array $selected_records, + bool $all_records_selected + ): ?Modal; + + /** + * @param list $selected_records + */ + abstract protected function onSubmit( + URLBuilder $url_builder, + array $selected_records, + bool $all_records_selected + ): ?Modal; + + abstract protected function resolveRecords(?array $selected_ids = null): array; +} diff --git a/components/ILIAS/BookingManager/src/Common/Table/TableActions.php b/components/ILIAS/BookingManager/src/Common/Table/TableActions.php new file mode 100644 index 000000000000..a24ee8dee887 --- /dev/null +++ b/components/ILIAS/BookingManager/src/Common/Table/TableActions.php @@ -0,0 +1,102 @@ + $actions + */ + public function __construct( + protected readonly ilCtrlInterface $ctrl, + protected readonly ilLanguage $lng, + protected readonly ilGlobalTemplateInterface $tpl, + protected readonly UIFactory $ui_factory, + protected readonly UIRenderer $ui_renderer, + protected readonly Refinery $refinery, + protected readonly HttpService $http, + protected readonly array $actions + ) { + } + + public function getEnabledActions( + URLBuilder $url_builder, + URLBuilderToken $row_id_token, + URLBuilderToken $action_token, + URLBuilderToken $action_type_token + ): array { + return array_filter( + array_map( + static fn(TableAction $action): ?Action => $action->isAvailable() + ? $action->getTableAction($url_builder, $row_id_token, $action_token, $action_type_token) + : null, + $this->actions + ) + ); + } + + public function execute( + URLBuilder $url_builder, + URLBuilderToken $row_id_token, + URLBuilderToken $action_token, + URLBuilderToken $action_type_token + ): ?Modal { + if (!$this->http->has($action_token->getName())) { + return null; + } + + $action_id = $this->http->resolveRowParameter($action_token->getName()); + $action = $this->actions[$action_id] ?? null; + if ($action === null) { + return null; + } + + $response = $action->onExecute($url_builder, $row_id_token, $action_token, $action_type_token); + + return $response instanceof Modal ? $response : null; + } + + public function onDataRow(DataRow $row, mixed $record): DataRow + { + foreach ($this->actions as $action_id => $action) { + if ($action->allowActionForRecord($record)) { + continue; + } + + $row = $row->withDisabledAction($action_id); + } + + return $row; + } +} diff --git a/components/ILIAS/BookingManager/src/Common/Table/TableActionsFactory.php b/components/ILIAS/BookingManager/src/Common/Table/TableActionsFactory.php new file mode 100644 index 000000000000..446172504c43 --- /dev/null +++ b/components/ILIAS/BookingManager/src/Common/Table/TableActionsFactory.php @@ -0,0 +1,26 @@ + Date: Fri, 29 May 2026 11:28:44 +0200 Subject: [PATCH 2/8] BookingPool: Adds Booking Pool Language Variables See: https://docu.ilias.de/go/wiki/wpage_7320_1357 Adds and adjusts existing language variables used by the new Booking Pool tables. --- lang/ilias_de.lang | 48 ++++++++++++++++++++++++++++++++++++++++++---- lang/ilias_en.lang | 48 ++++++++++++++++++++++++++++++++++++++++++---- 2 files changed, 88 insertions(+), 8 deletions(-) diff --git a/lang/ilias_de.lang b/lang/ilias_de.lang index c6169ad684dd..02be4ea6dc65 100644 --- a/lang/ilias_de.lang +++ b/lang/ilias_de.lang @@ -2543,7 +2543,8 @@ book#:#book_all_pools_need_schedules#:#Die Änderungen wurden nicht gespeichert. book#:#book_all_users#:#Alle Personen book#:#book_assign#:#Buchen book#:#book_assign_object#:#Für diese Person buchen -book#:#book_assign_participant#:#Für andere Personen buchen +book#:#book_assign_participant#:#Für andere Person buchen +book#:#book_assign_participants#:#Für andere Person(en) buchen book#:#book_back_to_list#:#Zurück zur Liste book#:#book_bobj#:#Angebote book#:#book_book#:#Buchen @@ -2565,9 +2566,13 @@ book#:#book_confirm_booking_schedule_number_of_objects#:#Buchungsbestätigung book#:#book_confirm_booking_schedule_number_of_objects_info#:#Bitte wählen Sie die Anzahl der zu buchenden Angebote. book#:#book_confirm_cancel#:#Sind Sie sicher, dass Sie die folgenden Buchungen stornieren möchten? book#:#book_confirm_cancel_aggregation#:#Anzahl Stornierungen -book#:#book_confirm_delete#:#Sind Sie sicher, dass Sie die folgenden Angebote löschen wollen? +book#:#book_confirm_cancel_info#:#Die ausgewählten Buchungen werden storniert. +book#:#book_confirm_delete#:#Sind Sie sicher, dass Sie die folgenden Angebote löschen wollen inklusive aller zugehörigen Zeitfenster? Dabei werden auch alle zugehörigen Buchungen gelöscht. +book#:#book_confirm_remove_participant#:#Sind Sie sicher, dass Sie die folgende(n) Teilnehmer entfernen wollen? Dabei werden alle zugehörigen Buchungen gelöscht. book#:#book_copy#:#Buchungspool kopieren book#:#book_create_objects#:#Angebote erstellen +book#:#book_date_from#:#Von Datum +book#:#book_date_to#:#Bis Datum book#:#book_deadline#:#Deadline book#:#book_deadline_hours#:#Bis n Stunden vor Beginn des Zeitfensters book#:#book_deadline_info#:#Minimale Zeit zwischen Buchung und Buchungszeitraum @@ -2575,11 +2580,18 @@ book#:#book_deadline_options#:#Spätester Buchungszeitpunkt book#:#book_deadline_slot_end#:#Bis zum Ende des Zeitfensters book#:#book_deadline_slot_start#:#Bis zum Beginn des Zeitfensters book#:#book_deassign#:#Buchungen bearbeiten +book#:#book_delete_object#:#Löschen +book#:#book_deleted_booking#:#Gelöschte Buchung(en) +book#:#book_deleted_object#:#Gelöschtes buchbares Objekt book#:#book_download_info#:#Zusätzliche Beschreibung herunterladen book#:#book_edit#:#Buchungspool bearbeiten book#:#book_edit_object#:#Angebot bearbeiten book#:#book_edit_schedule#:#Zeitplan bearbeiten +book#:#book_filter_end_date#:#Enddatum +book#:#book_filter_objects#:#Buchbare Objekte book#:#book_filter_past_reservations#:#Abgelaufene Buchungen anzeigen +book#:#book_filter_period#:#Zeitraum +book#:#book_filter_start_date#:#Startdatum book#:#book_fromto#:#Datumsbereich book#:#book_hours#:#Stunden vor Beginn des Zeitfensters book#:#book_is_used#:#In Benutzung @@ -2594,14 +2606,23 @@ book#:#book_messages#:#Mitteilungen book#:#book_messages_info#:#Buchende können einer Buchung eine Mitteilung mitgeben. book#:#book_missing_availability#:#Es können leider nicht alle Termine gebucht werden. book#:#book_missing_items#:#Es fehlen $1. +book#:#book_modal_booking_confirmation#:#Buchungsbestätigung +book#:#book_modal_enter_quantity_intro#:#Bitte geben Sie die Anzahl der Buchungen ein. +book#:#book_modal_recurrence#:#Wiederholung +book#:#book_modal_recurrence_multiple#:#Mehrere Termine buchen +book#:#book_modal_recurrence_single#:#Nur diesen Termin buchen +book#:#book_modal_skipped_unknown_item#:#Unbekannte oder ungültige Auswahl (%s). +book#:#book_modal_warning_skipped_selections#:#Einige ausgewählte Angebote oder Termine sind nicht buchbar und werden in dieser Buchungsbestätigung nicht berücksichtigt. book#:#book_new#:#Neuen Buchungspool anlegen book#:#book_no_bookings_for_you#:#Es wurden keine Buchungen für Sie durchgeführt. book#:#book_no_objects#:#Keine Buchungen +book#:#book_no_objects_available#:#Keines der ausgewählten Angebote ist derzeit für eine Buchung verfügbar. book#:#book_no_of_objects#:#Anzahl Angebote book#:#book_no_pools_selected#:#Diesem Kurs ist kein Buchungspool zugewiesen. Buchungspools können über den Reiter „Ressourcen“ des Kurses zugewiesen werden. book#:#book_no_preferences_for_you#:#Sie haben keine Präferenzen abgespeichert. book#:#book_no_recurrence#:#Nur diesen Termin buchen book#:#book_not#:#Nicht +book#:#book_not_cancelled#:#Nicht storniert book#:#book_not_enough_preferences#:#Sie haben nicht genügend Präferenzen ausgewählt. Die Präferenzen wurden nicht gespeichert. book#:#book_notification#:#Benachrichtigungen über Reservierungen book#:#book_notification_cron_not_active#:#Achtung: Der notwendige Cron-Job "Benachrichtigungen über Reservierungen" ist zurzeit nicht aktiviert. @@ -2613,13 +2634,18 @@ book#:#book_obj_select#:#Auswählen book#:#book_object_added#:#Ein Angebot wurde hinzugefügt. book#:#book_object_deleted#:#Das Angebot wurde gelöscht. book#:#book_object_selection#:#Angebotsauswahl +book#:#book_object_title_or_description#:#Titel/Beschreibung book#:#book_object_updated#:#Das Angebot wurde geändert. book#:#book_objects_available#:#Verfügbare Angebote: %s book#:#book_open#:#Buchungspool öffnen book#:#book_overall_limit#:#Gesamtzahl der Buchungen pro Benutzer begrenzen book#:#book_overall_limit_warning#:#Sie haben die maximale Anzahl Buchungen erreicht. +book#:#book_overall_limit_would_be_exceeded#:#Ungültige Auswahl. Die Gesamtzahl der Buchungen würde überschritten werden. +book#:#book_participant#:#Teilnehmer book#:#book_participant_already_assigned#:#Mindestens eine Person war bereits zugewiesen. book#:#book_participant_assigned#:#Die Personen wurden hinzugefügt. +book#:#book_participant_removed#:#Die Teilnehmer und alle zugehörigen Buchungen wurden erfolgreich entfernt. +book#:#book_past_bookings#:#Vergangene Buchungen book#:#book_period#:#Dauer book#:#book_pool_added#:#Ein Buchungspool wurde angelegt. book#:#book_pool_selection#:#Auswahl Buchungspool @@ -2635,6 +2661,7 @@ book#:#book_pref_overview#:#Übersicht book#:#book_preference_info#:#Bitte wählen Sie %1 Präferenzen bis %2. Nach der Frist werden die Angebote automatisch zugeordnet. Die Präferenzen aller Beteiligten werden nach Möglichkeit berücksichtigt. book#:#book_preferences#:#Präferenzen book#:#book_preferences_saved#:#Die Präferenzen wurden gespeichert. +book#:#book_present_bookings#:#Aktuelle und zukünftige Buchungen book#:#book_public_log#:#Benutzer sehen alle Buchungen book#:#book_public_log_info#:#Benutzer mit Leserecht können im Reiter „Buchungen“ auch Buchungen anderer Benutzer einsehen. book#:#book_recurrence#:#Wiederholung @@ -2645,14 +2672,16 @@ book#:#book_reminder_day#:#Versenden book#:#book_reminder_day_info#:#Sendet eine Liste eigener anstehender Buchungen und zusätzlich eine Gesamtliste an Personen mit dem Recht „Einstellungen bearbeiten“. Bitte beachten Sie: Um Benachrichtigungen zu erhalten, müssen die Personen diese über das Menü „Aktionen“ oben rechts im Buchungspool „Benachrichtigungen“ aktiviert haben. book#:#book_reminder_days#:#Tage vor der Buchung book#:#book_reminder_setting#:#Per Mail an eigene Buchung erinnern +book#:#book_remove_participants#:#Teilnehmer entfernen book#:#book_rerun_assignments#:#Zuweisungen erneut durchführen book#:#book_rerun_confirmation#:#Achtung. Die Zuweisung nach Präferenzen ist bereits erfolgt. In seltenen Fällen kann ein Abbruch des Prozesses zu keinen oder unvollständigen Buchungen führen und ein erneutes Starten des Prozesses notwendig machen. Bevor sie den automatischen Prozess erneut starten, müssen sie alle bestehenden Buchungen entfernen, um Mehrfachzuweisungen zu verhindern. book#:#book_reservation_available#:#%s verfügbar +book#:#book_reservation_cancelled#:#Buchung(en) storniert. book#:#book_reservation_confirmed#:#Die Buchung wurde erfolgreich durchgeführt. book#:#book_reservation_failed#:#Die Buchung ist fehlgeschlagen. book#:#book_reservation_failed_overbooked#:#Die Buchung ist fehlgeschlagen, da das betreffende Angebot nicht mehr verfügbar ist. -book#:#book_reservation_filter_period#:#Voreinstellung für den Filter „Datumsbereich“ -book#:#book_reservation_filter_period_info#:#Im Reiter „Buchungen“ wird der Filter „Datumsbereich“ auf diesen Wert ab dem aktuellen Datum voreingestellt. +book#:#book_reservation_filter_period#:#Voreinstellung für den Filter Zeitraum +book#:#book_reservation_filter_period_info#:#Im Reiter „Buchungen“ und "Angebote" wird der Filter „Zeitraum“ auf diesen Wert ab dem aktuellen Datum voreingestellt. Bei einem Wert von "0" wird kein Filter gesetzt. book#:#book_reservation_fix_info#:#Buchungen können nur für die Zeitfenster durchgeführt werden, die unten als verfügbar angegeben werden. book#:#book_reservation_overview#:#Buchungen aller Benutzer book#:#book_reservation_status_5#:#Storniert @@ -2680,17 +2709,28 @@ book#:#book_select_pool#:#Pool auswählen book#:#book_set_cancel#:#Stornieren book#:#book_set_delete#:#Löschen book#:#book_show_message#:#Mitteilung anzeigen +book#:#book_show_past_bookings#:#Abgelaufene Buchungen anzeigen +book#:#book_some_reservations_unavailable#:#Einige der ausgewählten Angebote konnten nicht gebucht werden, da sie nicht mehr verfügbar sind. +book#:#book_table#:#Tabelle +book#:#book_table_col_availability#:#Verfügbarkeit +book#:#book_table_col_datetime#:#Datum/Uhrzeit book#:#book_title_description_nr#:#Titel; Beschreibung; Anzahl book#:#book_title_description_nr_info#:#Geben Sie bitte Titel, Beschreibung und Anzahl getrennt durch ein Semikolon oder TAB ein. Nutzen Sie einen Zeile je Buchungsobjekt. book#:#book_too_many_preferences#:#Sie haben zu viele Präferenzen ausgewählt. Die Präferenzen wurden nicht gespeichert. book#:#book_total_individual_bookings_limit#:#Begrenze die Gesamtanzahl an Buchungen, die von/für einzelne Teilnehmende durchgeführt werden können – über alle verfügbaren Objekte hinweg. Jedes buchbare Objekt kann dabei weiterhin nur einmal pro Teilnehmendem gebucht werden. book#:#book_type_warning#:#Es sind momentan keine Angebote vorhanden. Um den Pool nutzen zu können, müssen zuerst Angebote angelegt werden. +book#:#book_view#:#Ansicht book#:#book_week#:#Wochenansicht +book#:#book_week_no_objects_selected#:#Wählen Sie rechts unter ‘Angebotsauswahl‘ mindestens ein Angebot aus und klicken Sie auf den Button ‘Aktualisieren‘. book#:#book_your_bookings#:#Ihre Buchungen book#:#book_your_preferences#:#Ihre Präferenzen book#:#book_your_reservations#:#Ihre Buchungen book#:#booking_multiple_succesfully#:#Buchungen erfolgreich durchgeführt book#:#booking_nr_of_items#:#Anzahl +book#:#booking_process_error#:#Bei der Verarbeitung Ihrer Buchung(en) ist ein Fehler aufgetreten. Bitte versuchen Sie es erneut. +book#:#bookings_log#:#Buchungen +book#:#delete_bookable_item#:#Löschen des Angebots +book#:#no_valid_selection#:#Keine gültige Auswahl book#:#obj_book_duplicate#:#Buchungspool kopieren book#:#participants#:#Personen book#:#reservation_deleted#:#Buchung gelöscht diff --git a/lang/ilias_en.lang b/lang/ilias_en.lang index 1c83f5f65981..187c58e1c516 100644 --- a/lang/ilias_en.lang +++ b/lang/ilias_en.lang @@ -2544,7 +2544,8 @@ book#:#book_all_pools_need_schedules#:#Settings not saved. All selected booking book#:#book_all_users#:#All Participants book#:#book_assign#:#Book book#:#book_assign_object#:#Book Items for Participant -book#:#book_assign_participant#:#Book for Other Participant(s) +book#:#book_assign_participant#:#Book for Other Participant +book#:#book_assign_participants#:#Book for other Participant(s) book#:#book_back_to_list#:#Back to List book#:#book_bobj#:#Bookable Item book#:#book_book#:#Book @@ -2566,9 +2567,13 @@ book#:#book_confirm_booking_schedule_number_of_objects#:#Booking Confirmation book#:#book_confirm_booking_schedule_number_of_objects_info#:#Please enter the number of items that you would like to book. book#:#book_confirm_cancel#:#Are you sure you want to cancel the following booking(s)? book#:#book_confirm_cancel_aggregation#:#Number of Cancellations -book#:#book_confirm_delete#:#Are you sure you want to delete the following items? +book#:#book_confirm_cancel_info#:#The selected bookings will be cancelled. +book#:#book_confirm_delete#:#Are you sure you want to delete the following items including all related time slots? This will also delete all related Bookings. +book#:#book_confirm_remove_participant#:#Are you sure you want to remove the following Participant(s)? This will delete all related Bookings. book#:#book_copy#:#Copy Booking Pool book#:#book_create_objects#:#Create Items +book#:#book_date_from#:#From date +book#:#book_date_to#:#To date book#:#book_deadline#:#Deadline book#:#book_deadline_hours#:#X hours before time slot begins book#:#book_deadline_info#:#Minimum time between booking and booked period @@ -2576,11 +2581,18 @@ book#:#book_deadline_options#:#Bookings Can Be Made Until: book#:#book_deadline_slot_end#:#Time slot ends book#:#book_deadline_slot_start#:#Time slot begins book#:#book_deassign#:#Edit Bookings +book#:#book_delete_object#:#Delete +book#:#book_deleted_booking#:#Deleted Booking(s) +book#:#book_deleted_object#:#Deleted Bookable Item book#:#book_download_info#:#Download Additional Description book#:#book_edit#:#Edit Booking Pool book#:#book_edit_object#:#Edit Bookable Item book#:#book_edit_schedule#:#Edit Schedule +book#:#book_filter_end_date#:#End Date +book#:#book_filter_objects#:#Bookable Items book#:#book_filter_past_reservations#:#Show Past Bookings +book#:#book_filter_period#:#Period +book#:#book_filter_start_date#:#Start Date book#:#book_fromto#:#Date Range book#:#book_hours#:#Hours before start of timeslot. book#:#book_is_used#:#In Use @@ -2595,14 +2607,23 @@ book#:#book_messages#:#Messages book#:#book_messages_info#:#Allow users to add a message when booking an item. book#:#book_missing_availability#:#It was not possible to book all items on all dates. book#:#book_missing_items#:#$1 missing item(s). +book#:#book_modal_booking_confirmation#:#Booking Confirmation +book#:#book_modal_enter_quantity_intro#:#Please enter the number of items that you would like to book. +book#:#book_modal_recurrence#:#Recurrence +book#:#book_modal_recurrence_multiple#:#Book multiple dates +book#:#book_modal_recurrence_single#:#Book only this date +book#:#book_modal_skipped_unknown_item#:#Unknown or invalid selection (%s). +book#:#book_modal_warning_skipped_selections#:#Some selected items or periods are not available for booking and are excluded from this confirmation. book#:#book_new#:#New Booking Pool book#:#book_no_bookings_for_you#:#No bookings have been made for you. book#:#book_no_objects#:#No Bookings +book#:#book_no_objects_available#:#None of the selected items are currently available for booking. book#:#book_no_of_objects#:#No. of Units book#:#book_no_pools_selected#:#No booking pools have been assigned to this course. Booking pools can be assigned via the course's 'Resources' tab. book#:#book_no_preferences_for_you#:#No preferences have been recorded for you. book#:#book_no_recurrence#:#Book only this date book#:#book_not#:#Not +book#:#book_not_cancelled#:#Not cancelled book#:#book_not_enough_preferences#:#You have not selected enough preferences. Your preferences have not been saved. book#:#book_notification#:#Send Booking Notifications book#:#book_notification_cron_not_active#:#Note: The necessary cron job, 'Send Reservation Notifications', is currently not active. @@ -2614,13 +2635,18 @@ book#:#book_obj_select#:#Select book#:#book_object_added#:#Bookable item added. book#:#book_object_deleted#:#Bookable item deleted. book#:#book_object_selection#:#Item Selection +book#:#book_object_title_or_description#:#Title/Description book#:#book_object_updated#:#Bookable item updated. book#:#book_objects_available#:#Items available %s book#:#book_open#:#Open Booking Pool book#:#book_overall_limit#:#Limit Total Number of Bookings per User book#:#book_overall_limit_warning#:#You have reached the maximum number of bookings allowed. +book#:#book_overall_limit_would_be_exceeded#:#Invalid selection. The overall limit would be exceeded. +book#:#book_participant#:#Participant book#:#book_participant_already_assigned#:#One or more participants have already been added. book#:#book_participant_assigned#:#Participant(s) added. +book#:#book_participant_removed#:#The participants and all related bookings have been removed successfully. +book#:#book_past_bookings#:#Past bookings book#:#book_period#:#Period book#:#book_pool_added#:#Booking pool successfully created. book#:#book_pool_selection#:#Booking Pool Selection @@ -2636,6 +2662,7 @@ book#:#book_pref_overview#:#Overview book#:#book_preference_info#:#Please select %1 preference(s) by %2. After this deadline, bookings will be allocated automatically, taking into consideration the preferences of all users. book#:#book_preferences#:#Preferences book#:#book_preferences_saved#:#Your preferences have been saved. +book#:#book_present_bookings#:#Present bookings book#:#book_public_log#:#Bookings Visible to All Participants book#:#book_public_log_info#:#Users with the ‘Read’ permission are able to see the bookings of the other participants in the ‘Bookings’ tab. book#:#book_recurrence#:#Recurrence @@ -2646,14 +2673,16 @@ book#:#book_reminder_day#:#Send Reminder book#:#book_reminder_day_info#:#Send list of own bookings to users and full list of all upcoming bookings to admins. Please note: in order to receive reminders, users need to have activated notifications via the ‘Actions’ menu at the top right-hand corner of the booking pool. book#:#book_reminder_days#:#day(s) before time-slot of booking book#:#book_reminder_setting#:#Reminder +book#:#book_remove_participants#:#Remove Participant(s) book#:#book_rerun_assignments#:#Run Allocation Process book#:#book_rerun_confirmation#:#Attention. The process of allocating bookings according to preferences has already taken place. You may restart the process if any errors have occurred, e.g. no bookings have been saved. To prevent multiple allocations, please delete all existing bookings before restarting the process. book#:#book_reservation_available#:#%s available +book#:#book_reservation_cancelled#:#Reservation(s) cancelled. book#:#book_reservation_confirmed#:#Your booking has been confirmed. book#:#book_reservation_failed#:#It was not possible to confirm your booking. book#:#book_reservation_failed_overbooked#:#Your booking failed because the bookable item in question is no longer available. -book#:#book_reservation_filter_period#:#Default Value for ‘Date Range’ Filter -book#:#book_reservation_filter_period_info#:#The default value for the ‘Date Range’ filter in the ‘Bookings’ tab (in days from the current date). +book#:#book_reservation_filter_period#:#Default Value for ’Period’ Filter +book#:#book_reservation_filter_period_info#:#The default value for the ‘Period’ filter in the ‘Bookings’ and 'Bookable Items' tab (in days from the current date). With a value '0' no filter is set. book#:#book_reservation_fix_info#:#Bookings can only be made for the time slots shown as available below. book#:#book_reservation_overview#:#Bookings Overview book#:#book_reservation_status_5#:#Cancelled @@ -2681,17 +2710,28 @@ book#:#book_select_pool#:#Select Pool book#:#book_set_cancel#:#Cancel Booking book#:#book_set_delete#:#Delete Booking book#:#book_show_message#:#Show Message +book#:#book_show_past_bookings#:#Show past bookings +book#:#book_some_reservations_unavailable#:#Some of the selected items could not be booked because they are no longer available. +book#:#book_table#:#Table +book#:#book_table_col_availability#:#Availability +book#:#book_table_col_datetime#:#Date/Time book#:#book_title_description_nr#:#Title; Description; Number of Units book#:#book_title_description_nr_info#:#Enter title, description and number of units separated by semicolon or TAB character (if importing from spreadsheet software). Use one line per item. book#:#book_too_many_preferences#:#You have selected too many preferences. Your preferences have not been saved. book#:#book_total_individual_bookings_limit#:#Limit the number of bookings that can be carried out by/for individual participants in total, across all available items. Each bookable item can still only be booked once per participant. book#:#book_type_warning#:#There are currently no bookable items available. Please create some bookable items to be able to use this booking pool. +book#:#book_view#:#View book#:#book_week#:#Week +book#:#book_week_no_objects_selected#:#Select at least one Bookable Item at the ‘Item Selection’ section on the right and click the ‘Refresh’ button. book#:#book_your_bookings#:#Your Bookings book#:#book_your_preferences#:#Your Preferences book#:#book_your_reservations#:#Your Bookings book#:#booking_multiple_succesfully#:#Bookings successfully carried out. book#:#booking_nr_of_items#:#Number of Items +book#:#booking_process_error#:#There was an error while processing your booking(s). Please try again. +book#:#bookings_log#:#Bookings +book#:#delete_bookable_item#:#Delete Bookable Item +book#:#no_valid_selection#:#No valid selection book#:#obj_book_duplicate#:#Copy Booking Pool book#:#participants#:#Participants book#:#reservation_deleted#:#Booking deleted. From fb6154d50845fed337ee62fb6050955009521c1d Mon Sep 17 00:00:00 2001 From: Matheus Zych Date: Fri, 29 May 2026 11:29:57 +0200 Subject: [PATCH 3/8] BookingPool: Removes Booking Pool legacy templates See: https://docu.ilias.de/go/wiki/wpage_7320_1357 Removes superfluous twig templates used by the legacy Booking Pool tables. --- .../default/tpl.booking_object_row.html | 26 ------------------- .../default/tpl.booking_participant_row.html | 15 ----------- .../default/tpl.booking_schedule_row.html | 11 -------- 3 files changed, 52 deletions(-) delete mode 100755 components/ILIAS/BookingManager/templates/default/tpl.booking_object_row.html delete mode 100755 components/ILIAS/BookingManager/templates/default/tpl.booking_participant_row.html delete mode 100755 components/ILIAS/BookingManager/templates/default/tpl.booking_schedule_row.html diff --git a/components/ILIAS/BookingManager/templates/default/tpl.booking_object_row.html b/components/ILIAS/BookingManager/templates/default/tpl.booking_object_row.html deleted file mode 100755 index 2dd5a50dfc2a..000000000000 --- a/components/ILIAS/BookingManager/templates/default/tpl.booking_object_row.html +++ /dev/null @@ -1,26 +0,0 @@ - - - {TXT_TITLE} - -
({NOT_YET})
- - - - - {TXT_DESC} - - - - - {ADVMD_VAL} - - - - - {VALUE_AVAIL}/{VALUE_AVAIL_ALL} - - - - {ACTION_DROPDOWN} - - diff --git a/components/ILIAS/BookingManager/templates/default/tpl.booking_participant_row.html b/components/ILIAS/BookingManager/templates/default/tpl.booking_participant_row.html deleted file mode 100755 index 01fb3a062911..000000000000 --- a/components/ILIAS/BookingManager/templates/default/tpl.booking_participant_row.html +++ /dev/null @@ -1,15 +0,0 @@ - - - {TXT_NAME} - - - -

{TXT_OBJECT}

- - - - -

{TXT_ACTION}

- - - diff --git a/components/ILIAS/BookingManager/templates/default/tpl.booking_schedule_row.html b/components/ILIAS/BookingManager/templates/default/tpl.booking_schedule_row.html deleted file mode 100755 index 4db2afe30b77..000000000000 --- a/components/ILIAS/BookingManager/templates/default/tpl.booking_schedule_row.html +++ /dev/null @@ -1,11 +0,0 @@ - - - {TXT_TITLE} - - - {TXT_IS_USED} - - - {LAYER} - - From c4521c7e430ac642f9f804b7a04ea71248ddf500 Mon Sep 17 00:00:00 2001 From: Matheus Zych Date: Fri, 29 May 2026 11:32:39 +0200 Subject: [PATCH 4/8] BookingPool: Adds Booking Pool Base changes See: https://docu.ilias.de/go/wiki/wpage_7320_1357 Adds changes to the base classes of the Booking Pool to work correctly with the new Tables. --- .../BookingManager/Access/AccessManager.php | 15 + .../class.ObjectSelectionListGUI.php | 10 + .../class.ilBookingProcessWithScheduleGUI.php | 17 +- .../Objects/class.ilBookingObject.php | 35 +- .../Objects/class.ilBookingObjectGUI.php | 575 +++++++------- .../class.ilBookingObjectsTableGUI.php | 455 ----------- .../class.ilBookingParticipantGUI.php | 228 +++--- .../class.ilBookingParticipantsTableGUI.php | 200 ----- .../class.ilBookingPreferencesGUI.php | 37 +- .../class.ilBookingPreferencesManager.php | 13 +- .../class.ilBookingReservation.php | 7 +- .../class.ilBookingReservationsGUI.php | 738 ++++-------------- .../class.ilBookingReservationsTableGUI.php | 11 +- .../Schedule/class.ilBookingScheduleGUI.php | 372 ++++----- .../class.ilBookingSchedulesTableGUI.php | 119 --- .../classes/class.ilObjBookingPoolGUI.php | 73 +- 16 files changed, 842 insertions(+), 2063 deletions(-) delete mode 100755 components/ILIAS/BookingManager/Objects/class.ilBookingObjectsTableGUI.php delete mode 100755 components/ILIAS/BookingManager/Participants/class.ilBookingParticipantsTableGUI.php delete mode 100755 components/ILIAS/BookingManager/Schedule/class.ilBookingSchedulesTableGUI.php diff --git a/components/ILIAS/BookingManager/Access/AccessManager.php b/components/ILIAS/BookingManager/Access/AccessManager.php index 627c649503b1..48cdbfdf9b70 100644 --- a/components/ILIAS/BookingManager/Access/AccessManager.php +++ b/components/ILIAS/BookingManager/Access/AccessManager.php @@ -21,6 +21,7 @@ namespace ILIAS\BookingManager\Access; use ILIAS\BookingManager\InternalDomainService; +use ilObjBookingPool; class AccessManager { @@ -184,4 +185,18 @@ public function validateScheduleId(int $schedule_id, int $pool_id): void } } + public function canRead(int $ref_id, int $current_user = 0): bool + { + return $this->access->checkAccessOfUser( + $this->getCurrentUserId($current_user), + "read", + "", + $ref_id + ); + } + + public function canReadPublicLog(int $ref_id, int $current_user = 0): bool + { + return $this->canRead($ref_id, $current_user) && (new ilObjBookingPool($ref_id, true))->hasPublicLog(); + } } diff --git a/components/ILIAS/BookingManager/BookingProcess/class.ObjectSelectionListGUI.php b/components/ILIAS/BookingManager/BookingProcess/class.ObjectSelectionListGUI.php index d9e82f6934fb..63f15af37b65 100755 --- a/components/ILIAS/BookingManager/BookingProcess/class.ObjectSelectionListGUI.php +++ b/components/ILIAS/BookingManager/BookingProcess/class.ObjectSelectionListGUI.php @@ -20,6 +20,8 @@ namespace ILIAS\BookingManager\BookingProcess; +use ilGlobalTemplateInterface; + /** * @author Alexander Killing */ @@ -51,6 +53,14 @@ public function render(): string $tpl = new \ilTemplate("tpl.obj_selection.html", true, true, "components/ILIAS/BookingManager/BookingProcess"); $selected = $this->object_selection->getSelectedObjects(); + + if ($selected === []) { + $this->ui->mainTemplate()->setOnScreenMessage( + ilGlobalTemplateInterface::MESSAGE_TYPE_INFO, + $this->lng->txt("book_week_no_objects_selected") + ); + } + foreach ($this->object_manager->getObjectTitles() as $id => $title) { $tpl->setCurrentBlock("item"); if (in_array($id, $selected)) { diff --git a/components/ILIAS/BookingManager/BookingProcess/class.ilBookingProcessWithScheduleGUI.php b/components/ILIAS/BookingManager/BookingProcess/class.ilBookingProcessWithScheduleGUI.php index 16021d7f8446..79fe039b102f 100755 --- a/components/ILIAS/BookingManager/BookingProcess/class.ilBookingProcessWithScheduleGUI.php +++ b/components/ILIAS/BookingManager/BookingProcess/class.ilBookingProcessWithScheduleGUI.php @@ -171,14 +171,15 @@ public function week(): void // ok ); $tpl->setContent($week_gui->getHTML()); - $bar = $this->gui->toolbar(); - $list_link = $this->ctrl->getLinkTargetByClass("ilObjBookingPoolGUI", "render"); - $week_link = $this->ctrl->getLinkTargetByClass("ilBookingProcessWithScheduleGUI", "week"); - $mode_control = $this->gui->ui()->factory()->viewControl()->mode([ - $this->lng->txt("book_list") => $list_link, - $this->lng->txt("book_week") => $week_link - ], $this->lng->txt("book_view"))->withActive($this->lng->txt("book_week")); - $bar->addComponent($mode_control); + $book_week = $this->lng->txt('book_week'); + $mode_control = $this->gui->ui()->factory()->viewControl()->mode( + [ + $this->lng->txt('book_table') => $this->ctrl->getLinkTargetByClass(ilObjBookingPoolGUI::class, 'render'), + $book_week => $this->ctrl->getLinkTarget($this, 'week') + ], + $this->lng->txt('book_view') + )->withActive($book_week); + $this->gui->toolbar()->addComponent($mode_control); $list_gui = new \ILIAS\BookingManager\BookingProcess\ObjectSelectionListGUI( $this->pool->getId(), diff --git a/components/ILIAS/BookingManager/Objects/class.ilBookingObject.php b/components/ILIAS/BookingManager/Objects/class.ilBookingObject.php index 1611d29dab0e..87b8e800c149 100755 --- a/components/ILIAS/BookingManager/Objects/class.ilBookingObject.php +++ b/components/ILIAS/BookingManager/Objects/class.ilBookingObject.php @@ -165,22 +165,29 @@ public function deleteFiles(): void protected function read(): void { - $ilDB = $this->db; + if (!$this->id) { + return; + } - if ($this->id) { - $set = $ilDB->query('SELECT *' . - ' FROM booking_object' . - ' WHERE booking_object_id = ' . $ilDB->quote($this->id, 'integer')); - $row = $ilDB->fetchAssoc($set); - $this->setTitle((string) $row['title']); - $this->setDescription((string) $row['description']); - $this->setPoolId((int) $row['pool_id']); - $this->setScheduleId($row['schedule_id']); - $this->setNrOfItems((int) $row['nr_items']); - $this->setFile((string) $row['info_file']); - $this->setPostText((string) $row['post_text']); - $this->setPostFile((string) $row['post_file']); + $set = $this->db->queryF( + 'SELECT * FROM booking_object WHERE booking_object_id = %s', + [ilDBConstants::T_INTEGER], + [$this->id] + ); + $row = $this->db->fetchAssoc($set); + + if ($row === null || $row === []) { + return; } + + $this->setTitle((string) $row['title']); + $this->setDescription((string) $row['description']); + $this->setPoolId((int) $row['pool_id']); + $this->setScheduleId($row['schedule_id']); + $this->setNrOfItems((int) $row['nr_items']); + $this->setFile((string) $row['info_file']); + $this->setPostText((string) $row['post_text']); + $this->setPostFile((string) $row['post_file']); } /** diff --git a/components/ILIAS/BookingManager/Objects/class.ilBookingObjectGUI.php b/components/ILIAS/BookingManager/Objects/class.ilBookingObjectGUI.php index 0b858bfc4d0f..4f5d3bcf13d5 100755 --- a/components/ILIAS/BookingManager/Objects/class.ilBookingObjectGUI.php +++ b/components/ILIAS/BookingManager/Objects/class.ilBookingObjectGUI.php @@ -16,59 +16,89 @@ * *********************************************************************/ +use ILIAS\BookingManager\Access\AccessManager; +use ILIAS\BookingManager\BookableItem\Table\BookableItemTable; +use ILIAS\BookingManager\BookableItem\Table\BookableItemWithoutScheduleTable; +use ILIAS\BookingManager\BookableItem\Table\BookableItemWithScheduleTable; +use ILIAS\BookingManager\BookingProcess\BookingProcessManager; +use ILIAS\BookingManager\Common\HttpService; +use ILIAS\BookingManager\InternalGUIService; +use ILIAS\BookingManager\Objects\ObjectsManager; +use ILIAS\BookingManager\Settings\Settings; +use ILIAS\BookingManager\Schedule\ScheduleManager; +use ILIAS\BookingManager\StandardGUIRequest; +use ILIAS\Data\Factory as DataFactory; +use ILIAS\Refinery\Factory as Refinery; +use ILIAS\UI\Factory; +use ILIAS\UI\Renderer; +use ILIAS\UI\URLBuilder; +use ILIAS\UI\Component\Modal\Modal; + /** - * @author Jörg Lützenkirchen * @ilCtrl_Calls ilBookingObjectGUI: ilPropertyFormGUI, ilBookingProcessWithScheduleGUI, ilBookingProcessWithoutScheduleGUI * @ilCtrl_Calls ilBookingObjectGUI: ilBookBulkCreationGUI */ class ilBookingObjectGUI { - protected \ILIAS\BookingManager\Objects\ObjectsManager $objects_manager; - protected \ILIAS\BookingManager\Schedule\ScheduleManager $schedule_manager; + protected ObjectsManager $objects_manager; + protected ScheduleManager $schedule_manager; protected ilBookBulkCreationGUI $bulk_creation_gui; protected ilObjBookingPool $pool; - protected \ILIAS\BookingManager\InternalGUIService $gui; - protected \ILIAS\BookingManager\Access\AccessManager $access; - protected \ILIAS\BookingManager\StandardGUIRequest $book_request; + protected InternalGUIService $gui; + protected AccessManager $access; + protected StandardGUIRequest $book_request; protected ilCtrl $ctrl; protected ilGlobalTemplateInterface $tpl; protected ilLanguage $lng; protected ilTabsGUI $tabs; - protected ilBookingHelpAdapter $help; protected ilObjectDataCache $obj_data_cache; protected ilObjUser $user; + protected Factory $ui_factory; + protected Renderer $ui_renderer; + protected ilUIService $ui_service; + protected Refinery $refinery; + protected HttpService $http; + protected BookingProcessManager $process_manager; + protected DataFactory $data_factory; + protected Settings $settings; protected bool $pool_has_schedule; protected ?int $pool_overall_limit; protected bool $pool_uses_preferences = false; // Is management of objects (create/edit/delete) activated? protected bool $management = true; - // Context object id (e.g. course with booking service activated) - protected int $context_obj_id; protected int $object_id; - protected string $seed; - protected string $sseed; - protected ilObjBookingPoolGUI $pool_gui; protected array $rsv_ids = []; - protected ilAdvancedMDRecordGUI $record_gui; + protected ?ilAdvancedMDRecordGUI $record_gui = null; protected int $ref_id; public function __construct( - ilObjBookingPoolGUI $a_parent_obj, - string $seed, - string $sseed, - ilBookingHelpAdapter $help, - int $context_obj_id = 0 + protected ilObjBookingPoolGUI $a_parent_obj, + protected string $seed, + protected string $sseed, + protected ilBookingHelpAdapter $help, + // Context object id (e.g. course with booking service activated) + protected int $context_obj_id = 0 ) { global $DIC; $this->ctrl = $DIC->ctrl(); - $this->tpl = $DIC["tpl"]; + $this->tpl = $DIC->ui()->mainTemplate(); $this->lng = $DIC->language(); - $this->access = $DIC->bookingManager()->internal()->domain()->access(); + $this->access = $DIC + ->bookingManager() + ->internal() + ->domain() + ->access(); $this->tabs = $DIC->tabs(); - $this->help = $help; - $this->obj_data_cache = $DIC["ilObjDataCache"]; + $this->obj_data_cache = $DIC['ilObjDataCache']; $this->user = $DIC->user(); + $this->ui_factory = $DIC->ui()->factory(); + $this->ui_renderer = $DIC->ui()->renderer(); + $this->ui_service = $DIC->uiService(); + $this->refinery = $DIC->refinery(); + $this->http = new HttpService($DIC->http(), $this->refinery); + $this->process_manager = $DIC->bookingManager()->internal()->domain()->process(); + $this->data_factory = new DataFactory(); /** @var ilObjBookingPool $pool */ $pool = $a_parent_obj->getObject(); @@ -85,35 +115,32 @@ public function __construct( ->internal() ->domain() ->schedules($this->pool->getId()); + $this->settings = $DIC + ->bookingManager() + ->internal() + ->domain() + ->bookingSettings() + ->getByObjId($this->pool->getId()); - $this->seed = $seed; - $this->sseed = $sseed; - - $this->context_obj_id = $context_obj_id; - - $this->pool_gui = $a_parent_obj; - $this->bulk_creation_gui = $this->gui->objects() - ->ilBookBulkCreationGUI($this->pool); + $this->bulk_creation_gui = $this->gui->objects()->ilBookBulkCreationGUI($this->pool); - $this->pool_has_schedule = - ($a_parent_obj->getObject()->getScheduleType() === ilObjBookingPool::TYPE_FIX_SCHEDULE); - $this->pool_uses_preferences = - ($a_parent_obj->getObject()->getScheduleType() === ilObjBookingPool::TYPE_NO_SCHEDULE_PREFERENCES); - $this->pool_overall_limit = $this->pool_has_schedule - ? null - : $a_parent_obj->getObject()->getOverallLimit(); + $schedule_type = $a_parent_obj->getObject()->getScheduleType(); + $this->pool_has_schedule = $schedule_type === ilObjBookingPool::TYPE_FIX_SCHEDULE; + $this->pool_uses_preferences = $schedule_type === ilObjBookingPool::TYPE_NO_SCHEDULE_PREFERENCES; + $this->pool_overall_limit = !$this->pool_has_schedule ? $a_parent_obj->getObject()->getOverallLimit() : null; $this->object_id = $this->book_request->getObjectId(); $this->ref_id = $this->book_request->getRefId(); - $this->ctrl->saveParameter($this, "object_id"); + $this->ctrl->saveParameter($this, 'object_id'); $this->rsv_ids = array_map('intval', $this->book_request->getReservationIdsFromString()); - $this->objects_manager = $DIC->bookingManager()->internal()->domain()->objects($this->pool->getId()); + $this->objects_manager = $DIC + ->bookingManager() + ->internal() + ->domain() + ->objects($this->pool->getId()); - $this->access->validateBookingObjId( - $this->object_id, - (int) $this->pool_gui->getObject()?->getId() - ); + $this->access->validateBookingObjId($this->object_id, (int) $a_parent_obj->getObject()?->getId()); } public function activateManagement(bool $a_val): void @@ -121,9 +148,6 @@ public function activateManagement(bool $a_val): void $this->management = $a_val; } - /** - * Is management activated? - */ public function isManagementActivated(): bool { return $this->management; @@ -131,30 +155,22 @@ public function isManagementActivated(): bool protected function getPoolRefId(): int { - return $this->pool_gui->getRefId(); + return $this->a_parent_obj->getRefId(); } protected function getPoolObjId(): int { - return $this->pool_gui->getObject()->getId(); + return $this->pool->getId(); } - /** - * Has booking pool a schedule? - */ protected function hasPoolSchedule(): bool { - return ($this->pool_gui->getObject()->getScheduleType() === ilObjBookingPool::TYPE_FIX_SCHEDULE); + return $this->pool_has_schedule; } - /** - * Get booking pool overall limit - */ protected function getPoolOverallLimit(): ?int { - return $this->hasPoolSchedule() - ? null - : $this->pool_gui->getObject()->getOverallLimit(); + return $this->pool_overall_limit; } /** @@ -162,27 +178,16 @@ protected function getPoolOverallLimit(): ?int */ public function executeCommand(): void { - $ilCtrl = $this->ctrl; - - $next_class = $ilCtrl->getNextClass($this); - - switch ($next_class) { - case "ilpropertyformgui": + switch (strtolower($this->ctrl->getNextClass($this))) { + case strtolower(ilPropertyFormGUI::class): // only case is currently adv metadata internal link in info settings, see #24497 - $form = $this->initForm(); - $this->ctrl->forwardCommand($form); + $this->ctrl->forwardCommand($this->initForm()); break; - case "ilbookingprocesswithschedulegui": - if (!$this->pool_uses_preferences) { - $ilCtrl->setReturn($this, "render"); - } else { - $ilCtrl->setReturn($this, "returnToPreferences"); - } - /** @var ilObjBookingPool $pool */ - $pool = $this->pool_gui->getObject(); + case strtolower(ilBookingProcessWithScheduleGUI::class): + $this->ctrl->setReturn($this, $this->pool_uses_preferences ? 'returnToPreferences' : 'render'); $process_gui = $this->gui->process()->ilBookingProcessWithScheduleGUI( - $pool, + $this->pool, $this->object_id, $this->context_obj_id, $this->seed ?? $this->sseed @@ -190,16 +195,10 @@ public function executeCommand(): void $this->ctrl->forwardCommand($process_gui); break; - case "ilbookingprocesswithoutschedulegui": - if (!$this->pool_uses_preferences) { - $ilCtrl->setReturn($this, "render"); - } else { - $ilCtrl->setReturn($this, "returnToPreferences"); - } - /** @var ilObjBookingPool $pool */ - $pool = $this->pool_gui->getObject(); + case strtolower(ilBookingProcessWithoutScheduleGUI::class): + $this->ctrl->setReturn($this, $this->pool_uses_preferences ? 'returnToPreferences' : 'render'); $process_gui = $this->gui->process()->ilBookingProcessWithoutScheduleGUI( - $pool, + $this->pool, $this->object_id, $this->context_obj_id ); @@ -207,131 +206,157 @@ public function executeCommand(): void break; case strtolower(ilBookBulkCreationGUI::class): - $this->ctrl->setReturn($this, ""); + $this->ctrl->setReturn($this, ''); $this->ctrl->forwardCommand($this->bulk_creation_gui); break; default: - $cmd = $ilCtrl->getCmd("render"); + $cmd = $this->ctrl->getCmd('render'); $this->$cmd(); break; } } - protected function showNoScheduleMessage(): void - { - $this->pool_gui->showNoScheduleMessage(); - } - protected function returnToPreferences(): void { - $this->ctrl->redirectByClass("ilBookingPreferencesGUI"); + $this->ctrl->redirectByClass(ilBookingPreferencesGUI::class); } - /** - * Render list of booking objects - * uses ilBookingObjectsTableGUI - */ - public function render(): void + public function render(?Modal $modal = null): void { - $this->showNoScheduleMessage(); - - $tpl = $this->tpl; - $ilCtrl = $this->ctrl; - $lng = $this->lng; + $this->a_parent_obj->showNoScheduleMessage(); - $bar = ""; + $bar = ''; - if ($this->isManagementActivated() && $this->access->canManageObjects($this->getPoolRefId())) { + if ($this->isManagementActivated()) { $bar = new ilToolbarGUI(); - $bar->addButton($lng->txt('book_add_object'), $ilCtrl->getLinkTarget($this, 'create')); + if ($this->access->canManageObjects($this->getPoolRefId())) { + $bar->addButton($this->lng->txt('book_add_object'), $this->ctrl->getLinkTarget($this, 'create')); - // bulk creation - $this->bulk_creation_gui->modifyToolbar($bar); + // bulk creation + $this->bulk_creation_gui->modifyToolbar($bar); + } - if ($this->hasPoolSchedule()) { + if ($bar->getItems() !== []) { $bar->addSeparator(); - $list_link = $this->ctrl->getLinkTarget($this, ""); - $week_link = $this->ctrl->getLinkTargetByClass("ilBookingProcessWithScheduleGUI", "week"); - $mode_control = $this->gui->ui()->factory()->viewControl()->mode([ - $this->lng->txt("book_list") => $list_link, - $this->lng->txt("book_week") => $week_link - ], $this->lng->txt("book_view")); + } + + if ($this->hasPoolSchedule()) { + $mode_control = $this->gui->ui()->factory()->viewControl()->mode( + [ + $this->lng->txt('book_table') => $this->ctrl->getLinkTarget($this, ''), + $this->lng->txt('book_week') => $this->ctrl->getLinkTargetByClass(ilBookingProcessWithScheduleGUI::class, 'week') + ], + $this->lng->txt('book_view') + ); $bar->addComponent($mode_control); } + $bar = $bar->getHTML(); } - $tpl->setPermanentLink('book', $this->getPoolRefId()); + $components = $this->getTableComponents(); + if ($modal !== null) { + $components[] = $modal; + } + + $this->tpl->setPermanentLink('book', $this->getPoolRefId()); + $this->tpl->setContent($bar . $this->ui_renderer->render($components)); + } + + private function getTable(): BookableItemTable + { + if ($this->hasPoolSchedule()) { + return new BookableItemWithScheduleTable( + $this->ui_factory, + $this->ui_renderer, + $this->lng, + $this->http, + $this->ui_service, + $this->ctrl, + $this->tpl, + $this->refinery, + $this->access, + $this->pool, + $this->process_manager, + $this->settings, + $this->user, + $this->getPoolRefId(), + $this->isManagementActivated(), + $this->context_obj_id, + ); + } - $table = new ilBookingObjectsTableGUI($this, 'render', $this->getPoolRefId(), $this->getPoolObjId(), $this->hasPoolSchedule(), $this->getPoolOverallLimit(), $this->isManagementActivated()); - $tpl->setContent($bar . $table->getHTML()); + return new BookableItemWithoutScheduleTable( + $this->ui_factory, + $this->ui_renderer, + $this->lng, + $this->http, + $this->ui_service, + $this->ctrl, + $this->tpl, + $this->refinery, + $this->access, + $this->pool, + $this->process_manager, + $this->settings, + $this->user, + $this->getPoolRefId(), + $this->isManagementActivated(), + $this->context_obj_id, + ); } - public function applyFilter(): void + private function getTableComponents(): array { - $table = new ilBookingObjectsTableGUI($this, 'render', $this->getPoolRefId(), $this->getPoolObjId(), $this->hasPoolSchedule(), $this->getPoolOverallLimit(), $this->isManagementActivated()); - $table->resetOffset(); - $table->writeFilterToSession(); - $this->render(); + return $this->getTable()->getComponents($this->getURLBuilder()); } - public function resetFilter(): void + public function executeTableAction(): void { - $table = new ilBookingObjectsTableGUI($this, 'render', $this->getPoolRefId(), $this->getPoolObjId(), $this->hasPoolSchedule(), $this->getPoolOverallLimit(), $this->isManagementActivated()); - $table->resetOffset(); - $table->resetFilter(); - $this->render(); + $modal = $this->getTable()->execute($this->getURLBuilder()); + if ($modal !== null) { + $this->render($modal); + return; + } + $this->ctrl->redirectByClass(self::class, 'render'); + } + + private function getURLBuilder(): URLBuilder + { + return new URLBuilder( + $this->data_factory->uri(ILIAS_HTTP_PATH . "/{$this->ctrl->getLinkTarget($this, 'executeTableAction')}") + ); } - /** - * Render creation form - */ public function create(?ilPropertyFormGUI $a_form = null): void { if (!$this->access->canManageObjects($this->ref_id)) { return; } - $ilCtrl = $this->ctrl; - $tpl = $this->tpl; - $lng = $this->lng; - $ilTabs = $this->tabs; - - $ilTabs->clearTargets(); - $ilTabs->setBackTarget($lng->txt('book_back_to_list'), $ilCtrl->getLinkTarget($this, 'render')); + $this->tabs->clearTargets(); + $this->tabs->setBackTarget($this->lng->txt('book_back_to_list'), $this->ctrl->getLinkTarget($this, 'render')); $this->setHelpId('create'); - if (!$a_form) { - $a_form = $this->initForm(); - } - $tpl->setContent($a_form->getHTML()); + $a_form ??= $this->initForm(); + $this->tpl->setContent($a_form->getHTML()); } - /** - * Render edit form - */ public function edit(?ilPropertyFormGUI $a_form = null): void { if (!$this->access->canManageObjects($this->ref_id)) { return; } - $tpl = $this->tpl; - $ilCtrl = $this->ctrl; - $ilTabs = $this->tabs; - $lng = $this->lng; - - $ilTabs->clearTargets(); - $ilTabs->setBackTarget($lng->txt('book_back_to_list'), $ilCtrl->getLinkTarget($this, 'render')); + $this->tabs->clearTargets(); + $this->tabs->setBackTarget($this->lng->txt('book_back_to_list'), $this->ctrl->getLinkTarget($this, 'render')); $this->setHelpId('edit'); - if (!$a_form) { - $a_form = $this->initForm('edit', $this->object_id); - } - $tpl->setContent($a_form->getHTML()); + $a_form ??= $this->initForm('edit', $this->object_id); + $this->tpl->setContent($a_form->getHTML()); } protected function setHelpId(string $a_id): void @@ -339,80 +364,72 @@ protected function setHelpId(string $a_id): void $this->help->setHelpId($a_id); } - /** - * Build property form - */ - public function initForm( - string $a_mode = "create", - ?int $id = null - ): ilPropertyFormGUI { - $lng = $this->lng; - $ilCtrl = $this->ctrl; - $ilObjDataCache = $this->obj_data_cache; - + public function initForm(string $a_mode = 'create', ?int $id = null): ilPropertyFormGUI + { $form_gui = new ilPropertyFormGUI(); - $title = new ilTextInputGUI($lng->txt("title"), "title"); + $title = new ilTextInputGUI($this->lng->txt('title'), 'title'); $title->setRequired(true); $title->setSize(40); $title->setMaxLength(120); $form_gui->addItem($title); - $desc = new ilTextAreaInputGUI($lng->txt("description"), "desc"); + $desc = new ilTextAreaInputGUI($this->lng->txt('description'), 'desc'); $desc->setCols(70); $desc->setRows(15); $desc->setMaxNumOfChars(1000); $form_gui->addItem($desc); - $file = new ilFileInputGUI($lng->txt("book_additional_info_file"), "file"); + $file = new ilFileInputGUI($this->lng->txt('book_additional_info_file'), 'file'); $file->setAllowDeletion(true); $form_gui->addItem($file); - $nr = new ilNumberInputGUI($lng->txt("booking_nr_of_items"), "items"); + $nr = new ilNumberInputGUI($this->lng->txt('booking_nr_of_items'), 'items'); $nr->setRequired(true); $nr->setSize(3); $nr->setMaxLength(3); - $nr->setSuffix($lng->txt("book_booking_objects")); + $nr->setSuffix($this->lng->txt('book_booking_objects')); $form_gui->addItem($nr); + $schedule = null; if ($this->hasPoolSchedule()) { - $options = array(); - foreach ($this->schedule_manager->getScheduleList() as $schedule_id => $schedule_title) { - $options[$schedule_id] = $schedule_title; - } - $schedule = new ilSelectInputGUI($lng->txt("book_schedule"), "schedule"); + $options = array_map( + static fn(string $schedule_title): string => $schedule_title, + $this->schedule_manager->getScheduleList() + ); + $schedule = new ilSelectInputGUI($this->lng->txt('book_schedule'), 'schedule'); $schedule->setRequired(true); $schedule->setOptions($options); $form_gui->addItem($schedule); } $post = new ilFormSectionHeaderGUI(); - $post->setTitle($lng->txt("book_post_booking_information")); + $post->setTitle($this->lng->txt('book_post_booking_information')); $form_gui->addItem($post); - $pdesc = new ilTextAreaInputGUI($lng->txt("book_post_booking_text"), "post_text"); + $pdesc = new ilTextAreaInputGUI($this->lng->txt('book_post_booking_text'), 'post_text'); $pdesc->setCols(70); $pdesc->setRows(15); - $pdesc->setInfo($lng->txt("book_post_booking_text_info")); + $pdesc->setInfo($this->lng->txt('book_post_booking_text_info')); $form_gui->addItem($pdesc); - $pfile = new ilFileInputGUI($lng->txt("book_post_booking_file"), "post_file"); + $pfile = new ilFileInputGUI($this->lng->txt('book_post_booking_file'), 'post_file'); $pfile->setAllowDeletion(true); $form_gui->addItem($pfile); // #18214 - should also work for new objects $this->record_gui = new ilAdvancedMDRecordGUI( ilAdvancedMDRecordGUI::MODE_EDITOR, - "book", + 'book', $this->getPoolObjId(), - "bobj", + 'bobj', (int) $id ); $this->record_gui->setPropertyForm($form_gui); $this->record_gui->parse(); - if ($a_mode === "edit") { - $form_gui->setTitle($lng->txt("book_edit_object")); + if ($a_mode === 'edit') { + $form_gui->setTitle($this->lng->txt('book_edit_object')); $item = new ilHiddenInputGUI('object_id'); $item->setValue($id); @@ -426,135 +443,89 @@ public function initForm( $file->setValue($this->objects_manager->getObjectInfoFilename($id)); $pfile->setValue($this->objects_manager->getBookingInfoFilename($id)); - if (isset($schedule)) { - $schedule->setValue($obj->getScheduleId()); - } + $schedule?->setValue($obj->getScheduleId()); - $form_gui->addCommandButton("update", $lng->txt("save")); + $form_gui->addCommandButton('update', $this->lng->txt('save')); } else { - $form_gui->setTitle($lng->txt("book_add_object")); - $form_gui->addCommandButton("save", $lng->txt("save")); - $form_gui->addCommandButton("render", $lng->txt("cancel")); + $form_gui->setTitle($this->lng->txt('book_add_object')); + $form_gui->addCommandButton('save', $this->lng->txt('save')); + $form_gui->addCommandButton('render', $this->lng->txt('cancel')); } - $form_gui->setFormAction($ilCtrl->getFormAction($this)); + $form_gui->setFormAction($this->ctrl->getFormAction($this)); return $form_gui; } public function save(): void + { + $this->handleForm(true); + } + + public function update(): void + { + $this->handleForm(false); + } + + private function handleForm(bool $create): void { if (!$this->access->canManageObjects($this->ref_id)) { return; } - $ilCtrl = $this->ctrl; - $lng = $this->lng; + $form = $create ? $this->initForm() : $this->initForm('edit', $this->object_id); + + if ( + $form->checkInput() + && ( + !$this->record_gui instanceof ilAdvancedMDRecordGUI + || $this->record_gui->importEditFormPostValues() + ) + ) { + $obj = new ilBookingObject($this->object_id); + $obj->setTitle($form->getInput('title')); + $obj->setDescription($form->getInput('desc')); + $obj->setNrOfItems($form->getInput('items')); + $obj->setPostText($form->getInput('post_text')); - $form = $this->initForm(); - if ($form->checkInput()) { - $valid = true; - if ($this->record_gui && - !$this->record_gui->importEditFormPostValues()) { - $valid = false; + if ($this->hasPoolSchedule()) { + $obj->setScheduleId($form->getInput('schedule')); } - if ($valid) { - $obj = new ilBookingObject(); - $obj->setPoolId($this->getPoolObjId()); - $obj->setTitle($form->getInput("title")); - $obj->setDescription($form->getInput("desc")); - $obj->setNrOfItems($form->getInput("items")); - $obj->setPostText($form->getInput("post_text")); - - if ($this->hasPoolSchedule()) { - $obj->setScheduleId($form->getInput("schedule")); - } + if ($create) { + $obj->setPoolId($this->getPoolObjId()); $obj->save(); - - $file = $form->getItemByPostVar("file"); - if ($_FILES["file"]["tmp_name"]) { - $this->objects_manager->importObjectInfoFromLegacyUpload($obj->getId(), $_FILES["file"]); - } elseif ($file !== null && $file->getDeletionFlag()) { - $this->objects_manager->deleteObjectInfo($obj->getId()); - } - - $pfile = $form->getItemByPostVar("post_file"); - if ($_FILES["post_file"]["tmp_name"]) { - $this->objects_manager->importBookingInfoFromLegacyUpload($obj->getId(), $_FILES["post_file"]); - } elseif ($pfile !== null && $pfile->getDeletionFlag()) { - $this->objects_manager->deleteBookingInfo($obj->getId()); - } - + } else { $obj->update(); - - if ($this->record_gui) { - $this->record_gui->writeEditForm(null, $obj->getId()); - } - - $this->tpl->setOnScreenMessage('success', $lng->txt("book_object_added"), true); - $ilCtrl->redirect($this, "render"); } - } - - $form->setValuesByPost(); - $this->create($form); - } - public function update(): void - { - if (!$this->access->canManageObjects($this->ref_id)) { - return; - } - - $lng = $this->lng; - $ilCtrl = $this->ctrl; + if ($_FILES['file']['tmp_name']) { + $this->objects_manager->importObjectInfoFromLegacyUpload($obj->getId(), $_FILES['file']); + } elseif ($form->getItemByPostVar('file')?->getDeletionFlag()) { + $this->objects_manager->deleteObjectInfo($obj->getId()); + } - $form = $this->initForm('edit', $this->object_id); - if ($form->checkInput()) { - $valid = true; - if ($this->record_gui && - !$this->record_gui->importEditFormPostValues()) { - $valid = false; + if ($_FILES['post_file']['tmp_name']) { + $this->objects_manager->importBookingInfoFromLegacyUpload($obj->getId(), $_FILES['post_file']); + } elseif ($form->getItemByPostVar('post_file')?->getDeletionFlag()) { + $this->objects_manager->deleteBookingInfo($obj->getId()); } - if ($valid) { - $obj = new ilBookingObject($this->object_id); - $obj->setTitle($form->getInput("title")); - $obj->setDescription($form->getInput("desc")); - $obj->setNrOfItems($form->getInput("items")); - $obj->setPostText($form->getInput("post_text")); - - $file = $form->getItemByPostVar("file"); - if ($_FILES["file"]["tmp_name"]) { - $this->objects_manager->importObjectInfoFromLegacyUpload($obj->getId(), $_FILES["file"]); - } elseif ($file !== null && $file->getDeletionFlag()) { - $this->objects_manager->deleteObjectInfo($obj->getId()); - } - - $pfile = $form->getItemByPostVar("post_file"); - if ($_FILES["post_file"]["tmp_name"]) { - $this->objects_manager->importBookingInfoFromLegacyUpload($obj->getId(), $_FILES["post_file"]); - } elseif ($pfile !== null && $pfile->getDeletionFlag()) { - $this->objects_manager->deleteBookingInfo($obj->getId()); - } - - if ($this->hasPoolSchedule()) { - $obj->setScheduleId($form->getInput("schedule")); - } + $obj->update(); - $obj->update(); + $create + ? $this->record_gui?->writeEditForm(null, $obj->getId()) + : $this->record_gui?->writeEditForm(); - if ($this->record_gui) { - $this->record_gui->writeEditForm(); - } - - $this->tpl->setOnScreenMessage('success', $lng->txt("book_object_updated"), true); - $ilCtrl->redirect($this, "edit"); - } + $this->tpl->setOnScreenMessage( + 'success', + $this->lng->txt($create ? 'book_object_added' : 'book_object_updated'), + true + ); + $this->ctrl->redirect($this, $create ? 'render' : 'edit'); } $form->setValuesByPost(); - $this->edit($form); + $create ? $this->create($form) : $this->edit($form); } public function confirmDelete(): void @@ -563,24 +534,19 @@ public function confirmDelete(): void return; } - $ilCtrl = $this->ctrl; - $lng = $this->lng; - $tpl = $this->tpl; - $ilTabs = $this->tabs; - - $ilTabs->clearTargets(); - $ilTabs->setBackTarget($lng->txt('book_back_to_list'), $ilCtrl->getLinkTarget($this, 'render')); + $this->tabs->clearTargets(); + $this->tabs->setBackTarget($this->lng->txt('book_back_to_list'), $this->ctrl->getLinkTarget($this, 'render')); $conf = new ilConfirmationGUI(); - $conf->setFormAction($ilCtrl->getFormAction($this)); - $conf->setHeaderText($lng->txt('book_confirm_delete')); + $conf->setFormAction($this->ctrl->getFormAction($this)); + $conf->setHeaderText($this->lng->txt('book_confirm_delete')); $type = new ilBookingObject($this->object_id); $conf->addItem('object_id', $this->object_id, $type->getTitle()); - $conf->setConfirm($lng->txt('delete'), 'delete'); - $conf->setCancel($lng->txt('cancel'), 'render'); + $conf->setConfirm($this->lng->txt('delete'), 'delete'); + $conf->setCancel($this->lng->txt('cancel'), 'render'); - $tpl->setContent($conf->getHTML()); + $this->tpl->setContent($conf->getHTML()); } public function delete(): void @@ -589,26 +555,21 @@ public function delete(): void return; } - $ilCtrl = $this->ctrl; - $lng = $this->lng; - $obj = new ilBookingObject($this->object_id); $obj->deleteReservationsAndCalEntries($this->object_id); $obj->delete(); - $this->tpl->setOnScreenMessage('success', $lng->txt('book_object_deleted'), true); - $ilCtrl->setParameter($this, 'object_id', ""); - $ilCtrl->redirect($this, 'render'); + $this->tpl->setOnScreenMessage('success', $this->lng->txt('book_object_deleted'), true); + $this->ctrl->setParameter($this, 'object_id', ''); + $this->ctrl->redirect($this, 'render'); } - public function deliverInfo(): void { - $id = $this->object_id; - if (!$id) { + if (!$this->object_id) { return; } - $this->objects_manager->deliverObjectInfo($id); + $this->objects_manager->deliverObjectInfo($this->object_id); } } diff --git a/components/ILIAS/BookingManager/Objects/class.ilBookingObjectsTableGUI.php b/components/ILIAS/BookingManager/Objects/class.ilBookingObjectsTableGUI.php deleted file mode 100755 index 325395f01b88..000000000000 --- a/components/ILIAS/BookingManager/Objects/class.ilBookingObjectsTableGUI.php +++ /dev/null @@ -1,455 +0,0 @@ - - */ -class ilBookingObjectsTableGUI extends ilTable2GUI -{ - protected string $process_class; - protected \ILIAS\BookingManager\Access\AccessManager $access; - protected \ILIAS\UI\Renderer $ui_renderer; - protected \ILIAS\UI\Factory $ui_factory; - protected ilObjUser $user; - protected int $ref_id; - protected int $pool_id; - protected bool $has_schedule; - protected bool $may_edit; - protected bool $may_assign; - protected ?int $overall_limit = null; - protected array $reservations = array(); - protected int $current_bookings; - protected array $advmd; - protected array $filter; - protected ?ilAdvancedMDRecordGUI $record_gui = null; - protected bool $active_management; - protected \ilGlobalTemplateInterface $main_tpl; - - public function __construct( - object $a_parent_obj, - string $a_parent_cmd, - int $a_ref_id, - int $a_pool_id, - bool $a_pool_has_schedule, - ?int $a_pool_overall_limit, - bool $active_management = true - ) { - global $DIC; - $this->main_tpl = $DIC->ui()->mainTemplate(); - - $this->ctrl = $DIC->ctrl(); - $this->lng = $DIC->language(); - $this->access = $DIC->bookingManager()->internal()->domain()->access(); - $this->user = $DIC->user(); - $ilCtrl = $DIC->ctrl(); - $lng = $DIC->language(); - $ilAccess = $DIC->access(); - $this->ui_factory = $DIC->ui()->factory(); - $this->ui_renderer = $DIC->ui()->renderer(); - - $this->ref_id = $a_ref_id; - $this->pool_id = $a_pool_id; - $this->has_schedule = $a_pool_has_schedule; - $this->overall_limit = $a_pool_overall_limit; - $this->active_management = $active_management; - $this->may_edit = ($this->active_management && - $this->access->canManageObjects($this->ref_id)); - $this->may_assign = ($this->active_management && - $this->access->canManageAllReservations($this->ref_id)); - - $this->advmd = ilObjBookingPool::getAdvancedMDFields($this->ref_id); - - $this->setId("bkobj"); - - parent::__construct($a_parent_obj, $a_parent_cmd); - - $this->setTitle($lng->txt("book_booking_objects")); - - // $this->setLimit(9999); - - $this->addColumn($this->lng->txt("title"), "title"); - - $cols = $this->getSelectableColumns(); - foreach ($this->getSelectedColumns() as $col) { - $this->addColumn($cols[$col]["txt"], $col); - } - - if (!$this->has_schedule) { - $this->addColumn($this->lng->txt("available")); - } - - $this->addColumn($this->lng->txt("actions")); - - $this->setEnableHeader(true); - $this->setFormAction($ilCtrl->getFormAction($a_parent_obj, $a_parent_cmd)); - $this->setRowTemplate("tpl.booking_object_row.html", "components/ILIAS/BookingManager"); - $this->process_class = $DIC->bookingManager() - ->internal() - ->gui() - ->process() - ->getProcessClass($a_pool_has_schedule); - - $this->initFilter(); - $this->getItems(); - } - - /** - * needed for advmd filter handling - */ - protected function getAdvMDRecordGUI(): ?ilAdvancedMDRecordGUI - { - // #16827 - return $this->record_gui; - } - - public function initFilter(): void - { - $lng = $this->lng; - - // title/description - $title = $this->addFilterItemByMetaType( - "title", - ilTable2GUI::FILTER_TEXT, - false, - $lng->txt("title") . "/" . $lng->txt("description") - ); - if ($title !== null) { - $this->filter["title"] = $title->getValue(); - } - - // #18651 - if ($this->has_schedule) { - // booking period - $period = $this->addFilterItemByMetaType( - "period", - ilTable2GUI::FILTER_DATE_RANGE, - false, - $lng->txt("book_period") - ); - if ($period !== null) { - $this->filter["period"] = $period->getValue(); - } - } - } - - public function getItems(): void - { - $ilUser = $this->user; - - $data = ilBookingObject::getList($this->pool_id, $this->filter["title"]); - // check schedule availability - if ($this->has_schedule) { - $now = time(); - $limit = strtotime("+1year"); - foreach ($data as $idx => $item) { - $schedule = new ilBookingSchedule($item["schedule_id"]); - $av_from = ($schedule->getAvailabilityFrom() && !$schedule->getAvailabilityFrom()->isNull()) - ? $schedule->getAvailabilityFrom()->get(IL_CAL_UNIX) - : null; - $av_to = ($schedule->getAvailabilityTo() && !$schedule->getAvailabilityTo()->isNull()) - ? strtotime($schedule->getAvailabilityTo()->get(IL_CAL_DATE) . " 23:59:59") - : null; - if (($av_from && $av_from > $limit)) { - unset($data[$idx]); - } - if ($av_from > $now) { - $data[$idx]["not_yet"] = ilDatePresentation::formatDate(new ilDate($av_from, IL_CAL_UNIX)); - } - if ($av_to) { - // #18658 - if (!ilBookingReservation::isObjectAvailableInPeriod($item["booking_object_id"], $schedule, $av_from, $av_to)) { - $this->lng->loadLanguageModule("dateplaner"); - $data[$idx]["full_up"] = $this->lng->txt("cal_booked_out"); - } - } - } - } - - foreach ($data as $idx => $item) { - $item_id = $item["booking_object_id"]; - - // available for given period? - if (isset($this->filter["period"]["from"]) || - isset($this->filter["period"]["to"])) { - $from = is_object($this->filter["period"]["from"]) - ? strtotime($this->filter["period"]["from"]->get(IL_CAL_DATE) . " 00:00:00") - : null; - $to = is_object($this->filter["period"]["to"]) - ? strtotime($this->filter["period"]["to"]->get(IL_CAL_DATE) . " 23:59:59") - : null; - - $bobj = new ilBookingObject($item_id); - $schedule = new ilBookingSchedule($bobj->getScheduleId()); - - if (!ilBookingReservation::isObjectAvailableInPeriod($item_id, $schedule, $from, $to)) { - unset($data[$idx]); - continue; - } - } - - // cache reservations - $item_rsv = ilBookingReservation::getList(array($item_id), 1000, 0, array()); - $this->reservations[$item_id] = $item_rsv["data"]; - } - - if (!$this->has_schedule && - $this->overall_limit) { - $this->current_bookings = 0; - foreach ($this->reservations as $obj_rsv) { - foreach ($obj_rsv as $item) { - if ($item["status"] != ilBookingReservation::STATUS_CANCELLED) { - if ($item["user_id"] == $ilUser->getId()) { - $this->current_bookings++; - } - } - } - } - - if ($this->current_bookings >= $this->overall_limit) { - $this->main_tpl->setOnScreenMessage('info', $this->lng->txt("book_overall_limit_warning")); - } - } - if ($this->advmd) { - // advanced metadata - $this->record_gui = new ilAdvancedMDRecordGUI( - ilAdvancedMDRecordGUI::MODE_FILTER, - "book", - $this->pool_id, - "bobj" - ); - $this->record_gui->setTableGUI($this); - $this->record_gui->parse(); - - $data = ilAdvancedMDValues::queryForRecords( - $this->ref_id, - "book", - "bobj", - [$this->pool_id], - "bobj", - $data, - "pool_id", - "booking_object_id", - $this->record_gui->getFilterElements() - ); - } - - $this->setMaxCount(count($data)); - $this->setData($data); - } - - public function numericOrdering(string $a_field): bool - { - if (str_starts_with($a_field, "md_")) { - $md_id = (int) substr($a_field, 3); - if ($this->advmd[$md_id]["type"] === ilAdvancedMDFieldDefinition::TYPE_DATE) { - return true; - } - } - return false; - } - - public function getSelectableColumns(): array - { - $cols = array(); - - $cols["description"] = array( - "txt" => $this->lng->txt("description"), - "default" => true - ); - - foreach ($this->advmd as $field) { - $cols["advmd" . $field["id"]] = array( - "txt" => $field["title"], - "default" => false - ); - } - - return $cols; - } - - protected function fillRow(array $a_set): void - { - $lng = $this->lng; - $ilCtrl = $this->ctrl; - $ilUser = $this->user; - - $has_booking = false; - $booking_possible = $this->access->canManageOwnReservations($this->ref_id); - $assign_possible = true; - $has_reservations = false; - - $selected = $this->getSelectedColumns(); - - $this->tpl->setVariable("TXT_TITLE", $a_set["title"]); - - if (in_array("description", $selected, true)) { - $this->tpl->setVariable("TXT_DESC", nl2br($a_set["description"] ?? "")); - } - - if (isset($a_set["full_up"])) { - $this->tpl->setVariable("NOT_YET", $a_set["full_up"]); - $booking_possible = false; - $assign_possible = false; - } elseif (isset($a_set["not_yet"])) { - $this->tpl->setVariable("NOT_YET", $a_set["not_yet"]); - } - - if (!$this->has_schedule) { - $cnt = 0; - foreach ($this->reservations[$a_set["booking_object_id"]] as $item) { - if ($item["status"] != ilBookingReservation::STATUS_CANCELLED) { - $cnt++; - - if ($item["user_id"] == $ilUser->getId()) { - $has_booking = true; - } - - $has_reservations = true; - } - } - - $this->tpl->setVariable("VALUE_AVAIL", $a_set["nr_items"] - $cnt); - $this->tpl->setVariable("VALUE_AVAIL_ALL", $a_set["nr_items"]); - - if ($a_set["nr_items"] <= $cnt || ($this->overall_limit && $this->current_bookings && $this->current_bookings >= $this->overall_limit)) { - $booking_possible = false; - } - if ($has_booking) { - $booking_possible = false; - } - if ($a_set["nr_items"] <= $cnt) { - $assign_possible = false; - } - } elseif (!$this->may_edit) { - foreach ($this->reservations[$a_set["booking_object_id"]] as $item) { - if ($item["status"] != ilBookingReservation::STATUS_CANCELLED && - $item["user_id"] == $ilUser->getId()) { - $has_booking = true; - } - } - } - - //Actions - $items = array(); - - $ilCtrl->setParameter($this->parent_obj, 'object_id', $a_set['booking_object_id']); - - if ($booking_possible) { - if (isset($this->filter['period']['from'])) { - $ilCtrl->setParameter($this->parent_obj, 'sseed', $this->filter['period']['from']->get(IL_CAL_DATE)); - } - - $items[] = $this->ui_factory->button()->shy( - $lng->txt('book_book'), - $ilCtrl->getLinkTargetByClass($this->process_class, 'book') - ); - - $ilCtrl->setParameter($this->parent_obj, 'sseed', ''); - } - - if ($has_booking || $this->may_edit) { - if (trim($a_set['post_text'] ?? "") || $a_set['post_file']) { - $items[] = $this->ui_factory->button()->shy( - $lng->txt('book_post_booking_information'), - $ilCtrl->getLinkTargetByClass($this->process_class, 'displayPostInfo') - ); - } - } - - // #16663 - if (!$this->has_schedule && $has_booking) { - $ilCtrl->setParameterByClass("ilbookingreservationsgui", 'object_id', $a_set['booking_object_id']); - $items[] = $this->ui_factory->button()->shy($lng->txt('book_set_cancel'), $ilCtrl->getLinkTargetByClass("ilbookingreservationsgui", 'rsvConfirmCancelUser')); - $ilCtrl->setParameterByClass("ilbookingreservationsgui", 'object_id', ""); - } - - if ($this->may_edit || $has_booking) { - $ilCtrl->setParameterByClass('ilBookingReservationsGUI', 'object_id', $a_set['booking_object_id']); - $items[] = $this->ui_factory->button()->shy( - $lng->txt('book_log'), - $ilCtrl->getLinkTargetByClass('ilBookingReservationsGUI', 'log') - ); - $ilCtrl->setParameterByClass('ilBookingReservationsGUI', 'object_id', ''); - } - - if ($this->may_assign && $assign_possible) { - // note: this call is currently super expensive - // see #26388, it has been performed even for users without edit permissions before - // now the call has been moved here, but still this needs improvement - // EDIT: deactivated for now due to performance reasons - //if (!empty(ilBookingParticipant::getAssignableParticipants($a_set["booking_object_id"]))) { - if (isset($this->filter['period']['from'])) { - $ilCtrl->setParameterByClass( - $this->process_class, - 'sseed', - $this->filter['period']['from']->get(IL_CAL_DATE) - ); - } - - $items[] = $this->ui_factory->button()->shy( - $lng->txt('book_assign_participant'), - $ilCtrl->getLinkTargetByClass($this->process_class, 'assignParticipants') - ); - - $ilCtrl->setParameterByClass($this->process_class, 'sseed', ''); - //} - } - - if ($a_set['obj_info_rid']) { - $items[] = $this->ui_factory->button()->shy($lng->txt('book_download_info'), $ilCtrl->getLinkTarget($this->parent_obj, 'deliverInfo')); - } - - if ($this->may_edit) { - $items[] = $this->ui_factory->button()->shy($lng->txt('edit'), $ilCtrl->getLinkTarget($this->parent_obj, 'edit')); - - // #10890 - if (!$has_reservations) { - $items[] = $this->ui_factory->button()->shy($lng->txt('delete'), $ilCtrl->getLinkTarget($this->parent_obj, 'confirmDelete')); - } - } - - if ($this->advmd) { - foreach ($this->advmd as $item) { - $advmd_id = (int) $item["id"]; - - if (!in_array("advmd" . $advmd_id, $selected)) { - continue; - } - - $val = " "; - $key = "md_" . $advmd_id . "_presentation"; - if (isset($a_set[$key])) { - $pb = $a_set[$key]->getList(); - if ($pb) { - $val = $pb; - } - } - - $this->tpl->setCurrentBlock("advmd_bl"); - $this->tpl->setVariable("ADVMD_VAL", $val); - $this->tpl->parseCurrentBlock(); - } - } - - if (count($items)) { - $actions_dropdown = $this->ui_factory->dropdown()->standard($items)->withLabel($this->lng->txt('actions')); - $this->tpl->setVariable("ACTION_DROPDOWN", $this->ui_renderer->render($actions_dropdown)); - } - } -} diff --git a/components/ILIAS/BookingManager/Participants/class.ilBookingParticipantGUI.php b/components/ILIAS/BookingManager/Participants/class.ilBookingParticipantGUI.php index 8a19294fa671..b2cd73c4410b 100755 --- a/components/ILIAS/BookingManager/Participants/class.ilBookingParticipantGUI.php +++ b/components/ILIAS/BookingManager/Participants/class.ilBookingParticipantGUI.php @@ -16,18 +16,25 @@ * *********************************************************************/ +use ILIAS\BookingManager\Access\AccessManager; +use ILIAS\BookingManager\StandardGUIRequest; +use ILIAS\BookingManager\Common\HttpService; +use ILIAS\BookingManager\Participant\ParticipantRepository; +use ILIAS\BookingManager\Participant\Table\ParticipantTable; +use ILIAS\Data\Factory; +use ILIAS\Refinery\Factory as Refinery; +use ILIAS\UI\Factory as UIFactory; +use ILIAS\UI\Renderer as UIRenderer; +use ILIAS\UI\URLBuilder; + /** - * Class ilBookingParticipantGUI - * @author Jesús López * @ilCtrl_Calls ilBookingParticipantGUI: ilRepositorySearchGUI */ class ilBookingParticipantGUI { - public const FILTER_ACTION_APPLY = 1; - public const FILTER_ACTION_RESET = 2; public const PARTICIPANT_VIEW = 1; - protected \ILIAS\BookingManager\Access\AccessManager $access; - protected \ILIAS\BookingManager\StandardGUIRequest $book_request; + protected AccessManager $access; + protected StandardGUIRequest $book_request; protected ilGlobalTemplateInterface $tpl; protected ilTabsGUI $tabs; @@ -37,101 +44,120 @@ class ilBookingParticipantGUI protected int $ref_id; protected int $pool_id; - public function __construct( - ilObjBookingPoolGUI $a_parent_obj - ) { + private readonly Refinery $refinery; + private readonly UIFactory $ui_factory; + private readonly UIRenderer $ui_renderer; + private readonly HttpService $http; + private readonly ilUIService $ui_service; + private readonly Factory $data_factory; + private readonly ParticipantRepository $participant_repository; + + public function __construct(ilObjBookingPoolGUI $a_parent_obj) + { global $DIC; - $this->tpl = $DIC["tpl"]; + $this->tpl = $DIC->ui()->mainTemplate(); $this->tabs = $DIC->tabs(); $this->ctrl = $DIC->ctrl(); $this->lng = $DIC->language(); $this->access = $DIC->bookingManager()->internal()->domain()->access(); $this->toolbar = $DIC->toolbar(); - $this->book_request = $DIC->bookingManager() - ->internal() - ->gui() - ->standardRequest(); - + $this->book_request = $DIC + ->bookingManager() + ->internal() + ->gui() + ->standardRequest(); + $this->refinery = $DIC->refinery(); + $this->ui_factory = $DIC->ui()->factory(); + $this->ui_renderer = $DIC->ui()->renderer(); + $this->http = new HttpService($DIC->http(), $this->refinery); + $this->ui_service = $DIC->uiService(); + $this->data_factory = new Factory(); + $this->participant_repository = new ParticipantRepository($DIC->database()); $this->ref_id = $a_parent_obj->getRefId(); $this->pool_id = $a_parent_obj->getObject()->getId(); - $this->lng->loadLanguageModule("book"); - $this->lng->loadLanguageModule("exc"); + $this->lng->loadLanguageModule('book'); + $this->lng->loadLanguageModule('exc'); } public function executeCommand(): void { - $ilCtrl = $this->ctrl; - - $next_class = $ilCtrl->getNextClass($this); - - switch ($next_class) { - case 'ilrepositorysearchgui': + switch ($this->ctrl->getNextClass($this)) { + case strtolower(ilRepositorySearchGUI::class): $rep_search = new ilRepositorySearchGUI(); - $ref_id = $this->ref_id; - $rep_search->addUserAccessFilterCallable(function ($a_user_id) { - return $this->access->filterManageableParticipants( - $this->ref_id, - $a_user_id - ); - }); - $rep_search->setTitle($this->lng->txt("book_add_participant")); + $rep_search->addUserAccessFilterCallable( + fn(array $a_user_id): array => $this->access->filterManageableParticipants($this->ref_id, $a_user_id) + ); + $rep_search->setTitle($this->lng->txt('book_add_participant')); $rep_search->setCallback($this, 'addParticipantObject'); $this->ctrl->setReturn($this, 'render'); $this->ctrl->forwardCommand($rep_search); break; default: - $cmd = $ilCtrl->getCmd("render"); - $this->$cmd(); + $cmd = $this->ctrl->getCmd('render'); + if (method_exists($this, $cmd)) { + $this->$cmd(); + } break; } } - /** - * Render list of booking participants. - * uses ilBookingParticipantsTableGUI - */ - public function render(): void + public function executeTableAction(): void { - if ($this->access->canManageParticipants($this->ref_id)) { - ilRepositorySearchGUI::fillAutoCompleteToolbar( - $this, - $this->toolbar, - array( - 'auto_complete_name' => $this->lng->txt('user'), - 'submit_name' => $this->lng->txt('add'), - 'add_search' => true, - 'add_from_container' => $this->ref_id - ) - ); - - $table = new ilBookingParticipantsTableGUI($this, 'render', $this->ref_id, $this->pool_id); + $this->configureParticipantTable()->execute($this->getTableActionUrlBuilder()); + $this->render(); + } - $this->tpl->setContent($table->getHTML()); + public function render(): void + { + if (!$this->access->canManageParticipants($this->ref_id)) { + return; } + + ilRepositorySearchGUI::fillAutoCompleteToolbar( + $this, + $this->toolbar, + [ + 'auto_complete_name' => $this->lng->txt('user'), + 'submit_name' => $this->lng->txt('add'), + 'add_search' => true, + 'add_from_container' => $this->ref_id + ] + ); + + $this->tpl->setContent( + $this->ui_renderer->render( + $this->configureParticipantTable()->getComponents($this->getTableActionUrlBuilder()) + ) + ); } public function addUserFromAutoCompleteObject(): bool { if (trim($this->book_request->getUserLogin()) === '') { - $this->tpl->setOnScreenMessage('failure', $this->lng->txt('msg_no_search_string')); + $this->tpl->setOnScreenMessage( + ilGlobalTemplateInterface::MESSAGE_TYPE_FAILURE, + $this->lng->txt('msg_no_search_string') + ); $this->render(); return false; } - $users = explode(',', $this->book_request->getUserLogin()); - - $user_ids = array(); - foreach ($users as $user) { + $user_ids = []; + foreach (explode(',', $this->book_request->getUserLogin()) as $user) { $user_id = ilObjUser::_lookupId($user); if (!$user_id) { - $this->tpl->setOnScreenMessage('failure', $this->lng->txt('user_not_known')); + $this->tpl->setOnScreenMessage( + ilGlobalTemplateInterface::MESSAGE_TYPE_FAILURE, + $this->lng->txt('user_not_known') + ); $this->render(); } + $user_ids[] = $user_id; } @@ -139,62 +165,74 @@ public function addUserFromAutoCompleteObject(): bool } /** - * Add new participant * @param int[] $a_user_ids - * @throws ilCtrlException */ - public function addParticipantObject( - array $a_user_ids - ): bool { + public function addParticipantObject(array $a_user_ids): bool + { foreach ($a_user_ids as $user_id) { - if (ilObject::_lookupType($user_id) === "usr") { + if (ilObject::_lookupType($user_id) === 'usr') { $participant_obj = new ilBookingParticipant($user_id, $this->pool_id); + if ($participant_obj->getIsNew()) { - $this->tpl->setOnScreenMessage('success', $this->lng->txt("book_participant_assigned"), true); - } else { - $this->tpl->setOnScreenMessage('failure', $this->lng->txt("book_participant_already_assigned"), true); + $this->tpl->setOnScreenMessage( + ilGlobalTemplateInterface::MESSAGE_TYPE_SUCCESS, + $this->lng->txt('book_participant_assigned'), + true + ); + continue; } - } else { - $this->tpl->setOnScreenMessage('failure', "dummy error message, change me"); - return false; + + $this->tpl->setOnScreenMessage( + ilGlobalTemplateInterface::MESSAGE_TYPE_FAILURE, + $this->lng->txt('book_participant_already_assigned'), + true + ); + continue; } + + $this->tpl->setOnScreenMessage( + ilGlobalTemplateInterface::MESSAGE_TYPE_FAILURE, + 'dummy error message, change me' + ); + return false; } - $this->ctrl->redirect($this, "render"); + $this->ctrl->redirect($this, 'render'); return true; } - public function applyParticipantsFilter(): void + public function assignObjects(): void { - $this->applyFilterAction(self::FILTER_ACTION_APPLY); - } + $this->tabs->clearTargets(); + $this->tabs->setBackTarget($this->lng->txt('book_back_to_list'), $this->ctrl->getLinkTarget($this, 'render')); - public function resetParticipantsFilter(): void - { - $this->applyFilterAction(self::FILTER_ACTION_RESET); + $table = new ilBookingAssignObjectsTableGUI($this, 'assignObjects', $this->ref_id, $this->pool_id); + $this->tpl->setContent($table->getHTML()); } - protected function applyFilterAction( - int $a_filter_action - ): void { - $table = new ilBookingParticipantsTableGUI($this, 'render', $this->ref_id, $this->pool_id); - $table->resetOffset(); - if ($a_filter_action === self::FILTER_ACTION_RESET) { - $table->resetFilter(); - } else { - $table->writeFilterToSession(); - } - $this->render(); + private function configureParticipantTable(): ParticipantTable + { + return new ParticipantTable( + $this->ui_factory, + $this->ui_renderer, + $this->lng, + $this->http, + $this->ui_service, + $this->ctrl, + $this->tpl, + $this->refinery, + $this->access, + $this->participant_repository, + $this->ref_id, + $this->pool_id, + ); } - public function assignObjects(): void + private function getTableActionUrlBuilder(): URLBuilder { - $this->tabs->clearTargets(); - $this->tabs->setBackTarget($this->lng->txt('book_back_to_list'), $this->ctrl->getLinkTarget($this, 'render')); - - $table = new ilBookingAssignObjectsTableGUI($this, 'assignObjects', $this->ref_id, $this->pool_id); - - $this->tpl->setContent($table->getHTML()); + return new URLBuilder($this->data_factory->uri( + ILIAS_HTTP_PATH . '/' . $this->ctrl->getLinkTargetByClass(self::class, 'executeTableAction') + )); } } diff --git a/components/ILIAS/BookingManager/Participants/class.ilBookingParticipantsTableGUI.php b/components/ILIAS/BookingManager/Participants/class.ilBookingParticipantsTableGUI.php deleted file mode 100755 index dde9536e952b..000000000000 --- a/components/ILIAS/BookingManager/Participants/class.ilBookingParticipantsTableGUI.php +++ /dev/null @@ -1,200 +0,0 @@ - - */ -class ilBookingParticipantsTableGUI extends ilTable2GUI -{ - protected ilAccessHandler $access; - protected ilObjUser $user; - protected int $ref_id; - protected int $pool_id; - protected array $filter; - protected array $objects; - - public function __construct( - ilBookingParticipantGUI $a_parent_obj, - string $a_parent_cmd, - int $a_ref_id, - int $a_pool_id - ) { - global $DIC; - - $this->ctrl = $DIC->ctrl(); - $this->lng = $DIC->language(); - $this->user = $DIC->user(); - $this->access = $DIC->access(); - $this->ref_id = $a_ref_id; - $this->pool_id = $a_pool_id; - - $this->setId("bkprt" . $a_ref_id); - - parent::__construct($a_parent_obj, $a_parent_cmd); - - $this->setTitle($this->lng->txt("participants")); - - $this->addColumn($this->lng->txt("name"), "name"); - $this->addColumn($this->lng->txt("book_bobj")); - $this->addColumn($this->lng->txt("action")); - - $this->setDefaultOrderField("name"); - $this->setDefaultOrderDirection("asc"); - - $this->setEnableHeader(true); - $this->setFormAction($this->ctrl->getFormAction($a_parent_obj, $a_parent_cmd)); - $this->setRowTemplate("tpl.booking_participant_row.html", "components/ILIAS/BookingManager"); - $this->setResetCommand("resetParticipantsFilter"); - $this->setFilterCommand("applyParticipantsFilter"); - $this->setDisableFilterHiding(true); - - $this->initFilter(); - - $this->getItems($this->getCurrentFilter()); - } - - public function initFilter(): void - { - //object - $this->objects = array(); - foreach (ilBookingObject::getList($this->pool_id) as $item) { - $this->objects[$item["booking_object_id"]] = $item["title"]; - } - $item = $this->addFilterItemByMetaType("object", ilTable2GUI::FILTER_SELECT); - if ($item !== null) { - $item->setOptions([ - "" => $this->lng->txt('book_all'), - -1 => $this->lng->txt('book_no_objects') - ] + $this->objects); - $this->filter["object"] = $item->getValue(); - $title = $this->addFilterItemByMetaType( - "title", - ilTable2GUI::FILTER_TEXT, - false, - $this->lng->txt("object") . " " . $this->lng->txt("title") . "/" . $this->lng->txt("description") - ); - if ($title !== null) { - $this->filter["title"] = $title->getValue(); - } - //user - $options = array("" => $this->lng->txt('book_all')) + - ilBookingParticipant::getUserFilter($this->pool_id); - $item = $this->addFilterItemByMetaType("user", ilTable2GUI::FILTER_SELECT); - $item->setOptions($options); - $this->filter["user_id"] = $item->getValue(); - } - } - - /** - * Get current filter settings - */ - public function getCurrentFilter(): array - { - $filter = array(); - if ($this->filter["object"]) { - $filter["object"] = $this->filter["object"]; - } - if ($this->filter["title"]) { - $filter["title"] = $this->filter["title"]; - } - if ($this->filter["user_id"]) { - $filter["user_id"] = $this->filter["user_id"]; - } - - return $filter; - } - - /** - * Gather data and build rows - */ - public function getItems(array $filter): void - { - $filter_object = (int) ($filter["object"] ?? 0); - if ($filter_object > 0) { - $data = ilBookingParticipant::getList($this->pool_id, $filter, $filter_object); - } elseif ($filter_object === -1) { - $data = array_filter( - ilBookingParticipant::getList($this->pool_id, $filter), - static fn(array $item): bool => ($item["obj_count"] ?? 0) === 0 - ); - } else { - $data = ilBookingParticipant::getList($this->pool_id, $filter); - } - - $this->setMaxCount(count($data)); - $this->setData($data); - } - - protected function fillRow(array $a_set): void - { - $ctrl = $this->ctrl; - $lng = $this->lng; - - $this->tpl->setVariable("TXT_NAME", $a_set['name']); - $this->tpl->setCurrentBlock('object_titles'); - foreach ($a_set['object_title'] as $obj_title) { - $this->tpl->setVariable("TXT_OBJECT", $obj_title); - $this->tpl->parseCurrentBlock(); - } - - $obj_count = (int) ($a_set['obj_count'] ?? 0); - - // determin actions form data - // action assign only if user did not booked all objects. - $actions = []; - if ($obj_count < ilBookingObject::getNumberOfObjectsForPool($this->pool_id)) { - $ctrl->setParameterByClass('ilbookingparticipantgui', 'bkusr', $a_set['user_id']); - $actions[] = array( - 'text' => $lng->txt("book_assign_object"), - 'url' => $ctrl->getLinkTargetByClass("ilbookingparticipantgui", 'assignObjects') - ); - $ctrl->setParameterByClass('ilbookingparticipantgui', 'bkusr', ''); - } - - $bp = new ilObjBookingPool($this->pool_id, false); - if ($obj_count === 1 && $bp->getScheduleType() === ilObjBookingPool::TYPE_NO_SCHEDULE) { - $ctrl->setParameterByClass('ilbookingreservationsgui', 'bkusr', $a_set['user_id']); - $ctrl->setParameterByClass('ilbookingreservationsgui', 'object_id', $a_set['object_ids'][0]); - $ctrl->setParameterByClass('ilbookingreservationsgui', 'part_view', ilBookingParticipantGUI::PARTICIPANT_VIEW); - - $actions[] = array( - 'text' => $lng->txt("book_deassign"), - 'url' => $ctrl->getLinkTargetByClass("ilbookingreservationsgui", 'rsvConfirmCancelUser') - ); - - $ctrl->setParameterByClass('ilbookingreservationsgui', 'bkusr', ''); - $ctrl->setParameterByClass('ilbookingreservationsgui', 'object_id', ''); - $ctrl->setParameterByClass('ilbookingreservationsgui', 'part_view', ''); - } elseif ($obj_count > 1 || $bp->getScheduleType() === ilObjBookingPool::TYPE_FIX_SCHEDULE) { - $ctrl->setParameterByClass('ilbookingreservationsgui', 'user_id', $a_set['user_id']); - $actions[] = array( - 'text' => $lng->txt("book_deassign"), - 'url' => $ctrl->getLinkTargetByClass("ilbookingreservationsgui", 'log') - ); - $ctrl->setParameterByClass('ilbookingreservationsgui', 'user_id', ''); - } - - $this->tpl->setCurrentBlock('actions'); - foreach ($actions as $action) { - $this->tpl->setVariable("TXT_ACTION", $action['text']); - $this->tpl->setVariable("URL_ACTION", $action['url']); - $this->tpl->parseCurrentBlock(); - } - } -} diff --git a/components/ILIAS/BookingManager/Preferences/class.ilBookingPreferencesGUI.php b/components/ILIAS/BookingManager/Preferences/class.ilBookingPreferencesGUI.php index f5cdb1f8101c..ab3937bc9874 100755 --- a/components/ILIAS/BookingManager/Preferences/class.ilBookingPreferencesGUI.php +++ b/components/ILIAS/BookingManager/Preferences/class.ilBookingPreferencesGUI.php @@ -20,6 +20,7 @@ use ILIAS\BookingManager\Objects\ObjectsManager; use ILIAS\BookingManager\Access\AccessManager; use ILIAS\BookingManager\InternalService; +use ILIAS\Data\Factory as DataFactory; /** * Booking preferences ui class @@ -216,9 +217,10 @@ protected function listBookingResults(): void $preferences = $preferences->getPreferences(); $cnt = 1; if (isset($preferences[$this->user->getId()])) { + $book_deleted_object = "[{$this->lng->txt("book_deleted_object")}]"; foreach ($preferences[$this->user->getId()] as $book_obj_id) { $book_obj = new ilBookingObject($book_obj_id); - $info_gui->addProperty((string) $cnt++, $book_obj->getTitle()); + $info_gui->addProperty((string) $cnt++, $book_obj->getTitle() ?: $book_deleted_object); } } else { $info_gui->addProperty("", $lng->txt("book_no_preferences_for_you")); @@ -257,22 +259,33 @@ protected function listBookingResults(): void $info_gui->addSection($lng->txt("book_all_users")); $preferences = $repo->getPreferences($this->pool->getId()); $preferences = $preferences->getPreferences(); + $book_deleted_object = "[{$this->lng->txt("book_deleted_object")}]"; + $book_deleted_booking = "[{$this->lng->txt("book_deleted_booking")}]"; + + $markdown = (new DataFactory())->text()->markdown(); + foreach ($preferences as $user_id => $obj_ids) { - $booking_str = "
" . $lng->txt("book_log") . ": -"; - if (isset($bookings[$user_id])) { - $booking_str = "
" . $lng->txt("book_log") . ": " . implode(", ", array_map( - static function ($obj_id) { - return (new ilBookingObject($obj_id))->getTitle(); - }, + $booking_titles = !isset($bookings[$user_id]) + ? [$book_deleted_booking] + : array_map( + static fn(int $obj_id): string => (new ilBookingObject($obj_id))->getTitle(), $bookings[$user_id] - )); - } + ); + + $booking_str = "{$lng->txt("book_log")}: " . implode(", ", $booking_titles); + + $preferences_str = "{$lng->txt("book_preferences")}: " . implode( + ", ", + array_map( + fn(int $obj_id): string + => (new ilBookingObject($obj_id))->getTitle() ?: $book_deleted_object, + $obj_ids + ) + ); $info_gui->addProperty( ilUserUtil::getNamePresentation($user_id, false, false, "", true), - $lng->txt("book_preferences") . ": " . implode(", ", array_map(static function ($obj_id) { - return (new ilBookingObject($obj_id))->getTitle(); - }, $obj_ids)) . $booking_str + (string) $markdown->simpleDocument("{$preferences_str}\n{$booking_str}")->toHTML() ); } } diff --git a/components/ILIAS/BookingManager/Preferences/class.ilBookingPreferencesManager.php b/components/ILIAS/BookingManager/Preferences/class.ilBookingPreferencesManager.php index 2f6df6c901e8..6a7d6b711c64 100755 --- a/components/ILIAS/BookingManager/Preferences/class.ilBookingPreferencesManager.php +++ b/components/ILIAS/BookingManager/Preferences/class.ilBookingPreferencesManager.php @@ -219,16 +219,19 @@ protected function getUsersForObject( /** * Calculate popularity (number of preferences each object got from users) */ - protected function calculatePopularity( - array $booking_object_ids, - array $preferences - ): array { + protected function calculatePopularity(array $booking_object_ids, array $preferences): array + { $popularity = []; foreach ($booking_object_ids as $book_obj_id) { $popularity[$book_obj_id] = 0; } - foreach ($preferences as $user_id => $bobj_ids) { + + foreach ($preferences as $bobj_ids) { foreach ($bobj_ids as $bobj_id) { + if (!isset($popularity[$bobj_id])) { + continue; + } + ++$popularity[$bobj_id]; } } diff --git a/components/ILIAS/BookingManager/Reservations/class.ilBookingReservation.php b/components/ILIAS/BookingManager/Reservations/class.ilBookingReservation.php index 88fa48f86732..c70f0311fb20 100755 --- a/components/ILIAS/BookingManager/Reservations/class.ilBookingReservation.php +++ b/components/ILIAS/BookingManager/Reservations/class.ilBookingReservation.php @@ -22,8 +22,8 @@ */ class ilBookingReservation { - public const STATUS_IN_USE = 2; - public const STATUS_CANCELLED = 5; + public const int STATUS_IN_USE = 2; + public const int STATUS_CANCELLED = 5; protected ilDBInterface $db; protected int $id = 0; @@ -541,6 +541,9 @@ public static function getList( if (isset($filter['to'])) { $where[] = 'date_to <= ' . $ilDB->quote($filter['to'], 'integer'); } + if (isset($filter['user_id'])) { + $where[] = 'user_id = ' . $ilDB->quote($filter['user_id'], 'integer'); + } if (count($where)) { $sql .= ' WHERE ' . implode(' AND ', $where); $count_sql .= ' WHERE ' . implode(' AND ', $where); diff --git a/components/ILIAS/BookingManager/Reservations/class.ilBookingReservationsGUI.php b/components/ILIAS/BookingManager/Reservations/class.ilBookingReservationsGUI.php index e994a4656545..a633795ca9cb 100755 --- a/components/ILIAS/BookingManager/Reservations/class.ilBookingReservationsGUI.php +++ b/components/ILIAS/BookingManager/Reservations/class.ilBookingReservationsGUI.php @@ -16,19 +16,29 @@ * *********************************************************************/ -/** - * Reservations screen - * @author Alexander Killing - */ +use ILIAS\BookingManager\Access\AccessManager; +use ILIAS\BookingManager\BookingProcess\ProcessUtilGUI; +use ILIAS\BookingManager\Bookings\Table\BookingsTable; +use ILIAS\BookingManager\Bookings\Table\BookingsWithoutScheduleTable; +use ILIAS\BookingManager\Bookings\Table\BookingsWithScheduleTable; +use ILIAS\BookingManager\Common\HttpService; +use ILIAS\BookingManager\InternalService; +use ILIAS\BookingManager\StandardGUIRequest; +use ILIAS\Data\Factory; +use ILIAS\DI\UIServices; +use ILIAS\UI\URLBuilder; + class ilBookingReservationsGUI { - protected \ILIAS\BookingManager\BookingProcess\ProcessUtilGUI $util_gui; - protected \ILIAS\BookingManager\Access\AccessManager $access; + public const string DEFAULT_CMD = 'log'; + + protected ProcessUtilGUI $util_gui; + protected AccessManager $access; protected ilToolbarGUI $toolbar; - protected \ILIAS\DI\UIServices $ui; - protected \ILIAS\BookingManager\InternalService $service; + protected UIServices $ui; + protected InternalService $service; protected array $raw_post_data; - protected \ILIAS\BookingManager\StandardGUIRequest $book_request; + protected StandardGUIRequest $book_request; protected ilBookingHelpAdapter $help; protected int $context_obj_id; protected ilCtrl $ctrl; @@ -39,15 +49,12 @@ class ilBookingReservationsGUI protected ilObjBookingPool $pool; protected int $ref_id; protected int $book_obj_id; - protected int $pbooked_user; protected string $reservation_id; // see BookingReservationDBRepo, obj_user_(slot)_context protected int $booked_user; + protected ilUIService $ui_service; - public function __construct( - ilObjBookingPool $pool, - ilBookingHelpAdapter $help, - int $context_obj_id = 0 - ) { + public function __construct(ilObjBookingPool $pool, ilBookingHelpAdapter $help, int $context_obj_id = 0) + { global $DIC; $this->tpl = $DIC->ui()->mainTemplate(); @@ -59,67 +66,66 @@ public function __construct( $this->tabs_gui = $DIC->tabs(); $this->help = $help; $this->user = $DIC->user(); - $this->book_request = $DIC->bookingManager() - ->internal() - ->gui() - ->standardRequest(); + $this->book_request = $DIC->bookingManager()->internal()->gui()->standardRequest(); $this->service = $DIC->bookingManager()->internal(); $this->ui = $DIC->ui(); $this->toolbar = $DIC->toolbar(); + $this->ui_service = $DIC->uiservice(); $this->book_obj_id = $this->book_request->getObjectId(); $this->context_obj_id = $context_obj_id; - // user who's reservation is being tackled (e.g. canceled) $this->booked_user = $this->book_request->getBookedUser(); if ($this->booked_user === 0) { $this->booked_user = $DIC->user()->getId(); } - // we get this from the reservation screen $this->reservation_id = $this->book_request->getReservationId(); - $this->ctrl->saveParameter($this, ["object_id", "bkusr"]); + $this->ctrl->saveParameter($this, ['object_id', 'bkusr']); if ($this->book_request->getObjectId() > 0 && ilBookingObject::lookupPoolId($this->book_request->getObjectId()) !== $this->pool->getId()) { - throw new ilException("Booking Object ID does not match Booking Pool."); + throw new ilException('Booking Object ID does not match Booking Pool.'); } $this->raw_post_data = $DIC->http()->request()->getParsedBody(); - $this->util_gui = $DIC->bookingManager()->internal()->gui()->process()->ProcessUtilGUI( - $this->pool, - $this - ); + $this->util_gui = $DIC + ->bookingManager() + ->internal() + ->gui() + ->process() + ->ProcessUtilGUI($this->pool, $this); } - /** - * Reservations IDs as currently provided from - */ protected function getLogReservationIds(): array { - $mrsv = $this->book_request->getReservationIds(); - if (count($mrsv) > 0) { - return $mrsv; - } - if ($this->reservation_id > 0) { - return array($this->reservation_id); + $reservation_ids = $this->book_request->getReservationIds(); + if ($reservation_ids !== []) { + return $reservation_ids; } - return []; + + return $this->reservation_id > 0 ? [$this->reservation_id] : []; } public function executeCommand(): void { - $ctrl = $this->ctrl; - - $next_class = $ctrl->getNextClass($this); - $cmd = $ctrl->getCmd("log"); - - switch ($next_class) { - default: - if (in_array($cmd, array("log", "logDetails", "changeStatusObject", "rsvConfirmCancelUser", "rsvCancelUser", - "applyLogFilter", "resetLogFilter", "rsvConfirmCancel", "rsvCancel", "back", "rsvConfirmDelete", "rsvDelete", "confirmResetRun", "resetRun", "displayPostInfo", "deliverPostFile", "redirectMailToBooker"))) { - $this->$cmd(); - } + $cmd = $this->ctrl->getCmd(self::DEFAULT_CMD); + if ($cmd === '') { + $cmd = self::DEFAULT_CMD; + } + + $cmds = [ + self::DEFAULT_CMD, + 'executeTableAction', + 'changeStatusObject', + 'back', + 'confirmResetRun', + 'resetRun', + 'displayPostInfo', + 'deliverPostFile' + ]; + if (in_array($cmd, $cmds, true)) { + $this->$cmd(); } } @@ -128,621 +134,165 @@ protected function setHelpId(string $a_id): void $this->help->setHelpId($a_id); } - /** - * List reservations - */ - public function log(): void + private function getBookingsTable(): BookingsTable { + global $DIC; + $this->showRerunPreferenceAssignment(); - $this->tpl->setContent($this->getReservationsTable()->getHTML()); + $booking_table_class = $this->pool->getScheduleType() === ilObjBookingPool::TYPE_FIX_SCHEDULE + ? BookingsWithScheduleTable::class + : BookingsWithoutScheduleTable::class; + + return new $booking_table_class( + $this->ui->factory(), + $this->ui->renderer(), + $this->access, + $this->tpl, + $DIC->refinery(), + $this->lng, + new HttpService($DIC->http(), $DIC->refinery()), + $this->user, + $DIC->bookingManager()->internal()->repo()->reservation(), + $this->ctrl, + $DIC->uiService(), + $this->service->domain()->bookingSettings()->getByObjId($this->pool->getId()), + $this->pool + ); } - /** - * Get reservationsTable - */ - protected function getReservationsTable(?string $reservation_id = null): ilBookingReservationsTableGUI + private function getTableActionUrlBuilder(): URLBuilder { - $show_all = $this->access->canManageAllReservations($this->ref_id) || $this->pool->hasPublicLog(); + return new URLBuilder((new Factory())->uri( + ILIAS_HTTP_PATH . '/' . $this->ctrl->getLinkTargetByClass(self::class, 'executeTableAction') + )); + } - $filter = []; - if ($this->book_obj_id > 0) { - $filter['object'] = $this->book_obj_id; - } - // coming from participants tab to cancel reservations. - if ($this->book_request->getUserId() > 0) { - $filter['user_id'] = $this->book_request->getUserId(); + private function presetBookingsFilterFromRequest(): void + { + $object_id = $this->book_request->getObjectId(); + if ($object_id <= 0) { + return; } - return new ilBookingReservationsTableGUI( + $filter_id = $this->pool->getScheduleType() === ilObjBookingPool::TYPE_FIX_SCHEDULE + ? BookingsWithScheduleTable::ID . '_filter' + : BookingsWithoutScheduleTable::ID . '_filter'; + + $gateway = new ilUIFilterServiceSessionGateway(); + $gateway->writeValue($filter_id, 'object', (string) $object_id); + $gateway->writeActivated($filter_id, true); + } + + public function log(): void + { + $this->presetBookingsFilterFromRequest(); + + $bookings_table = $this->getBookingsTable(); + $this->tpl->setContent( + $this->ui->renderer()->render( + $bookings_table->getComponents($this->getTableActionUrlBuilder()) + ) + ); + + $reservations_table = new ilBookingReservationsTableGUI( $this, 'log', $this->ref_id, $this->pool, - $show_all, - $filter, - $reservation_id, + $this->access->canManageAllReservations($this->ref_id) || $this->pool->hasPublicLog(), + $this->ui_service->filter()->getData($bookings_table->getFilter()) ?? [], + null, $this->context_obj_id > 0 ? [$this->context_obj_id] : null ); + $reservations_table->getExportMode() > 0 && $reservations_table->exportData($reservations_table->getExportMode(), true); } - public function logDetails(): void + public function executeTableAction(): void { - $tpl = $this->tpl; - - $this->tabs_gui->clearTargets(); - $this->tabs_gui->setBackTarget( - $this->lng->txt("back"), - $this->ctrl->getLinkTarget($this, "log") - ); - - $table = $this->getReservationsTable($this->reservation_id); - $tpl->setContent($table->getHTML()); + $this->getBookingsTable()->execute($this->getTableActionUrlBuilder()); + $this->ctrl->redirectByClass(self::class, 'log'); } - /** - * Change status of given reservations - */ public function changeStatusObject(): void { $rsv_ids = $this->book_request->getReservationIds(); - if (count($rsv_ids) === 0) { + if ($rsv_ids === []) { $this->tpl->setOnScreenMessage('failure', $this->lng->txt('select_one')); $this->log(); } if ($this->access->canManageAllReservations($this->ref_id)) { - ilBookingReservation::changeStatus( - $rsv_ids, - $this->book_request->getStatus() - ); + ilBookingReservation::changeStatus($rsv_ids, $this->book_request->getStatus()); } $this->tpl->setOnScreenMessage('success', $this->lng->txt('settings_saved'), true); - $this->ctrl->redirect($this, 'log'); - } - - public function applyLogFilter(): void - { - $table = $this->getReservationsTable(); - $table->resetOffset(); - $table->writeFilterToSession(); $this->log(); } - public function resetLogFilter(): void - { - $table = $this->getReservationsTable(); - $table->resetOffset(); - $table->resetFilter(); - $this->log(); - } - - // - // Cancelation reservations - // - - /** - * (C1) Confirmation screen for canceling booking without schedule from booking objects screen - * or from participants screen, if only one object has been selected. - * - * If the process is started form the booking objects screen, the current user - * is the owner of the reservation. - * - * From the participants screen the user id is provided as bkusr - */ - public function rsvConfirmCancelUser(): void - { - $ilCtrl = $this->ctrl; - $lng = $this->lng; - $tpl = $this->tpl; - $id = $this->book_obj_id; - if (!$id) { - return; - } - - $this->setHelpId("cancel_booking"); - - $conf = new ilConfirmationGUI(); - $conf->setFormAction($ilCtrl->getFormAction($this)); - $conf->setHeaderText($lng->txt('book_confirm_cancel')); - - $type = new ilBookingObject($id); - $conf->addItem('object_id', $id, $type->getTitle()); - $conf->setConfirm($lng->txt('book_set_cancel'), 'rsvCancelUser'); - $conf->setCancel($lng->txt('cancel'), 'back'); - - $tpl->setContent($conf->getHTML()); - } - - /** - * (C1.a) Confirmed (C1) - */ - public function rsvCancelUser(): void - { - $lng = $this->lng; - - $id = $this->book_obj_id; - $user_id = $this->booked_user; - - if (!$id || !$user_id) { - return; - } - - $ids = ilBookingReservation::getObjectReservationForUser($id, $user_id); - $id = current($ids); - $obj = new ilBookingReservation($id); - if ($obj->getUserId() !== $user_id) { - $this->tpl->setOnScreenMessage('failure', $lng->txt('permission_denied'), true); - $this->back(); - } - - $obj->setStatus(ilBookingReservation::STATUS_CANCELLED); - $obj->update(); - - $this->tpl->setOnScreenMessage('success', $lng->txt('settings_saved'), true); - $this->back(); - } - - /** - * Back to reservation list - */ protected function back(): void { - $this->ctrl->redirect($this, "log"); - } - - /** - * (C2) Confirmation screen for canceling booking from reservations screen (with and without schedule) - */ - public function rsvConfirmCancel(): void - { - $ilCtrl = $this->ctrl; - $lng = $this->lng; - $tpl = $this->tpl; - $ilUser = $this->user; - - $ids = $this->getLogReservationIds(); - if (count($ids) === 0) { - $this->back(); - } - $max = array(); - foreach ($ids as $idx => $id) { - if (!is_numeric($id)) { - [$obj_id, $user_id, $from, $to] = explode("_", $id); - - $valid_ids = array(); - foreach (ilBookingObject::getList($this->pool->getId()) as $item) { - $valid_ids[$item["booking_object_id"]] = $item["title"]; - } - if (array_key_exists($obj_id, $valid_ids) && $from > time() && ($this->access->canManageAllReservations($this->ref_id) || (int) $user_id === $ilUser->getId())) { - $rsv_ids = ilBookingReservation::getCancelDetails($obj_id, $user_id, $from, $to); - if (!count($rsv_ids)) { - unset($ids[$idx]); - } - if (count($rsv_ids) > 1) { - $max[$id] = count($rsv_ids); - $ids[$idx] = $rsv_ids; - } else { - // only 1 in group? treat as normal reservation - $ids[$idx] = array_shift($rsv_ids); - } - } else { - unset($ids[$idx]); - } - } - } - - if (!is_array($ids) || !count($ids)) { - $this->ctrl->redirect($this, 'log'); - } - - // show form instead - if (count($max) && max($max) > 1) { - $this->rsvConfirmCancelAggregation($ids); - return; - } - - $this->tabs_gui->clearTargets(); - $this->tabs_gui->setBackTarget( - $lng->txt("back"), - $ilCtrl->getLinkTargetByClass("ilBookingReservationsGUI", "") - ); - - $this->setHelpId("cancel_booking"); - - $conf = new ilConfirmationGUI(); - $conf->setFormAction($ilCtrl->getFormAction($this, 'rsvCancel')); - $conf->setHeaderText($lng->txt('book_confirm_cancel')); - $conf->setConfirm($lng->txt('book_set_cancel'), 'rsvCancel'); - $conf->setCancel($lng->txt('cancel'), 'back'); - - foreach ($ids as $id) { - $rsv = new ilBookingReservation($id); - $obj = new ilBookingObject($rsv->getObjectId()); - - $details = $obj->getTitle(); - if ($this->pool->getScheduleType() !== ilObjBookingPool::TYPE_NO_SCHEDULE) { - $details .= ", " . ilDatePresentation::formatPeriod( - new ilDateTime($rsv->getFrom(), IL_CAL_UNIX), - new ilDateTime($rsv->getTo() + 1, IL_CAL_UNIX) - ); - } - - $conf->addItem('rsv_id[]', $id, $details); - } - - $tpl->setContent($conf->getHTML()); - } - - - /** - * (C2.a) Cancel aggregated booking from reservations screen (with and without schedule) - * called in (C2) - */ - public function rsvConfirmCancelAggregation(?array $a_ids = null): void - { - $tpl = $this->tpl; - $ilCtrl = $this->ctrl; - $lng = $this->lng; - - $this->tabs_gui->clearTargets(); - $this->tabs_gui->setBackTarget( - $lng->txt("back"), - $ilCtrl->getLinkTarget($this, "log") - ); - - $this->setHelpId("cancel_booking"); - - // #13511 - $this->tpl->setOnScreenMessage('question', $lng->txt("book_confirm_cancel")); - - $form = $this->rsvConfirmCancelAggregationForm($a_ids); - - $tpl->setContent($form->getHTML()); - } - - /** - * Form being used in (C2.a) - */ - public function rsvConfirmCancelAggregationForm( - array $a_ids - ): ilPropertyFormGUI { - $form = new ilPropertyFormGUI(); - $form->setFormAction($this->ctrl->getFormAction($this, "rsvCancel")); - $form->setTitle($this->lng->txt("book_confirm_cancel_aggregation")); - - ilDatePresentation::setUseRelativeDates(false); - - foreach ($a_ids as $idx => $ids) { - $first = $ids; - if (is_array($ids)) { - $first = array_shift($first); - } - - $rsv = new ilBookingReservation($first); - $obj = new ilBookingObject($rsv->getObjectId()); - - $caption = $obj->getTitle() . ", " . ilDatePresentation::formatPeriod( - new ilDateTime($rsv->getFrom(), IL_CAL_UNIX), - new ilDateTime($rsv->getTo() + 1, IL_CAL_UNIX) - ); - - // #17869 - if (is_array($ids)) { - $caption .= " (" . count($ids) . ")"; - } - - $item = new ilNumberInputGUI($caption, "rsv_id_" . $idx); - $item->setRequired(true); - $item->setMinValue(0); - $item->setSize(4); - $form->addItem($item); - - if (is_array($ids)) { - $item->setMaxValue(count($ids)); - - foreach ($ids as $id) { - $hidden = new ilHiddenInputGUI("rsv_aggr[" . $idx . "][]"); - $hidden->setValue($id); - $form->addItem($hidden); - } - } else { - $item->setMaxValue(1); - - $hidden = new ilHiddenInputGUI("rsv_aggr[" . $idx . "]"); - $hidden->setValue($ids); - $form->addItem($hidden); - } - - if ($this->book_request->getCancelNr($idx)) { - $item->setValue($this->book_request->getCancelNr($idx)); - } - } - - $form->addCommandButton("rsvCancel", $this->lng->txt("confirm")); - $form->addCommandButton("log", $this->lng->txt("cancel")); - - return $form; - } - - /** - * (C2.b) Cancel reservations (coming from C2 or C2.a) - */ - public function rsvCancel(): void - { - $ilUser = $this->user; - $tpl = $this->tpl; - $lng = $this->lng; - $ilCtrl = $this->ctrl; - - // simple version of reservation id - $ids = $this->book_request->getReservationIds(); - - $rsv_aggr = $this->raw_post_data["rsv_aggr"] ?? null; - // aggregated version: determine reservation ids - if (!is_null($rsv_aggr)) { - $form = $this->rsvConfirmCancelAggregationForm($rsv_aggr); - if (!$form->checkInput()) { - $this->tabs_gui->clearTargets(); - $this->tabs_gui->setBackTarget( - $lng->txt("back"), - $ilCtrl->getLinkTarget($this, "log") - ); - - $tpl->setContent($form->getHTML()); - return; - } - - $ids = array(); - foreach ($rsv_aggr as $idx => $aggr_ids) { - $max = $this->book_request->getCancelNr($idx); - if ($max) { - if (!is_array($aggr_ids)) { - $ids[] = $aggr_ids; - } else { - $aggr_ids = array_slice($aggr_ids, 0, $max); - $ids = array_merge($ids, $aggr_ids); - } - } - } - } - - // for all reservation ids -> set reservation status to cancelled (and remove calendar entry) - if ($ids) { - foreach ($ids as $id) { - $res = new ilBookingReservation($id); - - // either write permission or own booking - if (!$this->access->canManageReservationForUser($this->ref_id, $res->getUserId())) { - $this->tpl->setOnScreenMessage('failure', $this->lng->txt('permission_denied'), true); - $this->ctrl->redirect($this, 'log'); - } - - $res->setStatus(ilBookingReservation::STATUS_CANCELLED); - $res->update(); - - if ($this->pool->getScheduleType() !== ilObjBookingPool::TYPE_NO_SCHEDULE) { - // remove user calendar entry (#11086) - $cal_entry_id = $res->getCalendarEntry(); - if ($cal_entry_id) { - $entry = new ilCalendarEntry($cal_entry_id); - $entry->delete(); - } - } - } - } - - $this->tpl->setOnScreenMessage('success', $this->lng->txt('settings_saved')); - $this->log(); - } - - public function rsvConfirmDelete(): void - { - global $DIC; - if (!$this->access->canManageAllReservations($this->ref_id)) { - $this->tpl->setOnScreenMessage('failure', $this->lng->txt('permission_denied'), true); - $this->ctrl->redirect($this, 'log'); - } - - $ids = $this->getLogReservationIds(); - if (count($ids) === 0) { - $this->back(); - } - - $this->tabs_gui->clearTargets(); - $this->tabs_gui->setBackTarget( - $this->lng->txt("back"), - $this->ctrl->getLinkTarget($this, "log") - ); - - $conf = new ilConfirmationGUI(); - $conf->setFormAction($this->ctrl->getFormAction($this, 'rsvDelete')); - $conf->setHeaderText($this->lng->txt('book_confirm_delete')); - $conf->setConfirm($this->lng->txt('book_set_delete'), 'rsvDelete'); - $conf->setCancel($this->lng->txt('cancel'), 'log'); - - if ($this->pool->getScheduleType() === ilObjBookingPool::TYPE_FIX_SCHEDULE) { - foreach ($ids as $idx => $id) { - [$obj_id, $user_id, $from, $to] = explode("_", $id); - $rsv_ids = ilBookingReservation::getCancelDetails($obj_id, $user_id, $from, $to); - $rsv_id = $rsv_ids[0]; - - $rsv = new ilBookingReservation($rsv_id); - $obj = new ilBookingObject($rsv->getObjectId()); - - $details = sprintf($this->lng->txt('X_reservations_of'), count($rsv_ids)) . ' ' . $obj->getTitle(); - $details .= ", " . ilDatePresentation::formatPeriod( - new ilDateTime($rsv->getFrom(), IL_CAL_UNIX), - new ilDateTime($rsv->getTo() + 1, IL_CAL_UNIX) - ); - $conf->addItem('mrsv[]', $id, $details); - } - } else { - foreach ($ids as $idx => $rsv_id) { - $rsv = new ilBookingReservation($rsv_id); - $obj = new ilBookingObject($rsv->getObjectId()); - $details = sprintf($this->lng->txt('X_reservations_of'), 1) . ' ' . $obj->getTitle(); - $conf->addItem('mrsv[]', $rsv_id, $details); - } - } - $this->tpl->setContent($conf->getHTML()); - } - - public function rsvDelete(): void - { - global $DIC; - $get = $DIC->http()->request()->getParsedBody()['mrsv']; - if ($get) { - foreach ($get as $id) { - if ($this->pool->getScheduleType() == ilObjBookingPool::TYPE_FIX_SCHEDULE) { - list($obj_id, $user_id, $from, $to) = explode("_", $id); - $rsv_ids = ilBookingReservation::getCancelDetails($obj_id, $user_id, $from, $to); - } else { - $rsv_ids = [$id]; - } - - foreach ($rsv_ids as $rsv_id) { - $res = new ilBookingReservation($rsv_id); - $obj = new ilBookingObject($res->getObjectId()); - if (!$this->access->canManageAllReservations($this->ref_id) || $obj->getPoolId() !== $this->pool->getId()) { - $this->tpl->setOnScreenMessage('failure', $this->lng->txt('permission_denied'), true); - $this->ctrl->redirect($this, 'log'); - } - if ($this->pool->getScheduleType() !== ilObjBookingPool::TYPE_NO_SCHEDULE) { - $cal_entry_id = $res->getCalendarEntry(); - if ($cal_entry_id) { - $entry = new ilCalendarEntry($cal_entry_id); - $entry->delete(); - } - } - $res->delete(); - } - } - } - $this->tpl->setOnScreenMessage('success', $this->lng->txt('reservation_deleted'), true); $this->ctrl->redirect($this, 'log'); } protected function showRerunPreferenceAssignment(): void { - if (!$this->access->canManageAllReservations($this->ref_id)) { + if ( + !$this->access->canManageAllReservations($this->ref_id) + || $this->pool->getScheduleType() !== ilObjBookingPool::TYPE_NO_SCHEDULE_PREFERENCES + ) { return; } - if ($this->pool->getScheduleType() === ilObjBookingPool::TYPE_NO_SCHEDULE_PREFERENCES) { - $pref_manager = $this->service->domain()->preferences($this->pool); - if ($pref_manager->hasRun()) { - $this->toolbar->addComponent($this->ui->factory()->button()->standard( - $this->lng->txt("book_rerun_assignments"), - $this->ctrl->getLinkTarget($this, "confirmResetRun") - )); - } + + if ($this->service->domain()->preferences($this->pool)->hasRun()) { + $this->toolbar->addComponent($this->ui->factory()->button()->standard( + $this->lng->txt('book_rerun_assignments'), + $this->ctrl->getLinkTarget($this, 'confirmResetRun') + )); } } - protected function confirmResetRun() + protected function confirmResetRun(): void { if (!$this->access->canManageAllReservations($this->ref_id)) { return; } - $this->tabs_gui->activateTab("log"); - $mess = $this->ui->factory()->messageBox()->confirmation($this->lng->txt("book_rerun_confirmation"))->withButtons( + + $this->tabs_gui->activateTab('log'); + $mess = $this->ui->factory()->messageBox()->confirmation($this->lng->txt('book_rerun_confirmation'))->withButtons( [ - $this->ui->factory()->button()->standard( - $this->lng->txt("book_rerun_assignments"), - $this->ctrl->getLinkTarget($this, "resetRun") - ), - $this->ui->factory()->button()->standard( - $this->lng->txt("cancel"), - $this->ctrl->getLinkTarget($this, "log") - ) + $this->ui->factory()->button()->standard($this->lng->txt('book_rerun_assignments'), $this->ctrl->getLinkTarget($this, 'resetRun')), + $this->ui->factory()->button()->standard($this->lng->txt('cancel'), $this->ctrl->getLinkTarget($this, 'log')) ] ); - $this->tpl->setContent( - $this->ui->renderer()->render($mess) - ); + $this->tpl->setContent($this->ui->renderer()->render($mess)); } - protected function resetRun() + protected function resetRun(): void { if (!$this->access->canManageAllReservations($this->ref_id)) { return; } - if ($this->pool->getScheduleType() === ilObjBookingPool::TYPE_NO_SCHEDULE_PREFERENCES - && $this->access->canManageAllReservations($this->pool->getRefId())) { + + if ( + $this->pool->getScheduleType() === ilObjBookingPool::TYPE_NO_SCHEDULE_PREFERENCES + && $this->access->canManageAllReservations($this->pool->getRefId()) + ) { $pref_manager = $this->service->domain()->preferences($this->pool); $repo = $this->service->repo()->preferences(); $pref_manager->resetRun(); - $pref_manager->storeBookings( - $repo->getPreferences($this->pool->getId()) - ); + $pref_manager->storeBookings($repo->getPreferences($this->pool->getId())); } - $this->ctrl->redirect($this, "log"); + $this->ctrl->redirect($this, 'log'); } public function displayPostInfo(): void { - $this->ctrl->saveParameter($this, "object_id"); - $this->util_gui->displayPostInfo( - $this->book_obj_id, - 0, - "deliverPostFile" - ); + $this->ctrl->saveParameter($this, 'object_id'); + $this->util_gui->displayPostInfo($this->book_obj_id, 0, 'deliverPostFile'); } public function deliverPostFile(): void { - $this->util_gui->deliverPostFile( - $this->book_obj_id, - $this->user->getId() - ); + $this->util_gui->deliverPostFile($this->book_obj_id, $this->user->getId()); } - - public function redirectMailToBooker(): void - { - if (!$this->access->canManageAllReservations($this->ref_id)) { - $this->tpl->setOnScreenMessage('failure', $this->lng->txt('permission_denied'), true); - $this->ctrl->redirect($this, 'log'); - } - - $ids = $this->getLogReservationIds(); - if (count($ids) === 0) { - $this->back(); - } - - $users = []; - if ($this->pool->getScheduleType() === ilObjBookingPool::TYPE_FIX_SCHEDULE) { - foreach ($ids as $idx => $id) { - [$obj_id, $user_id, $from, $to] = explode("_", $id); - $rsv_ids = ilBookingReservation::getCancelDetails($obj_id, $user_id, $from, $to); - $rsv_id = $rsv_ids[0]; - $rsv = new ilBookingReservation($rsv_id); - $users[$rsv->getUserId()] = ilObjUser::_lookupLogin($rsv->getUserId()); - } - } else { - foreach ($ids as $idx => $rsv_id) { - $rsv = new ilBookingReservation($rsv_id); - $users[$rsv->getUserId()] = ilObjUser::_lookupLogin($rsv->getUserId()); - } - } - $logins = implode(",", $users); - // #16530 - see ilObjCourseGUI::createMailSignature - $sig = chr(13) . chr(10) . chr(13) . chr(10) . chr(13) . chr(10); - $sig .= $this->lng->txt('book_mail_permanent_link') . ": "; - $sig .= chr(13) . chr(10); - $sig .= ilLink::_getLink($this->book_request->getRefId()); - $sig = rawurlencode(base64_encode($sig)); - - ilMailFormCall::setRecipients($users); - \ilUtil::redirect(ilMailFormCall::getRedirectTarget( - $this, - "log", - array(), - array( - 'type' => 'new', - 'rcp_to' => $logins, - ilMailFormCall::SIGNATURE_KEY => $sig - ) - )); - } - } diff --git a/components/ILIAS/BookingManager/Reservations/class.ilBookingReservationsTableGUI.php b/components/ILIAS/BookingManager/Reservations/class.ilBookingReservationsTableGUI.php index 728c3b363f18..d557655ecb92 100755 --- a/components/ILIAS/BookingManager/Reservations/class.ilBookingReservationsTableGUI.php +++ b/components/ILIAS/BookingManager/Reservations/class.ilBookingReservationsTableGUI.php @@ -189,7 +189,6 @@ public function __construct( $this->addMultiCommand('rsvConfirmCancel', $lng->txt('book_set_cancel')); } if ($this->access->canManageAllReservations($this->ref_id)) { - $this->addMultiCommand('redirectMailToBooker', $lng->txt('book_mail_to_booker')); $this->addMultiCommand('rsvConfirmDelete', $lng->txt('delete')); } } @@ -379,9 +378,9 @@ public function initFilter( ilBookingReservation::getUserFilter(array_keys($this->objects)); $item = $this->addFilterItemByMetaType("user", ilTable2GUI::FILTER_SELECT); $item->setOptions($options); - if (is_array($a_filter_pre) && isset($a_filter_pre["user_id"])) { - $item->setValue($a_filter_pre["user_id"]); - $this->filter["user_id"] = $a_filter_pre["user_id"]; + if (is_array($a_filter_pre) && isset($a_filter_pre["user"])) { + $item->setValue($a_filter_pre["user"]); + $this->filter["user_id"] = $a_filter_pre["user"]; } else { $this->filter["user_id"] = $item->getValue(); } @@ -718,10 +717,6 @@ protected function fillRow(array $a_set): void if ($this->access->canManageAllReservations($this->ref_id)) { $ilCtrl->setParameter($this->parent_obj, 'reservation_id', $a_set['booking_reservation_id']); - $dd_items[] = $f->button()->shy( - $lng->txt('book_mail_to_booker'), - $ilCtrl->getLinkTarget($this->parent_obj, 'redirectMailToBooker') - ); $dd_items[] = $f->button()->shy( $lng->txt('delete'), $ilCtrl->getLinkTarget($this->parent_obj, 'rsvConfirmDelete') diff --git a/components/ILIAS/BookingManager/Schedule/class.ilBookingScheduleGUI.php b/components/ILIAS/BookingManager/Schedule/class.ilBookingScheduleGUI.php index c1eebbaee03a..e9897081371e 100755 --- a/components/ILIAS/BookingManager/Schedule/class.ilBookingScheduleGUI.php +++ b/components/ILIAS/BookingManager/Schedule/class.ilBookingScheduleGUI.php @@ -16,16 +16,25 @@ * *********************************************************************/ +use ILIAS\BookingManager\Access\AccessManager; +use ILIAS\BookingManager\StandardGUIRequest; +use ILIAS\BookingManager\Common\HttpService; +use ILIAS\BookingManager\Schedule\ScheduleManager; +use ILIAS\BookingManager\Schedule\Table\ScheduleTable; +use ILIAS\BookingManager\Service as BookingManager; +use ILIAS\Data\Factory; +use ILIAS\Refinery\Factory as Refinery; +use ILIAS\UI\Factory as UIFactory; +use ILIAS\UI\Renderer as UIRenderer; +use ILIAS\UI\URLBuilder; + /** - * Class ilBookingScheduleGUI - * - * @author Jörg Lützenkirchen * @ilCtrl_Calls ilBookingScheduleGUI: */ class ilBookingScheduleGUI { - protected \ILIAS\BookingManager\Access\AccessManager $access; - protected \ILIAS\BookingManager\StandardGUIRequest $book_request; + protected AccessManager $access; + protected StandardGUIRequest $book_request; protected ilGlobalTemplateInterface $tpl; protected ilTabsGUI $tabs; protected ilCtrl $ctrl; @@ -34,10 +43,16 @@ class ilBookingScheduleGUI protected ilObjectDataCache $obj_data_cache; protected int $schedule_id; protected int $ref_id; - - public function __construct( - ilObjBookingPoolGUI $a_parent_obj - ) { + private readonly Refinery $refinery; + private readonly UIFactory $ui_factory; + private readonly UIRenderer $ui_renderer; + private readonly HttpService $http; + private readonly BookingManager $booking_manager; + private readonly ilToolbarGUI $toolbar; + private readonly Factory $data_factory; + + public function __construct(ilObjBookingPoolGUI $a_parent_obj) + { global $DIC; $this->tpl = $DIC->ui()->mainTemplate(); @@ -45,175 +60,149 @@ public function __construct( $this->ctrl = $DIC->ctrl(); $this->lng = $DIC->language(); $this->access = $DIC->bookingManager()->internal()->domain()->access(); - $this->help = $DIC["ilHelp"]; - $this->obj_data_cache = $DIC["ilObjDataCache"]; + $this->help = $DIC['ilHelp']; + $this->obj_data_cache = $DIC['ilObjDataCache']; + $this->refinery = $DIC->refinery(); + $this->ui_factory = $DIC->ui()->factory(); + $this->ui_renderer = $DIC->ui()->renderer(); + $this->http = new HttpService($DIC->http(), $this->refinery); + $this->booking_manager = $DIC->bookingManager(); + $this->toolbar = $DIC->toolbar(); + $this->data_factory = new Factory(); + $this->ref_id = $a_parent_obj->getRefId(); - $this->book_request = $DIC->bookingManager() - ->internal() - ->gui() - ->standardRequest(); + $this->book_request = $DIC->bookingManager()->internal()->gui()->standardRequest(); $this->schedule_id = $this->book_request->getScheduleId(); if ($this->schedule_id > 0) { - $this->access->validateScheduleId( - $this->schedule_id, - ilObject::_lookupObjId($this->ref_id) - ); + $this->access->validateScheduleId($this->schedule_id, ilObject::_lookupObjId($this->ref_id)); } } public function executeCommand(): void { - $ilCtrl = $this->ctrl; - - $next_class = $ilCtrl->getNextClass($this); - - switch ($next_class) { + switch ($this->ctrl->getNextClass($this)) { default: - $cmd = $ilCtrl->getCmd("render"); - $this->$cmd(); + $cmd = $this->ctrl->getCmd('render'); + if (method_exists($this, $cmd)) { + $this->$cmd(); + } break; } } - /** - * Render list of booking schedules - * uses ilBookingSchedulesTableGUI - */ + public function executeTableAction(): void + { + $pool_id = $this->obj_data_cache->lookupObjId($this->ref_id); + $schedule_manager = $this->booking_manager->internal()->domain()->schedules($pool_id); + + $this->configureScheduleTable($schedule_manager)->execute($this->getTableActionUrlBuilder()); + $this->render(); + } + public function render(): void { - $tpl = $this->tpl; - $lng = $this->lng; - $ilCtrl = $this->ctrl; - $table = new ilBookingSchedulesTableGUI($this, 'render', $this->ref_id); + $pool_id = $this->obj_data_cache->lookupObjId($this->ref_id); + $schedule_manager = $this->booking_manager->internal()->domain()->schedules($pool_id); - $bar = ""; - if ($this->access->canManageSettings($this->ref_id)) { - // if we have schedules but no objects - show info - if (count($table->getData())) { - if (!count(ilBookingObject::getList(ilObject::_lookupObjId($this->ref_id)))) { - $this->tpl->setOnScreenMessage('info', $lng->txt("book_type_warning")); - } - } + $this->checkForInfoMessageAboutMissingBookableItems($schedule_manager, $pool_id); - $bar = new ilToolbarGUI(); - $bar->addButton($lng->txt('book_add_schedule'), $ilCtrl->getLinkTarget($this, 'create')); - $bar = $bar->getHTML(); + if ($this->access->canManageSettings($this->ref_id)) { + $this->toolbar->addComponent( + $this->ui_factory->button()->standard( + $this->lng->txt('book_add_schedule'), + $this->ctrl->getLinkTarget($this, 'create') + ) + ); } - $tpl->setContent($bar . $table->getHTML()); + $this->tpl->setContent( + $this->ui_renderer->render( + $this->configureScheduleTable($schedule_manager)->getComponents($this->getTableActionUrlBuilder()) + ) + ); } - /** - * Render creation form - */ public function create(): void { - $tpl = $this->tpl; - $ilCtrl = $this->ctrl; - $ilTabs = $this->tabs; - $lng = $this->lng; - $ilHelp = $this->help; - - $ilTabs->clearTargets(); - $ilTabs->setBackTarget($lng->txt('book_back_to_list'), $ilCtrl->getLinkTarget($this, 'render')); - $ilHelp->setScreenIdComponent("book"); - $ilHelp->setScreenId("schedules"); - $ilHelp->setSubScreenId("create"); - - $form = $this->initForm(); - $tpl->setContent($form->getHTML()); + $this->tabs->clearTargets(); + $this->tabs->setBackTarget($this->lng->txt('book_back_to_list'), $this->ctrl->getLinkTarget($this, 'render')); + $this->help->setScreenIdComponent('book'); + $this->help->setScreenId('schedules'); + $this->help->setSubScreenId('create'); + $this->tpl->setContent($this->initForm()->getHTML()); } - /** - * Render edit form - */ public function edit(): void { - $tpl = $this->tpl; - $ilCtrl = $this->ctrl; - $ilTabs = $this->tabs; - $lng = $this->lng; - $ilHelp = $this->help; - - $ilTabs->clearTargets(); - $ilTabs->setBackTarget($lng->txt('book_back_to_list'), $ilCtrl->getLinkTarget($this, 'render')); - $ilHelp->setScreenIdComponent("book"); - $ilHelp->setScreenId("schedules"); - $ilHelp->setSubScreenId("edit"); - - $form = $this->initForm('edit', $this->schedule_id); - $tpl->setContent($form->getHTML()); + $this->tabs->clearTargets(); + $this->tabs->setBackTarget($this->lng->txt('book_back_to_list'), $this->ctrl->getLinkTarget($this, 'render')); + $this->help->setScreenIdComponent('book'); + $this->help->setScreenId('schedules'); + $this->help->setSubScreenId('edit'); + $this->tpl->setContent($this->initForm('edit', $this->schedule_id)->getHTML()); } - /** - * Build property form - */ - public function initForm( - string $a_mode = "create", - ?int $id = null - ): ilPropertyFormGUI { - $lng = $this->lng; - $ilCtrl = $this->ctrl; - - $lng->loadLanguageModule("dateplaner"); + public function initForm(string $a_mode = 'create', ?int $id = null): ilPropertyFormGUI + { + $this->lng->loadLanguageModule('dateplaner'); $form_gui = new ilPropertyFormGUI(); - $title = new ilTextInputGUI($lng->txt("title"), "title"); + $title = new ilTextInputGUI($this->lng->txt('title'), 'title'); $title->setRequired(true); $title->setSize(40); $title->setMaxLength(120); $form_gui->addItem($title); - $definition = new ilScheduleInputGUI($lng->txt("book_schedule_days"), "days"); - $definition->setInfo($lng->txt("book_schedule_days_info")); + $definition = new ilScheduleInputGUI($this->lng->txt('book_schedule_days'), 'days'); + $definition->setInfo($this->lng->txt('book_schedule_days_info')); $definition->setRequired(true); $form_gui->addItem($definition); - $deadline_opts = new ilRadioGroupInputGUI($lng->txt("book_deadline_options"), "deadline_opts"); + $deadline_opts = new ilRadioGroupInputGUI($this->lng->txt('book_deadline_options'), 'deadline_opts'); $deadline_opts->setRequired(true); $form_gui->addItem($deadline_opts); - $deadline_time = new ilRadioOption($lng->txt("book_deadline_hours"), "hours"); + $deadline_time = new ilRadioOption($this->lng->txt('book_deadline_hours'), 'hours'); $deadline_opts->addOption($deadline_time); - $deadline = new ilNumberInputGUI($lng->txt("book_deadline"), "deadline"); - $deadline->setInfo($lng->txt("book_deadline_info")); - $deadline->setSuffix($lng->txt("book_hours")); + $deadline = new ilNumberInputGUI($this->lng->txt('book_deadline'), 'deadline'); + $deadline->setInfo($this->lng->txt('book_deadline_info')); + $deadline->setSuffix($this->lng->txt('book_hours')); $deadline->setMinValue(1); $deadline->setSize(3); $deadline->setMaxLength(3); $deadline->setRequired(true); $deadline_time->addSubItem($deadline); - $deadline_start = new ilRadioOption($lng->txt("book_deadline_slot_start"), "slot_start"); + $deadline_start = new ilRadioOption($this->lng->txt('book_deadline_slot_start'), 'slot_start'); $deadline_opts->addOption($deadline_start); - $deadline_slot = new ilRadioOption($lng->txt("book_deadline_slot_end"), "slot_end"); + $deadline_slot = new ilRadioOption($this->lng->txt('book_deadline_slot_end'), 'slot_end'); $deadline_opts->addOption($deadline_slot); - if ($a_mode === "edit") { + if ($a_mode === 'edit') { $schedule = new ilBookingSchedule($id); } $av = new ilFormSectionHeaderGUI(); - $av->setTitle($lng->txt("obj_activation_list_gui")); + $av->setTitle($this->lng->txt('obj_activation_list_gui')); $form_gui->addItem($av); // #18221 - $lng->loadLanguageModule('rep'); + $this->lng->loadLanguageModule('rep'); - $from = new ilDateTimeInputGUI($lng->txt("rep_activation_limited_start"), "from"); + $from = new ilDateTimeInputGUI($this->lng->txt('rep_activation_limited_start'), 'from'); $from->setShowTime(true); $form_gui->addItem($from); - $to = new ilDateTimeInputGUI($lng->txt("rep_activation_limited_end"), "to"); + $to = new ilDateTimeInputGUI($this->lng->txt('rep_activation_limited_end'), 'to'); $to->setShowTime(true); $form_gui->addItem($to); - if ($a_mode === "edit") { - $form_gui->setTitle($lng->txt("book_edit_schedule")); + if ($a_mode === 'edit') { + $form_gui->setTitle($this->lng->txt('book_edit_schedule')); $item = new ilHiddenInputGUI('schedule_id'); $item->setValue($id); @@ -225,160 +214,127 @@ public function initForm( $to->setDate($schedule->getAvailabilityTo()); if ($schedule->getDeadline() === 0) { - $deadline_opts->setValue("slot_start"); + $deadline_opts->setValue('slot_start'); } elseif ($schedule->getDeadline() > 0) { $deadline->setValue($schedule->getDeadline()); - $deadline_opts->setValue("hours"); + $deadline_opts->setValue('hours'); } else { $deadline->setValue(0); - $deadline_opts->setValue("slot_end"); + $deadline_opts->setValue('slot_end'); } $definition->setValue($schedule->getDefinitionBySlots()); - $form_gui->addCommandButton("update", $lng->txt("save")); + $form_gui->addCommandButton('update', $this->lng->txt('save')); } else { - $form_gui->setTitle($lng->txt("book_add_schedule")); - $form_gui->addCommandButton("save", $lng->txt("save")); - $form_gui->addCommandButton("render", $lng->txt("cancel")); + $form_gui->setTitle($this->lng->txt('book_add_schedule')); + $form_gui->addCommandButton('save', $this->lng->txt('save')); + $form_gui->addCommandButton('render', $this->lng->txt('cancel')); } - $form_gui->setFormAction($ilCtrl->getFormAction($this)); + $form_gui->setFormAction($this->ctrl->getFormAction($this)); return $form_gui; } public function save(): void { - $tpl = $this->tpl; - $lng = $this->lng; - $form = $this->initForm(); + if ($form->checkInput()) { $obj = new ilBookingSchedule(); $this->formToObject($form, $obj); $obj->save(); - $this->tpl->setOnScreenMessage('success', $lng->txt("book_schedule_added")); - $this->render(); - } else { - $form->setValuesByPost(); - $tpl->setContent($form->getHTML()); + $this->tpl->setOnScreenMessage( + ilGlobalTemplateInterface::MESSAGE_TYPE_SUCCESS, + $this->lng->txt('book_schedule_added'), + true + ); + $this->ctrl->redirect($this, 'render'); + return; } + + $form->setValuesByPost(); + $this->tpl->setContent($form->getHTML()); } public function update(): void { - $tpl = $this->tpl; - $lng = $this->lng; - $form = $this->initForm('edit', $this->schedule_id); + if ($form->checkInput()) { $obj = new ilBookingSchedule($this->schedule_id); $this->formToObject($form, $obj); $obj->update(); - $this->tpl->setOnScreenMessage('success', $lng->txt("book_schedule_updated")); - $this->render(); - } else { - $form->setValuesByPost(); - $tpl->setContent($form->getHTML()); + $this->tpl->setOnScreenMessage( + ilGlobalTemplateInterface::MESSAGE_TYPE_SUCCESS, + $this->lng->txt('book_schedule_updated'), + true + ); + $this->ctrl->redirect($this, 'render'); + return; } - } - /** - * Set form data into schedule object - */ - protected function formToObject( - ilPropertyFormGUI $form, - ilBookingSchedule $schedule - ): void { - $ilObjDataCache = $this->obj_data_cache; + $form->setValuesByPost(); + $this->tpl->setContent($form->getHTML()); + } - $schedule->setTitle($form->getInput("title")); - $schedule->setPoolId($ilObjDataCache->lookupObjId($this->ref_id)); + protected function formToObject(ilPropertyFormGUI $form, ilBookingSchedule $schedule): void + { + $schedule->setTitle($form->getInput('title')); + $schedule->setPoolId($this->obj_data_cache->lookupObjId($this->ref_id)); - $from = $form->getItemByPostVar("from"); + $from = $form->getItemByPostVar('from'); if ($from !== null) { $schedule->setAvailabilityFrom($from->getDate()); } - $to = $form->getItemByPostVar("to"); + $to = $form->getItemByPostVar('to'); if ($to !== null) { $schedule->setAvailabilityTo($to->getDate()); } - switch ($form->getInput("deadline_opts")) { - case "slot_start": - $schedule->setDeadline(0); - break; - - case "hours": - $schedule->setDeadline($form->getInput("deadline")); - break; - - case "slot_end": - $schedule->setDeadline(-1); - break; - } - - /* - if($form->getInput("type") == "flexible") - { - $schedule->setRaster($form->getInput("raster")); - $schedule->setMinRental($form->getInput("rent_min")); - $schedule->setMaxRental($form->getInput("rent_max")); - $schedule->setAutoBreak($form->getInput("break")); - } - else - { - $schedule->setRaster(NULL); - $schedule->setMinRental(NULL); - $schedule->setMaxRental(NULL); - $schedule->setAutoBreak(NULL); - } - */ + match ($form->getInput('deadline_opts')) { + 'slot_start' => $schedule->setDeadline(0), + 'hours' => $schedule->setDeadline($form->getInput('deadline')), + 'slot_end' => $schedule->setDeadline(-1), + }; - $days = $form->getInput("days"); - $schedule->setDefinitionBySlots($days); + $schedule->setDefinitionBySlots($form->getInput('days')); } - /** - * Confirm delete - */ - public function confirmDelete(): void + private function configureScheduleTable(ScheduleManager $schedule_manager): ScheduleTable { - $ilCtrl = $this->ctrl; - $lng = $this->lng; - $tpl = $this->tpl; - $ilHelp = $this->help; - - $ilHelp->setSubScreenId("delete"); - + return new ScheduleTable( + $this->ctrl, + $this->lng, + $this->tpl, + $this->ui_factory, + $this->ui_renderer, + $this->refinery, + $this->access, + $this->http, + $schedule_manager, + $this->ref_id + ); + } - $conf = new ilConfirmationGUI(); - $conf->setFormAction($ilCtrl->getFormAction($this)); - $conf->setHeaderText($lng->txt('book_confirm_delete')); + private function checkForInfoMessageAboutMissingBookableItems(ScheduleManager $schedule_manager, int $pool_id): void + { + $schedule_data = $schedule_manager->getScheduleData(); - $type = new ilBookingSchedule($this->schedule_id); - $conf->addItem('schedule_id', $this->schedule_id, $type->getTitle()); - $conf->setConfirm($lng->txt('delete'), 'delete'); - $conf->setCancel($lng->txt('cancel'), 'render'); + if ($schedule_data === [] || ilBookingObject::getList($pool_id) !== []) { + return; + } - $tpl->setContent($conf->getHTML()); + $this->tpl->setOnScreenMessage(ilGlobalTemplateInterface::MESSAGE_TYPE_INFO, $this->lng->txt('book_type_warning')); } - /** - * Delete schedule - */ - public function delete(): void + private function getTableActionUrlBuilder(): URLBuilder { - $ilCtrl = $this->ctrl; - $lng = $this->lng; - - $obj = new ilBookingSchedule($this->schedule_id); - $obj->delete(); - - $this->tpl->setOnScreenMessage('success', $lng->txt('book_schedule_deleted'), true); - $ilCtrl->redirect($this, 'render'); + return new URLBuilder($this->data_factory->uri( + ILIAS_HTTP_PATH . '/' . $this->ctrl->getLinkTargetByClass(self::class, 'executeTableAction') + )); } } diff --git a/components/ILIAS/BookingManager/Schedule/class.ilBookingSchedulesTableGUI.php b/components/ILIAS/BookingManager/Schedule/class.ilBookingSchedulesTableGUI.php deleted file mode 100755 index c4e914241c69..000000000000 --- a/components/ILIAS/BookingManager/Schedule/class.ilBookingSchedulesTableGUI.php +++ /dev/null @@ -1,119 +0,0 @@ - - */ -class ilBookingSchedulesTableGUI extends ilTable2GUI -{ - protected \ILIAS\BookingManager\InternalGUIService $gui; - protected \ILIAS\BookingManager\InternalDomainService $domain; - protected \ILIAS\BookingManager\Access\AccessManager $access; - protected ilObjectDataCache $obj_data_cache; - protected int $ref_id; - - public function __construct( - object $a_parent_obj, - string $a_parent_cmd, - int $a_ref_id - ) { - global $DIC; - - $this->ctrl = $DIC->ctrl(); - $this->lng = $DIC->language(); - $this->access = $DIC->bookingManager()->internal()->domain()->access(); - $this->obj_data_cache = $DIC["ilObjDataCache"]; - $ilCtrl = $DIC->ctrl(); - $ilObjDataCache = $DIC["ilObjDataCache"]; - - $this->ref_id = $a_ref_id; - $this->setId("bksd"); - - parent::__construct($a_parent_obj, $a_parent_cmd); - - $this->addColumn($this->lng->txt("title"), "title"); - $this->addColumn($this->lng->txt("book_is_used")); - $this->addColumn($this->lng->txt("actions")); - - $this->setEnableHeader(true); - $this->setFormAction($ilCtrl->getFormAction($a_parent_obj, $a_parent_cmd)); - $this->setRowTemplate("tpl.booking_schedule_row.html", "components/ILIAS/BookingManager"); - $this->domain = $DIC - ->bookingManager() - ->internal() - ->domain(); - $this->gui = $DIC - ->bookingManager() - ->internal() - ->gui(); - - $this->getItems($ilObjDataCache->lookupObjId($this->ref_id)); - } - - /** - * Build summary item rows for given object and filter(s) - * @param int $a_pool_id (aka parent obj id) - */ - public function getItems(int $a_pool_id): void - { - $data = $this->domain->schedules($a_pool_id)->getScheduleData(); - - $this->setMaxCount(count($data)); - $this->setData($data); - } - - protected function fillRow(array $a_set): void - { - $lng = $this->lng; - $ilCtrl = $this->ctrl; - $ui_factory = $this->gui->ui()->factory(); - $ui_renderer = $this->gui->ui()->renderer(); - - $this->tpl->setVariable("TXT_TITLE", $a_set["title"]); - - if ($a_set["is_used"]) { - $this->tpl->setVariable("TXT_IS_USED", $lng->txt("yes")); - } else { - $this->tpl->setVariable("TXT_IS_USED", $lng->txt("no")); - } - - $ilCtrl->setParameter($this->parent_obj, 'schedule_id', $a_set['booking_schedule_id']); - - $actions = []; - - if ($this->access->canManageSettings($this->ref_id)) { - $actions[] = $ui_factory->link()->standard( - $lng->txt('edit'), - $ilCtrl->getLinkTarget($this->parent_obj, 'edit') - ); - - if (!$a_set["is_used"]) { - $actions[] = $ui_factory->link()->standard( - $lng->txt('delete'), - $ilCtrl->getLinkTarget($this->parent_obj, 'confirmDelete') - ); - } - } - - if (count($actions) > 0) { - $dd = $ui_factory->dropdown()->standard($actions); - $this->tpl->setVariable("LAYER", $ui_renderer->render($dd)); - } - } -} diff --git a/components/ILIAS/BookingManager/classes/class.ilObjBookingPoolGUI.php b/components/ILIAS/BookingManager/classes/class.ilObjBookingPoolGUI.php index 3f7db02a6b79..982ad66a56f0 100755 --- a/components/ILIAS/BookingManager/classes/class.ilObjBookingPoolGUI.php +++ b/components/ILIAS/BookingManager/classes/class.ilObjBookingPoolGUI.php @@ -424,8 +424,6 @@ public function addExternalEditFormCustom(ilPropertyFormGUI $form): void */ protected function setTabs(): void { - $ilUser = $this->user; - /** @var ilObjBookingPool $pool */ $pool = $this->object; @@ -434,12 +432,12 @@ protected function setTabs(): void } if ($this->checkPermissionBool('read')) { - if ($ilUser->getId() !== ANONYMOUS_USER_ID) { + if (!$this->user->isAnonymous()) { if ($pool->getScheduleType() === ilObjBookingPool::TYPE_NO_SCHEDULE_PREFERENCES) { $this->tabs_gui->addTab( - "preferences", - $this->lng->txt("book_pref_overview"), - $this->ctrl->getLinkTargetByClass("ilbookingpreferencesgui", "") + 'preferences', + $this->lng->txt('book_pref_overview'), + $this->ctrl->getLinkTargetByClass(ilBookingPreferencesGUI::class, '') ); } @@ -447,64 +445,67 @@ protected function setTabs(): void $this->checkPermissionBool('write')) { $this->tabs_gui->addTab( "render", - $this->lng->txt("book_booking_objects"), - $this->ctrl->getLinkTarget($this, "render") + $this->lng->txt('book_booking_objects'), + $this->ctrl->getLinkTarget($this, 'render') ); } } - if ($ilUser->getId() !== ANONYMOUS_USER_ID || $this->object->hasPublicLog()) { + if ( + ($pool->hasPublicLog() && $this->checkPermissionBool('read')) + || $this->checkPermissionBool('manage_own_reservations') + || $this->checkPermissionBool('manage_all_reservations') + ) { $this->tabs_gui->addTab( - "log", - $this->lng->txt("book_log"), - $this->ctrl->getLinkTargetByClass("ilbookingreservationsgui", "") + 'log', + $this->lng->txt('book_log'), + $this->ctrl->getLinkTargetByClass(ilBookingReservationsGUI::class, '') ); } $this->tabs_gui->addTab( - "info", - $this->lng->txt("info_short"), - $this->ctrl->getLinkTarget($this, "infoscreen") + 'info', + $this->lng->txt('info_short'), + $this->ctrl->getLinkTarget($this, 'infoscreen') ); } if ($this->checkPermissionBool('write')) { /* $this->tabs_gui->addTab( - "settings", - $this->lng->txt("settings"), - $this->ctrl->getLinkTarget($this, "edit") + 'settings', + $this->lng->txt('settings'), + $this->ctrl->getLinkTarget($this, 'edit') );*/ $this->tabs_gui->addTab( - "settings", - $this->lng->txt("settings"), - $this->ctrl->getLinkTargetByClass(SettingsGUI::class, "") + 'settings', + $this->lng->txt('settings'), + $this->ctrl->getLinkTargetByClass(SettingsGUI::class, '') ); - if ($this->object->getScheduleType() === ilObjBookingPool::TYPE_FIX_SCHEDULE) { + if ($pool->getScheduleType() === ilObjBookingPool::TYPE_FIX_SCHEDULE) { $this->tabs_gui->addTab( - "schedules", - $this->lng->txt("book_schedules"), - $this->ctrl->getLinkTargetByClass("ilbookingschedulegui", "render") + 'schedules', + $this->lng->txt('book_schedules'), + $this->ctrl->getLinkTargetByClass(ilBookingScheduleGUI::class, 'render') ); } $this->tabs_gui->addTab( - "participants", - $this->lng->txt("participants"), - $this->ctrl->getLinkTargetByClass("ilbookingparticipantgui", "render") + 'participants', + $this->lng->txt('participants'), + $this->ctrl->getLinkTargetByClass(ilBookingParticipantGUI::class, 'render') ); // meta data - $mdgui = new ilObjectMetaDataGUI($this->object, "bobj"); - $mdtab = $mdgui->getTab(); + $mdtab = (new ilObjectMetaDataGUI($pool, 'bobj'))->getTab(); if ($mdtab) { $this->tabs_gui->addTarget( - "meta_data", + 'meta_data', $mdtab, - "", - "ilobjectmetadatagui" + '', + ilObjectMetaDataGUI::class ); } } @@ -512,9 +513,9 @@ protected function setTabs(): void if ($this->checkPermissionBool('edit_permission')) { $this->tabs_gui->addTab( - "perm_settings", - $this->lng->txt("perm_settings"), - $this->ctrl->getLinkTargetByClass("ilpermissiongui", "perm") + 'perm_settings', + $this->lng->txt('perm_settings'), + $this->ctrl->getLinkTargetByClass(ilPermissionGUI::class, 'perm') ); } } From 5f786564bbae51345c8237b07d0f57529f50f397 Mon Sep 17 00:00:00 2001 From: Matheus Zych Date: Fri, 29 May 2026 11:33:21 +0200 Subject: [PATCH 5/8] BookingPool: Adds Booking Pool Schedule Table See: https://docu.ilias.de/go/wiki/wpage_7320_1357 Adds new KS Booking Pool Schedule table. --- .../Action/ScheduleTableActionsFactory.php | 99 +++++++++ .../Action/ScheduleTableDeleteAction.php | 162 ++++++++++++++ .../Table/Action/ScheduleTableEditAction.php | 107 +++++++++ .../src/Schedule/Table/ScheduleTable.php | 209 ++++++++++++++++++ 4 files changed, 577 insertions(+) create mode 100644 components/ILIAS/BookingManager/src/Schedule/Table/Action/ScheduleTableActionsFactory.php create mode 100644 components/ILIAS/BookingManager/src/Schedule/Table/Action/ScheduleTableDeleteAction.php create mode 100644 components/ILIAS/BookingManager/src/Schedule/Table/Action/ScheduleTableEditAction.php create mode 100644 components/ILIAS/BookingManager/src/Schedule/Table/ScheduleTable.php diff --git a/components/ILIAS/BookingManager/src/Schedule/Table/Action/ScheduleTableActionsFactory.php b/components/ILIAS/BookingManager/src/Schedule/Table/Action/ScheduleTableActionsFactory.php new file mode 100644 index 000000000000..507bb1a236f6 --- /dev/null +++ b/components/ILIAS/BookingManager/src/Schedule/Table/Action/ScheduleTableActionsFactory.php @@ -0,0 +1,99 @@ +ctrl, + $this->lng, + $this->tpl, + $this->ui_factory, + $this->ui_renderer, + $this->refinery, + $this->http, + [ + self::ACTION_DELETE => $this->getDeleteAction(), + self::ACTION_EDIT => $this->getEditAction(), + ] + ); + } + + protected function getDeleteAction(): TableAction + { + return new ScheduleTableDeleteAction( + $this->ui_factory, + $this->ui_renderer, + $this->lng, + $this->access, + $this->ctrl, + $this->tpl, + $this->http, + $this->schedule_manager, + $this->ref_id, + ); + } + + protected function getEditAction(): TableAction + { + return new ScheduleTableEditAction( + $this->ui_factory, + $this->lng, + $this->access, + $this->ctrl, + $this->http, + $this->ref_id, + ); + } +} diff --git a/components/ILIAS/BookingManager/src/Schedule/Table/Action/ScheduleTableDeleteAction.php b/components/ILIAS/BookingManager/src/Schedule/Table/Action/ScheduleTableDeleteAction.php new file mode 100644 index 000000000000..094f255465f0 --- /dev/null +++ b/components/ILIAS/BookingManager/src/Schedule/Table/Action/ScheduleTableDeleteAction.php @@ -0,0 +1,162 @@ + + */ +class ScheduleTableDeleteAction implements TableAction +{ + use TableActionModalTrait; + + public const string ACTION_ID = 'delete'; + + public const string ACTION_LABEL = 'delete'; + + public function __construct( + private readonly UIFactory $ui_factory, + private readonly UIRenderer $ui_renderer, + private readonly ilLanguage $lng, + private readonly AccessManager $access, + private readonly ilCtrlInterface $ctrl, + private readonly ilGlobalTemplateInterface $tpl, + private readonly HttpService $http, + private readonly ScheduleManager $schedule_manager, + private readonly int $ref_id, + ) { + } + + public function getActionId(): string + { + return self::ACTION_ID; + } + + public function getActionLabel(): string + { + return self::ACTION_LABEL; + } + + public function isAvailable(): bool + { + return $this->access->canManageSettings($this->ref_id); + } + + public function getTableAction( + URLBuilder $url_builder, + URLBuilderToken $row_id_token, + URLBuilderToken $action_token, + URLBuilderToken $action_type_token + ): Action { + return $this->ui_factory->table()->action()->standard( + $this->lng->txt(self::ACTION_LABEL), + $url_builder + ->withParameter($action_token, self::ACTION_ID) + ->withParameter($action_type_token, self::SHOW_MODAL_ACTION), + $row_id_token + )->withAsync(); + } + + public function allowActionForRecord(mixed $record): bool + { + return $this->access->canManageSettings($this->ref_id) && !$record['is_used']; + } + + public function getModal( + URLBuilder $url_builder, + array $selected_records, + bool $all_records_selected + ): ?Modal { + return $this->ui_factory->modal()->interruptive( + $this->lng->txt('confirm'), + $this->lng->txt('book_confirm_delete'), + $url_builder->buildURI()->__toString() + )->withAffectedItems( + array_map( + fn(array $record): InterruptiveItem => $this->ui_factory->modal()->interruptiveItem()->standard( + (string) $record['booking_schedule_id'], + $record['title'] ?? '' + ), + $selected_records + ) + )->withActionButtonLabel($this->lng->txt('delete')); + } + + public function onSubmit( + URLBuilder $url_builder, + array $selected_records, + bool $all_records_selected + ): ?Modal { + if (!$this->access->canManageSettings($this->ref_id)) { + $this->showErrorMessage($this->lng->txt('no_permission')); + return null; + } + + $selected_records = array_filter( + $selected_records, + static fn(array $record): bool => !($record['is_used'] ?? true) + ); + + foreach ($selected_records as $record) { + (new ilBookingSchedule($record['booking_schedule_id']))->delete(); + } + + if ($selected_records !== []) { + $this->showSuccessMessage($this->lng->txt('book_schedule_deleted')); + } + $this->ctrl->redirectByClass(ilBookingScheduleGUI::class, 'render'); + return null; + } + + protected function resolveRecords(?array $selected_ids = null): array + { + $schedules = $this->schedule_manager->getScheduleData(); + + if ($selected_ids === null) { + return $schedules; + } + + return array_filter( + array_map( + static fn(int $id): ?array => $schedules[$id] ?? null, + $selected_ids + ) + ); + } +} diff --git a/components/ILIAS/BookingManager/src/Schedule/Table/Action/ScheduleTableEditAction.php b/components/ILIAS/BookingManager/src/Schedule/Table/Action/ScheduleTableEditAction.php new file mode 100644 index 000000000000..2c03098cf4e2 --- /dev/null +++ b/components/ILIAS/BookingManager/src/Schedule/Table/Action/ScheduleTableEditAction.php @@ -0,0 +1,107 @@ + + */ +class ScheduleTableEditAction implements TableAction +{ + public const string ACTION_ID = 'edit'; + + public const string ACTION_LABEL = 'edit'; + + public function __construct( + private readonly UIFactory $ui_factory, + private readonly ilLanguage $lng, + private readonly AccessManager $access, + private readonly ilCtrlInterface $ctrl, + private readonly HttpService $http, + private readonly int $ref_id + ) { + } + + public function getActionId(): string + { + return self::ACTION_ID; + } + + public function getActionLabel(): string + { + return self::ACTION_LABEL; + } + + public function isAvailable(): bool + { + return $this->access->canManageSettings($this->ref_id); + } + + public function getTableAction( + URLBuilder $url_builder, + URLBuilderToken $row_id_token, + URLBuilderToken $action_token, + URLBuilderToken $action_type_token + ): Action { + return $this->ui_factory->table()->action()->single( + $this->lng->txt(self::ACTION_LABEL), + $url_builder + ->withParameter($action_token, self::ACTION_ID) + ->withParameter($action_type_token, 'edit'), // TODO: Check for constant. + $row_id_token + ); + } + + public function onExecute( + URLBuilder $url_builder, + URLBuilderToken $row_id_token, + URLBuilderToken $action_token, + URLBuilderToken $action_type_token + ): mixed { + $this->ctrl->setParameterByClass( + ilBookingScheduleGUI::class, + 'schedule_id', + $this->http->resolveRowParameter($row_id_token->getName()) + ); + $this->ctrl->redirectByClass(ilBookingScheduleGUI::class, 'edit'); + + return null; + } + + /** + * @param ScheduleRecord $record + */ + public function allowActionForRecord(mixed $record): bool + { + return $this->access->canManageSettings($this->ref_id); + } +} diff --git a/components/ILIAS/BookingManager/src/Schedule/Table/ScheduleTable.php b/components/ILIAS/BookingManager/src/Schedule/Table/ScheduleTable.php new file mode 100644 index 000000000000..cb5fc69985db --- /dev/null +++ b/components/ILIAS/BookingManager/src/Schedule/Table/ScheduleTable.php @@ -0,0 +1,209 @@ + + */ + public function getComponents(URLBuilder $url_builder): array + { + return [ + $this->ui_factory->table()->data($this, $this->lng->txt('book_schedules'), $this->getColumns()) + ->withActions($this->getTableActions()->getEnabledActions(...$this->acquireParameters($url_builder))) + ->withRequest($this->http->getRequest()) + ->withId($this->getTableId()) + ]; + } + + public function getTotalRowCount( + mixed $additional_viewcontrol_data, + mixed $filter_data, + mixed $additional_parameters + ): ?int { + return count($this->loadRecords($filter_data)); + } + + public function getRows( + DataRowBuilder $row_builder, + array $visible_column_ids, + Range $range, + Order $order, + mixed $additional_viewcontrol_data, + mixed $filter_data, + mixed $additional_parameters + ): Generator { + $records = $this->limitRecords($range, $this->sortRecords($order, $this->loadRecords($filter_data))); + + foreach ($records as $record) { + yield $this->getTableActions()->onDataRow( + $row_builder->buildDataRow( + (string) $record['booking_schedule_id'], + [ + 'title' => $record['title'] ?? '', + 'is_used' => (bool) ($record['is_used'] ?? false), + ] + ), + $record + ); + } + } + + protected function loadRecords(mixed $filter_data): array + { + return $this->schedule_manager->getScheduleData(); + } + + protected function sortRecords(Order $order, array $records): array + { + $order_data = $order->get(); + if ($order_data === []) { + return $records; + } + + foreach ($order_data as $key => $value) { + $order_direction = $value === Order::DESC ? -1 : 1; + $callable = match ($key) { + 'title' => static fn(array $a, array $b): int => strcmp($a['title'], $b['title']) * $order_direction, + 'is_used' => static fn(array $a, array $b): int => (($a['is_used'] ?? false) <=> ($b['is_used'] ?? false)) * -$order_direction, + default => null, + }; + + if ($callable === null) { + continue; + } + + usort($records, $callable); + } + + return $records; + } + + protected function limitRecords(Range $range, array $records): array + { + return array_slice($records, $range->getStart(), $range->getLength()); + } + + /** + * @return array + */ + private function getColumns(): array + { + $column_factory = $this->ui_factory->table()->column(); + $icon_factory = $this->ui_factory->symbol()->icon(); + return [ + 'title' => $column_factory->text($this->lng->txt('title'))->withIsSortable(true), + 'is_used' => $column_factory->boolean( + $this->lng->txt('book_is_used'), + $icon_factory->custom( + ilUtil::getImagePath('standard/icon_ok.svg'), + $this->lng->txt('yes') + ), + $icon_factory->custom( + ilUtil::getImagePath('standard/icon_not_ok.svg'), + $this->lng->txt('no') + ) + )->withIsSortable(true), + ]; + } + + /** + * @return array{URLBuilder, URLBuilderToken, URLBuilderToken, URLBuilderToken} + */ + protected function acquireParameters(URLBuilder $url_builder): array + { + return $url_builder->acquireParameters( + [self::ID], + self::ROW_ID_PARAMETER, + self::ACTION_PARAMETER, + self::ACTION_TYPE_PARAMETER + ); + } + + protected function getTableActions(): TableActions + { + return (new ScheduleTableActionsFactory( + $this->ctrl, + $this->lng, + $this->tpl, + $this->ui_factory, + $this->ui_renderer, + $this->refinery, + $this->access, + $this->http, + $this->schedule_manager, + $this->ref_id, + ))->getTableActions(); + } +} From f5711b1f3d865cc0d6f20c2891ce28474a6ab210 Mon Sep 17 00:00:00 2001 From: Matheus Zych Date: Fri, 29 May 2026 11:33:37 +0200 Subject: [PATCH 6/8] BookingPool: Adds Booking Pool Participant Table See: https://docu.ilias.de/go/wiki/wpage_7320_1357 Adds new KS Booking Pool Participant table. --- .../src/Participant/ParticipantRepository.php | 51 ++++ .../Action/ParticipantTableActionsFactory.php | 118 ++++++++ ...rticipantTableBookForParticipantAction.php | 108 +++++++ .../Action/ParticipantTableDeleteAction.php | 162 +++++++++++ .../ParticipantTableEditBookingAction.php | 133 +++++++++ .../Participant/Table/ParticipantTable.php | 266 ++++++++++++++++++ 6 files changed, 838 insertions(+) create mode 100644 components/ILIAS/BookingManager/src/Participant/ParticipantRepository.php create mode 100644 components/ILIAS/BookingManager/src/Participant/Table/Action/ParticipantTableActionsFactory.php create mode 100644 components/ILIAS/BookingManager/src/Participant/Table/Action/ParticipantTableBookForParticipantAction.php create mode 100644 components/ILIAS/BookingManager/src/Participant/Table/Action/ParticipantTableDeleteAction.php create mode 100644 components/ILIAS/BookingManager/src/Participant/Table/Action/ParticipantTableEditBookingAction.php create mode 100644 components/ILIAS/BookingManager/src/Participant/Table/ParticipantTable.php diff --git a/components/ILIAS/BookingManager/src/Participant/ParticipantRepository.php b/components/ILIAS/BookingManager/src/Participant/ParticipantRepository.php new file mode 100644 index 000000000000..7a310cb94f7c --- /dev/null +++ b/components/ILIAS/BookingManager/src/Participant/ParticipantRepository.php @@ -0,0 +1,51 @@ +database->manipulateF( + "DELETE booking_reservation FROM booking_reservation + INNER JOIN booking_object ON booking_object.booking_object_id = booking_reservation.object_id + WHERE booking_reservation.user_id = %s AND booking_object.pool_id = %s", + [ilDBConstants::T_INTEGER, ilDBConstants::T_INTEGER], + [$user_id, $pool_id] + ); + + $this->database->manipulateF( + "DELETE FROM booking_member WHERE user_id = %s AND booking_pool_id = %s", + [ilDBConstants::T_INTEGER, ilDBConstants::T_INTEGER], + [$user_id, $pool_id] + ); + + return true; + } +} diff --git a/components/ILIAS/BookingManager/src/Participant/Table/Action/ParticipantTableActionsFactory.php b/components/ILIAS/BookingManager/src/Participant/Table/Action/ParticipantTableActionsFactory.php new file mode 100644 index 000000000000..c3d138e07dad --- /dev/null +++ b/components/ILIAS/BookingManager/src/Participant/Table/Action/ParticipantTableActionsFactory.php @@ -0,0 +1,118 @@ +ctrl, + $this->lng, + $this->tpl, + $this->ui_factory, + $this->ui_renderer, + $this->refinery, + $this->http, + [ + self::ACTION_BOOK_FOR_PARTICIPANT => $this->getBookForParticipantAction(), + self::ACTION_DELETE => $this->getDeleteAction(), + self::ACTION_EDIT_BOOKING => $this->getEditBookingAction(), + ] + ); + } + + protected function getBookForParticipantAction(): TableAction + { + return new ParticipantTableBookForParticipantAction( + $this->ui_factory, + $this->lng, + $this->access, + $this->ctrl, + $this->http, + $this->ref_id, + $this->pool_id, + ); + } + + protected function getDeleteAction(): TableAction + { + return new ParticipantTableDeleteAction( + $this->ui_factory, + $this->ui_renderer, + $this->lng, + $this->access, + $this->ctrl, + $this->tpl, + $this->http, + $this->participant_repository, + $this->ref_id, + $this->pool_id, + ); + } + + protected function getEditBookingAction(): TableAction + { + return new ParticipantTableEditBookingAction( + $this->ui_factory, + $this->lng, + $this->access, + $this->ctrl, + $this->http, + $this->ref_id, + $this->pool_id, + ); + } +} diff --git a/components/ILIAS/BookingManager/src/Participant/Table/Action/ParticipantTableBookForParticipantAction.php b/components/ILIAS/BookingManager/src/Participant/Table/Action/ParticipantTableBookForParticipantAction.php new file mode 100644 index 000000000000..1dda8e5929dd --- /dev/null +++ b/components/ILIAS/BookingManager/src/Participant/Table/Action/ParticipantTableBookForParticipantAction.php @@ -0,0 +1,108 @@ +, obj_count: int, object_ids: array} + * @implements TableAction + */ +class ParticipantTableBookForParticipantAction implements TableAction +{ + public const string ACTION_ID = 'book_for_participant'; + + public const string ACTION_LABEL = 'book_assign_object'; + + public function __construct( + private readonly UIFactory $ui_factory, + private readonly ilLanguage $lng, + private readonly AccessManager $access, + private readonly ilCtrlInterface $ctrl, + private readonly HttpService $http, + private readonly int $ref_id, + private readonly int $pool_id + ) { + } + + public function getActionId(): string + { + return self::ACTION_ID; + } + + public function getActionLabel(): string + { + return self::ACTION_LABEL; + } + + public function isAvailable(): bool + { + return $this->access->canManageParticipants($this->ref_id); + } + + public function getTableAction( + URLBuilder $url_builder, + URLBuilderToken $row_id_token, + URLBuilderToken $action_token, + URLBuilderToken $action_type_token + ): Action { + return $this->ui_factory->table()->action()->single( + $this->lng->txt('book_assign_object'), + $url_builder + ->withParameter($action_token, self::ACTION_ID) + ->withParameter($action_type_token, 'book'), + $row_id_token + ); + } + + public function onExecute( + URLBuilder $url_builder, + URLBuilderToken $row_id_token, + URLBuilderToken $action_token, + URLBuilderToken $action_type_token + ): mixed { + $user_id = $this->http->resolveRowParameter($row_id_token->getName()); + + $this->ctrl->setParameterByClass(ilBookingParticipantGUI::class, 'bkusr', (string) $user_id); + $this->ctrl->redirectByClass(ilBookingParticipantGUI::class, 'assignObjects'); + + return null; + } + + /** + * @param ParticipantRecord $record + */ + public function allowActionForRecord(mixed $record): bool + { + $obj_count = (int) ($record['obj_count'] ?? 0); + return $obj_count < ilBookingObject::getNumberOfObjectsForPool($this->pool_id); + } +} diff --git a/components/ILIAS/BookingManager/src/Participant/Table/Action/ParticipantTableDeleteAction.php b/components/ILIAS/BookingManager/src/Participant/Table/Action/ParticipantTableDeleteAction.php new file mode 100644 index 000000000000..4e8a98f98682 --- /dev/null +++ b/components/ILIAS/BookingManager/src/Participant/Table/Action/ParticipantTableDeleteAction.php @@ -0,0 +1,162 @@ +, obj_count: int, object_ids: array} + * @implements TableAction + */ +class ParticipantTableDeleteAction implements TableAction +{ + use TableActionModalTrait; + + public const string ACTION_ID = 'delete'; + + public const string ACTION_LABEL = 'book_remove_participants'; + + public function __construct( + private readonly UIFactory $ui_factory, + private readonly UIRenderer $ui_renderer, + private readonly ilLanguage $lng, + private readonly AccessManager $access, + private readonly ilCtrlInterface $ctrl, + private readonly ilGlobalTemplateInterface $tpl, + private readonly HttpService $http, + private readonly ParticipantRepository $participant_repository, + private readonly int $ref_id, + private readonly int $pool_id + ) { + } + + public function getActionId(): string + { + return self::ACTION_ID; + } + + public function getActionLabel(): string + { + return self::ACTION_LABEL; + } + + public function isAvailable(): bool + { + return $this->access->canManageParticipants($this->ref_id); + } + + public function getTableAction( + URLBuilder $url_builder, + URLBuilderToken $row_id_token, + URLBuilderToken $action_token, + URLBuilderToken $action_type_token + ): Action { + return $this->ui_factory->table()->action()->standard( + $this->lng->txt($this->getActionLabel()), + $url_builder + ->withParameter($action_token, $this->getActionId()) + ->withParameter($action_type_token, self::SHOW_MODAL_ACTION), + $row_id_token + )->withAsync(); + } + + /** + * @param ParticipantRecord $record + */ + public function allowActionForRecord(mixed $record): bool + { + return true; + } + + public function getModal( + URLBuilder $url_builder, + array $selected_records, + bool $all_records_selected + ): ?Modal { + return $this->ui_factory->modal()->interruptive( + $this->lng->txt('confirm'), + $this->lng->txt('book_confirm_remove_participant'), + $url_builder->buildURI()->__toString() + )->withAffectedItems( + array_map( + fn(array $record): InterruptiveItem => $this->ui_factory->modal()->interruptiveItem()->standard( + (string) $record['user_id'], + (string) ($record['name'] ?? '') + ), + $selected_records + ) + )->withActionButtonLabel($this->lng->txt('remove')); + } + + public function onSubmit( + URLBuilder $url_builder, + array $selected_records, + bool $all_records_selected + ): ?Modal { + if (!$this->access->canManageParticipants($this->ref_id)) { + $this->showErrorMessage($this->lng->txt('no_permission')); + return null; + } + + foreach ($selected_records as $record) { + $this->participant_repository->delete((int) $record['user_id'], $this->pool_id); + } + + if ($selected_records !== []) { + $this->showSuccessMessage($this->lng->txt('book_participant_removed')); + } + $this->ctrl->redirectByClass(ilBookingParticipantGUI::class, 'render'); + + return null; + } + + protected function resolveRecords(?array $selected_ids = null): array + { + $all_participants = ilBookingParticipant::getList($this->pool_id); + + if ($selected_ids === null) { + return array_values($all_participants); + } + + return array_filter( + array_map( + fn(int $id): ?array => $all_participants["{$this->pool_id}_{$id}"] ?? null, + $selected_ids + ) + ); + } +} diff --git a/components/ILIAS/BookingManager/src/Participant/Table/Action/ParticipantTableEditBookingAction.php b/components/ILIAS/BookingManager/src/Participant/Table/Action/ParticipantTableEditBookingAction.php new file mode 100644 index 000000000000..ea8eb722d1fc --- /dev/null +++ b/components/ILIAS/BookingManager/src/Participant/Table/Action/ParticipantTableEditBookingAction.php @@ -0,0 +1,133 @@ +, obj_count: int, object_ids: array} + * @implements TableAction + */ +class ParticipantTableEditBookingAction implements TableAction +{ + public const string ACTION_ID = 'edit_booking'; + + public const string ACTION_LABEL = 'book_deassign'; + + public function __construct( + private readonly UIFactory $ui_factory, + private readonly ilLanguage $lng, + private readonly AccessManager $access, + private readonly ilCtrlInterface $ctrl, + private readonly HttpService $http, + private readonly int $ref_id, + private readonly int $pool_id + ) { + } + + public function getActionId(): string + { + return self::ACTION_ID; + } + + public function getActionLabel(): string + { + return self::ACTION_LABEL; + } + + public function isAvailable(): bool + { + return $this->access->canManageParticipants($this->ref_id); + } + + public function getTableAction( + URLBuilder $url_builder, + URLBuilderToken $row_id_token, + URLBuilderToken $action_token, + URLBuilderToken $action_type_token + ): Action { + return $this->ui_factory->table()->action()->single( + $this->lng->txt('book_deassign'), + $url_builder + ->withParameter($action_token, self::ACTION_ID) + ->withParameter($action_type_token, 'edit'), + $row_id_token + ); + } + + public function onExecute( + URLBuilder $url_builder, + URLBuilderToken $row_id_token, + URLBuilderToken $action_token, + URLBuilderToken $action_type_token + ): mixed { + $this->ctrl->setParameterByClass( + ilBookingReservationsGUI::class, + 'user_id', + (int) $this->http->resolveRowParameter($row_id_token->getName()) + ); + $this->ctrl->redirectByClass(ilBookingReservationsGUI::class, 'log'); + + return null; + } + + /** + * @param ParticipantRecord $record + */ + public function allowActionForRecord(mixed $record): bool + { + $obj_count = (int) ($record['obj_count'] ?? 0); + return $obj_count > 0; + } + + private function getObjectCountForUser(int $user_id): int + { + $user_data = ilBookingParticipant::getList( + $this->pool_id, + ['user_id' => $user_id] + )["{$this->pool_id}_{$user_id}"] ?? null; + return (int) ($user_data['obj_count'] ?? 0); + } + + /** + * @return array + */ + private function getObjectIdsForUser(int $user_id): array + { + $user_data = ilBookingParticipant::getList( + $this->pool_id, + ['user_id' => $user_id] + )["{$this->pool_id}_{$user_id}"] ?? null; + return $user_data['object_ids'] ?? []; + } +} diff --git a/components/ILIAS/BookingManager/src/Participant/Table/ParticipantTable.php b/components/ILIAS/BookingManager/src/Participant/Table/ParticipantTable.php new file mode 100644 index 000000000000..9e8e3bea0ea8 --- /dev/null +++ b/components/ILIAS/BookingManager/src/Participant/Table/ParticipantTable.php @@ -0,0 +1,266 @@ +, obj_count: int, object_ids: array} + */ + public function __construct( + private readonly UIFactory $ui_factory, + private readonly UIRenderer $ui_renderer, + private readonly ilLanguage $lng, + private readonly HttpService $http, + private readonly ilUIService $ui_service, + private readonly ilCtrlInterface $ctrl, + private readonly ilGlobalTemplateInterface $tpl, + private readonly Refinery $refinery, + private readonly AccessManager $access, + private readonly ParticipantRepository $participant_repository, + private readonly int $ref_id, + private readonly int $pool_id, + ) { + } + + public function getTableId(): string + { + return self::ID; + } + + /** + * @return array + */ + public function getComponents(URLBuilder $url_builder): array + { + $filter = $this->getFilterComponent($url_builder->buildURI()->__toString()); + + $table = $this->ui_factory->table()->data($this, $this->lng->txt('participants'), $this->getColumns()) + ->withActions($this->getTableActions()->getEnabledActions(...$this->acquireParameters($url_builder))) + ->withRequest($this->http->getRequest()) + ->withId($this->getTableId()) + ->withFilter($this->ui_service->filter()->getData($filter)); + + return [$filter, $table]; + } + + public function getTotalRowCount( + mixed $additional_viewcontrol_data, + mixed $filter_data, + mixed $additional_parameters + ): ?int { + return count($this->loadRecords($filter_data)); + } + + public function getRows( + DataRowBuilder $row_builder, + array $visible_column_ids, + Range $range, + Order $order, + mixed $additional_viewcontrol_data, + mixed $filter_data, + mixed $additional_parameters + ): Generator { + $records = $this->limitRecords($range, $this->sortRecords($order, $this->loadRecords($filter_data))); + + foreach ($records as $record) { + yield $this->getTableActions()->onDataRow( + $row_builder->buildDataRow( + (string) $record['user_id'], + [ + 'name' => $record['name'] ?? '', + 'bookable_item' => $this->ui_renderer->render( + $this->ui_factory->listing()->unordered($record['object_title'] ?? []) + ), + ] + ), + $record + ); + } + } + + /** + * @return array + */ + private function getColumns(): array + { + $column_factory = $this->ui_factory->table()->column(); + return [ + 'name' => $column_factory->text($this->lng->txt('name'))->withIsSortable(true), + 'bookable_item' => $column_factory->text($this->lng->txt('book_bobj'))->withIsSortable(false), + ]; + } + + private function loadRecords(?array $filter_data): array + { + $filter = []; + + if (isset($filter_data['bookable_item_id']) && $filter_data['bookable_item_id'] !== '') { + $filter['object'] = (int) $filter_data['bookable_item_id']; + } + + if (isset($filter_data['bookable_item_title']) && $filter_data['bookable_item_title'] !== '') { + $filter['title'] = (string) $filter_data['bookable_item_title']; + } + + if (isset($filter_data['participant_id']) && $filter_data['participant_id'] !== '') { + $filter['user_id'] = (int) $filter_data['participant_id']; + } + + $filter_object = isset($filter['object']) ? (int) $filter['object'] : null; + if ($filter_object === -1) { + return array_filter( + ilBookingParticipant::getList($this->pool_id, $filter), + static fn(array $item): bool => ($item['obj_count'] ?? 0) === 0 + ); + } + + return ilBookingParticipant::getList($this->pool_id, $filter, $filter_object); + } + + protected function sortRecords(Order $order, array $records): array + { + $order_data = $order->get(); + if ($order_data === []) { + return $records; + } + + foreach ($order_data as $key => $value) { + $order_direction = $value === Order::DESC ? -1 : 1; + $callable = match ($key) { + 'name' => static fn(array $a, array $b): int => strcmp($a['name'], $b['name']) * $order_direction, + 'bookable_item' => static fn(array $a, array $b): int => strcmp($a['bookable_item'], $b['bookable_item']) * $order_direction, + default => null, + }; + + if ($callable === null) { + continue; + } + + usort($records, $callable); + } + + return $records; + } + + protected function limitRecords(Range $range, array $records): array + { + return array_slice($records, $range->getStart(), $range->getLength()); + } + + private function getFilterComponent(string $action): FilterComponent + { + $bookable_items = []; + foreach (ilBookingObject::getList($this->pool_id) as $item) { + $bookable_items[$item['booking_object_id']] = $item['title']; + } + + $field_factory = $this->ui_factory->input()->field(); + $filter_inputs = [ + 'bookable_item_id' => $field_factory->select( + $this->lng->txt('book_bobj'), + array_replace(['-1' => $this->lng->txt('book_no_objects')], $bookable_items) + ), + 'bookable_item_title' => $field_factory->text( + "{$this->lng->txt('book_bobj')} {$this->lng->txt('title')}/{$this->lng->txt('description')}" + ), + 'participant_id' => $field_factory->select( + $this->lng->txt('book_participant'), + ilBookingParticipant::getUserFilter($this->pool_id) + ), + ]; + + return $this->ui_service->filter()->standard( + "participant_filter_{$this->pool_id}", + $action, + $filter_inputs, + array_fill(0, count($filter_inputs), true), + true, + true + ); + } + + /** + * @return array{URLBuilder, URLBuilderToken, URLBuilderToken, URLBuilderToken} + */ + protected function acquireParameters(URLBuilder $url_builder): array + { + return $url_builder->acquireParameters( + [self::ID], + self::ROW_ID_PARAMETER, + self::ACTION_PARAMETER, + self::ACTION_TYPE_PARAMETER + ); + } + + protected function getTableActions(): TableActions + { + return (new ParticipantTableActionsFactory( + $this->ctrl, + $this->lng, + $this->tpl, + $this->ui_factory, + $this->ui_renderer, + $this->refinery, + $this->http, + $this->access, + $this->participant_repository, + $this->ref_id, + $this->pool_id, + ))->getTableActions(); + } +} From e89d36a5bb91d4c37214695b720f6236ecd18039 Mon Sep 17 00:00:00 2001 From: Matheus Zych Date: Fri, 29 May 2026 11:33:51 +0200 Subject: [PATCH 7/8] BookingPool: Adds Booking Pool Booking Table See: https://docu.ilias.de/go/wiki/wpage_7320_1357 Adds new KS Booking Pool Booking table. --- .../Action/BookingTableActionsFactory.php | 127 +++++ .../Action/BookingsTableCancelAction.php | 185 ++++++ .../Action/BookingsTableDeleteAction.php | 173 ++++++ .../Table/Action/BookingsTableMailAction.php | 154 +++++ .../src/Booking/Table/BookingsTable.php | 528 ++++++++++++++++++ .../Table/BookingsWithScheduleTable.php | 166 ++++++ .../Table/BookingsWithoutScheduleTable.php | 97 ++++ 7 files changed, 1430 insertions(+) create mode 100644 components/ILIAS/BookingManager/src/Booking/Table/Action/BookingTableActionsFactory.php create mode 100644 components/ILIAS/BookingManager/src/Booking/Table/Action/BookingsTableCancelAction.php create mode 100644 components/ILIAS/BookingManager/src/Booking/Table/Action/BookingsTableDeleteAction.php create mode 100644 components/ILIAS/BookingManager/src/Booking/Table/Action/BookingsTableMailAction.php create mode 100644 components/ILIAS/BookingManager/src/Booking/Table/BookingsTable.php create mode 100644 components/ILIAS/BookingManager/src/Booking/Table/BookingsWithScheduleTable.php create mode 100644 components/ILIAS/BookingManager/src/Booking/Table/BookingsWithoutScheduleTable.php diff --git a/components/ILIAS/BookingManager/src/Booking/Table/Action/BookingTableActionsFactory.php b/components/ILIAS/BookingManager/src/Booking/Table/Action/BookingTableActionsFactory.php new file mode 100644 index 000000000000..44502a36de95 --- /dev/null +++ b/components/ILIAS/BookingManager/src/Booking/Table/Action/BookingTableActionsFactory.php @@ -0,0 +1,127 @@ +ctrl, + $this->lng, + $this->tpl, + $this->ui_factory, + $this->ui_renderer, + $this->refinery, + $this->http, + [ + self::ACTION_CANCEL => $this->getCancelAction(), + self::ACTION_DELETE => $this->getDeleteAction(), + self::ACTION_MAIL => $this->getMailAction(), + ] + ); + } + + protected function getCancelAction(): BookingsTableCancelAction + { + return new BookingsTableCancelAction( + $this->access, + $this->ui_factory, + $this->ui_renderer, + $this->lng, + $this->reservation_repository, + $this->http, + $this->tpl, + $this->ctrl, + $this->booking_pool, + $this->bookings + ); + } + + protected function getDeleteAction(): BookingsTableDeleteAction + { + return new BookingsTableDeleteAction( + $this->access, + $this->ui_factory, + $this->ui_renderer, + $this->lng, + $this->reservation_repository, + $this->http, + $this->tpl, + $this->ctrl, + $this->booking_pool, + $this->bookings + ); + } + + protected function getMailAction(): BookingsTableMailAction + { + return new BookingsTableMailAction( + $this->access, + $this->ui_factory, + $this->ui_renderer, + $this->lng, + $this->reservation_repository, + $this->http, + $this->tpl, + $this->ctrl, + $this->booking_pool, + $this->bookings + ); + } +} diff --git a/components/ILIAS/BookingManager/src/Booking/Table/Action/BookingsTableCancelAction.php b/components/ILIAS/BookingManager/src/Booking/Table/Action/BookingsTableCancelAction.php new file mode 100644 index 000000000000..79f5e78d431c --- /dev/null +++ b/components/ILIAS/BookingManager/src/Booking/Table/Action/BookingsTableCancelAction.php @@ -0,0 +1,185 @@ +access->canManageReservationForUser($this->booking_pool->getRefId(), $record['user_id'])) { + return false; + } + + return + in_array($record['status'], [ilBookingReservation::STATUS_IN_USE, 0]) + && ($this->booking_pool->getScheduleType() !== ilObjBookingPool::TYPE_FIX_SCHEDULE || $record['date_from'] > time()); + } + + public function getTableAction( + URLBuilder $url_builder, + URLBuilderToken $row_id_token, + URLBuilderToken $action_token, + URLBuilderToken $action_type_token + ): Action { + return $this->ui_factory->table()->action()->standard( + $this->lng->txt($this->getActionLabel()), + $url_builder + ->withParameter($action_token, self::ACTION_ID) + ->withParameter($action_type_token, self::SHOW_MODAL_ACTION), + $row_id_token + )->withAsync(); + } + + protected function getModal( + URLBuilder $url_builder, + array $selected_records, + bool $all_records_selected + ): ?Modal { + return $this->ui_factory->modal()->interruptive( + $this->lng->txt('confirm'), + $this->lng->txt('book_confirm_cancel'), + $url_builder->buildURI()->__toString() + )->withAffectedItems( + array_map( + fn(array $selected_record): Standard => $this->ui_factory->modal()->interruptiveItem()->standard( + (string) $selected_record['booking_reservation_id'], + $this->formatBookingDescription($selected_record) + ), + $selected_records + ) + )->withActionButtonLabel($this->lng->txt($this->getActionLabel())); + } + + protected function onSubmit(URLBuilder $url_builder, array $selected_records, bool $all_records_selected): ?Modal + { + $allowed = true; + foreach ($selected_records as $selected_record) { + if (!$this->access->canManageReservationForUser($this->booking_pool->getRefId(), $selected_record['user_id'])) { + $allowed = false; + break; + } + } + + if (!$allowed) { + $this->showErrorMessage($this->lng->txt('no_permission')); + $this->ctrl->redirectByClass(ilBookingReservationsGUI::class, ilBookingReservationsGUI::DEFAULT_CMD); + return null; + } + + foreach ($selected_records as $selected_record) { + $booking_reservation = new ilBookingReservation($selected_record['booking_reservation_id']); + $booking_reservation->setStatus(ilBookingReservation::STATUS_CANCELLED); + $booking_reservation->update(); + + if ($this->booking_pool->getScheduleType() !== ilObjBookingPool::TYPE_NO_SCHEDULE) { + $calendar_entry_id = $booking_reservation->getCalendarEntry(); + if ($calendar_entry_id) { + (new ilCalendarEntry($calendar_entry_id))->delete(); + } + } + } + + $this->showSuccessMessage($this->lng->txt('book_reservation_cancelled')); + $this->ctrl->redirectByClass(ilBookingReservationsGUI::class, ilBookingReservationsGUI::DEFAULT_CMD); + return null; + } + + protected function resolveRecords(?array $selected_ids = null): array + { + return array_map( + fn(int $selected_record): array => $this->bookings[$selected_record], + $selected_ids ?? array_keys($this->bookings) + ); + } + + protected function formatBookingDescription(array $booking): string + { + $title = (new ilBookingObject($booking['object_id']))->getTitle(); + $user = strip_tags(ilUserUtil::getNamePresentation($booking['user_id'])); + $parts = [$title, $user]; + if ($this->booking_pool->getScheduleType() !== ilObjBookingPool::TYPE_NO_SCHEDULE && $booking['date_from']) { + $parts[] = ilDatePresentation::formatPeriod( + new ilDateTime($booking['date_from'], IL_CAL_UNIX), + new ilDateTime($booking['date_to'] + 1, IL_CAL_UNIX) + ); + } + return implode(' – ', $parts); + } +} diff --git a/components/ILIAS/BookingManager/src/Booking/Table/Action/BookingsTableDeleteAction.php b/components/ILIAS/BookingManager/src/Booking/Table/Action/BookingsTableDeleteAction.php new file mode 100644 index 000000000000..2c9314e95f89 --- /dev/null +++ b/components/ILIAS/BookingManager/src/Booking/Table/Action/BookingsTableDeleteAction.php @@ -0,0 +1,173 @@ +access->canManageAllReservations($this->booking_pool->getRefId()); + } + + public function getTableAction( + URLBuilder $url_builder, + URLBuilderToken $row_id_token, + URLBuilderToken $action_token, + URLBuilderToken $action_type_token + ): Action { + $action_type = $this->access->canManageAllReservations($this->booking_pool->getRefId()) ? 'standard' : 'single'; + return $this->ui_factory->table()->action()->$action_type( + $this->lng->txt($this->getActionLabel()), + $url_builder + ->withParameter($action_token, self::ACTION_ID) + ->withParameter($action_type_token, self::SHOW_MODAL_ACTION), + $row_id_token + )->withAsync(); + } + + protected function getModal( + URLBuilder $url_builder, + array $selected_records, + bool $all_records_selected + ): ?Modal { + return $this->ui_factory->modal()->interruptive( + $this->lng->txt('confirm'), + $this->lng->txt('book_confirm_delete'), + $url_builder->buildURI()->__toString() + )->withAffectedItems( + array_map( + fn(array $selected_record): Standard => $this->ui_factory->modal()->interruptiveItem()->standard( + (string) $selected_record['booking_reservation_id'], + $this->formatBookingDescription($selected_record) + ), + $selected_records + ) + )->withActionButtonLabel($this->lng->txt($this->getActionLabel())); + } + + protected function onSubmit(URLBuilder $url_builder, array $selected_records, bool $all_records_selected): ?Modal + { + if (!$this->access->canManageAllReservations($this->booking_pool->getRefId())) { + $this->showErrorMessage($this->lng->txt('no_permission')); + $this->ctrl->redirectByClass(ilBookingReservationsGUI::class, ilBookingReservationsGUI::DEFAULT_CMD); + return null; + } + + foreach ($selected_records as $selected_record) { + $booking_reservation = new ilBookingReservation($selected_record['booking_reservation_id']); + $booking_reservation->delete(); + + if ($this->booking_pool->getScheduleType() !== ilObjBookingPool::TYPE_NO_SCHEDULE) { + $cal_entry_id = $booking_reservation->getCalendarEntry(); + if ($cal_entry_id) { + (new ilCalendarEntry($cal_entry_id))->delete(); + } + } + } + + if ($selected_records !== []) { + $this->showSuccessMessage($this->lng->txt('reservation_deleted')); + } + $this->ctrl->redirectByClass(ilBookingReservationsGUI::class, ilBookingReservationsGUI::DEFAULT_CMD); + return null; + } + + protected function resolveRecords(?array $selected_ids = null): array + { + return array_map( + fn(int $selected_record): array => $this->bookings[$selected_record], + $selected_ids ?? array_keys($this->bookings) + ); + } + + protected function formatBookingDescription(array $booking): string + { + $title = (new ilBookingObject($booking['object_id']))->getTitle(); + $user = strip_tags(ilUserUtil::getNamePresentation($booking['user_id'])); + $parts = [$title, $user]; + if ($this->booking_pool->getScheduleType() !== ilObjBookingPool::TYPE_NO_SCHEDULE && $booking['date_from']) { + $parts[] = ilDatePresentation::formatPeriod( + new ilDateTime($booking['date_from'], IL_CAL_UNIX), + new ilDateTime($booking['date_to'] + 1, IL_CAL_UNIX) + ); + } + return implode(' – ', $parts); + } +} diff --git a/components/ILIAS/BookingManager/src/Booking/Table/Action/BookingsTableMailAction.php b/components/ILIAS/BookingManager/src/Booking/Table/Action/BookingsTableMailAction.php new file mode 100644 index 000000000000..8b0e787af04b --- /dev/null +++ b/components/ILIAS/BookingManager/src/Booking/Table/Action/BookingsTableMailAction.php @@ -0,0 +1,154 @@ +access->canManageAllReservations($this->booking_pool->getRefId()); + } + + public function getTableAction( + URLBuilder $url_builder, + URLBuilderToken $row_id_token, + URLBuilderToken $action_token, + URLBuilderToken $action_type_token + ): Action { + $action_type = $this->access->canManageAllReservations($this->booking_pool->getRefId()) ? 'standard' : 'single'; + return $this->ui_factory->table()->action()->$action_type( + $this->lng->txt($this->getActionLabel()), + $url_builder + ->withParameter($action_token, self::ACTION_ID) + ->withParameter($action_type_token, 'mail'), + $row_id_token + ); + } + + public function onExecute( + URLBuilder $url_builder, + URLBuilderToken $row_id_token, + URLBuilderToken $action_token, + URLBuilderToken $action_type_token + ): mixed { + $row_parameters = $this->http->resolveRowParameters($row_id_token->getName()); + $row_parameters = $row_parameters === HttpService::ALL_OBJECTS ? null : $row_parameters; + $selected_user_ids = $this->resolveRecords($row_parameters); + + if ($selected_user_ids === []) { + $this->tpl->setOnScreenMessage( + ilGlobalTemplateInterface::MESSAGE_TYPE_FAILURE, + $this->lng->txt('no_valid_selection'), + true + ); + return null; + } + + $users = []; + foreach ($selected_user_ids as $user_id) { + $users[$user_id] = ilObjUser::_lookupLogin($user_id); + } + + ilMailFormCall::setRecipients($users); + $return_url = $this->ctrl->getLinkTargetByClass(ilBookingReservationsGUI::class, ilBookingReservationsGUI::DEFAULT_CMD); + $this->ctrl->redirectToURL(ilMailFormCall::getRedirectTarget( + $return_url, + '', + [], + [ + 'type' => 'new', + 'rcp_to' => implode(',', $users), + ilMailFormCall::SIGNATURE_KEY => $this->createMailSignature() + ] + )); + return null; + } + + protected function resolveRecords(?array $selected_ids = null): array + { + $user_ids = array_map( + fn(int $reservation_id): int => $this->bookings[$reservation_id]['user_id'], + $selected_ids ?? array_keys($this->bookings) + ); + return array_values(array_unique($user_ids)); + } + + private function createMailSignature(): string + { + // #16530 - see ilObjCourseGUI::createMailSignature + $sig = chr(13) . chr(10) . chr(13) . chr(10) . chr(13) . chr(10); + $sig .= "{$this->lng->txt('book_mail_permanent_link')}: "; + $sig .= chr(13) . chr(10); + $sig .= ilLink::_getLink($this->booking_pool->getRefId()); + return rawurlencode(base64_encode($sig)); + } +} diff --git a/components/ILIAS/BookingManager/src/Booking/Table/BookingsTable.php b/components/ILIAS/BookingManager/src/Booking/Table/BookingsTable.php new file mode 100644 index 000000000000..98f605528707 --- /dev/null +++ b/components/ILIAS/BookingManager/src/Booking/Table/BookingsTable.php @@ -0,0 +1,528 @@ +booking_items = array_column( + ilBookingObject::getList($this->booking_pool->getId()), + null, + 'booking_object_id' + ); + + $filter = []; + if ( + !$this->access->canManageAllReservations($this->booking_pool->getRefId()) + && !$this->access->canReadPublicLog($this->booking_pool->getRefId()) + ) { + $filter['user_id'] = $this->user->getId(); + } + $this->bookings = array_column( + ilBookingReservation::getList(array_keys($this->booking_items), 1000, 0, $filter)['data'], + null, + 'booking_reservation_id' + ); + + $this->participants = array_column( + ilBookingParticipant::getList($this->booking_pool->getId()), + null, + 'user_id' + ); + + foreach (['dateplaner', 'tbl'] as $module) { + $this->lng->loadLanguageModule($module); + } + + $this->column_factory = $this->ui_factory->table()->column(); + $this->input_factory = $this->ui_factory->input()->field(); + } + + public function getTableId(): string + { + return static::ID; + } + + protected function getUserPresentationName(int $user_id): string + { + return str_replace('&', '&', htmlentities(ilUserUtil::getNamePresentation($user_id))); + } + + abstract public function getRows( + DataRowBuilder $row_builder, + array $visible_column_ids, + Range $range, + Order $order, + mixed $additional_viewcontrol_data, + mixed $filter_data, + mixed $additional_parameters + ): Generator; + + abstract protected function getColumns(): array; + + abstract protected function getFilterInputs(); + + protected function loadRecords(mixed $filter_data): array + { + if (!$filter_data instanceof Filter) { + return $this->bookings; + } + + $bookings = $this->bookings; + + foreach ($this->ui_service->filter()->getData($filter_data) ?? [] as $key => $filter_data_value) { + if ($filter_data_value === null || $filter_data_value === '') { + continue; + } + + $bookings = (match ($key) { + 'object' => static fn(array $bookings): array => array_filter( + $bookings, + static fn(array $booking): bool => (int) $booking['object_id'] === (int) $filter_data_value + ), + 'object_title_or_description' => static fn(array $bookings): array => array_filter( + $bookings, + static function (array $booking) use ($filter_data_value): bool { + $filter_data_value = strtolower($filter_data_value); + return + str_contains(strtolower($booking['title']), $filter_data_value) + || str_contains(strtolower($booking['message']), $filter_data_value); + } + ), + 'period' => function (array $bookings) use ($filter_data_value): array { + $bounds = $this->resolveBookingPeriodBounds($filter_data_value); + if ($bounds === null) { + return $bookings; + } + + [$period_from, $period_to] = $bounds; + return array_filter( + $bookings, + static fn(array $booking): bool => + $booking['date_from'] >= $period_from && $booking['date_to'] <= $period_to + ); + }, + 'time_slot' => static fn(array $bookings): array => array_filter( + $bookings, + static function (array $booking) use ($filter_data_value): bool { + $date_from = new DateTimeImmutable("@{$booking['date_from']}"); + $booking['date_to']++; + $date_to = new DateTimeImmutable("@{$booking['date_to']}"); + return "{$date_from->format('H:i')} - {$date_to->format('H:i')}" === $filter_data_value; + } + ), + 'past_bookings' => static function (array $bookings) use ($filter_data_value): array { + $now = new DateTimeImmutable(); + return array_filter( + $bookings, + static fn(array $booking): bool => (int) $filter_data_value === 1 + ? new DateTimeImmutable("@{$booking['date_to']}") < $now + : new DateTimeImmutable("@{$booking['date_to']}") >= $now + ); + }, + 'status' => static fn(array $bookings): array => array_filter( + $bookings, + static fn(array $booking): bool => match ((int) $filter_data_value) { + ilBookingReservation::STATUS_IN_USE => + $booking['status'] === ilBookingReservation::STATUS_IN_USE || $booking['status'] === 0, + ilBookingReservation::STATUS_CANCELLED => + $booking['status'] === ilBookingReservation::STATUS_CANCELLED, + default => true + } + ), + 'user' => static fn(array $bookings): array => array_filter( + $bookings, + static fn(array $booking): bool => (int) $booking['user_id'] === (int) $filter_data_value + ), + default => static fn(array $bookings): array => $bookings + })($bookings); + } + + return $bookings; + } + + protected function sortRecords(Order $order, array $records): array + { + $order_data = $order->get(); + if ($order_data === []) { + return $records; + } + + $users = []; + foreach ($order_data as $key => $value) { + $order_direction = $value === Order::DESC ? -1 : 1; + $callable = match ($key) { + 'title' => static fn(array $record_a, array $record_b): int => strcasecmp( + $record_a['title'], + $record_b['title'] + ) * $order_direction, + 'status' => static fn(array $record_a, array $record_b): int => + ($record_a['status'] <=> $record_b['status']) * $order_direction, + 'date' => static fn(array $record_a, array $record_b): int => + ($record_a['date_from'] <=> $record_b['date_from']) * $order_direction, + 'week' => static function (array $record_a, array $record_b) use ($order_direction): int { + $week_a = (int) (new DateTimeImmutable("@{$record_a['date_from']}"))->format('W'); + $week_b = (int) (new DateTimeImmutable("@{$record_b['date_from']}"))->format('W'); + return ($week_a <=> $week_b) * $order_direction; + }, + 'weekday' => static function (array $record_a, array $record_b) use ($order_direction): int { + $week_a = (int) (new DateTimeImmutable("@{$record_a['date_from']}"))->format('N'); + $week_b = (int) (new DateTimeImmutable("@{$record_b['date_from']}"))->format('N'); + return ($week_a <=> $week_b) * $order_direction; + }, + 'time_slot' => static function (array $record_a, array $record_b) use ($order_direction): int { + $date_from_a = new DateTimeImmutable("@{$record_a['date_from']}"); + $record_a['date_to']++; + $date_to_a = new DateTimeImmutable("@{$record_a['date_to']}"); + $time_slot_a = "{$date_from_a->format('H:i')} - {$date_to_a->format('H:i')}"; + + $date_from_b = new DateTimeImmutable("@{$record_b['date_from']}"); + $record_b['date_to']++; + $date_to_b = new DateTimeImmutable("@{$record_b['date_to']}"); + $time_slot_b = "{$date_from_b->format('H:i')} - {$date_to_b->format('H:i')}"; + + return strcasecmp($time_slot_a, $time_slot_b) * $order_direction; + }, + 'unit_count' => static fn(array $record_a, array $record_b): int => 0 * $order_direction, + 'message' => static fn(array $record_a, array $record_b): int => strcasecmp( + (string) ($record_a['message'] ?? ''), + (string) ($record_b['message'] ?? '') + ) * $order_direction, + 'user' => function (array $record_a, array $record_b) use (&$users, $order_direction): int { + $user_a = $record_a['user_id']; + $user_b = $record_b['user_id']; + $user_a = $users[$user_a] ??= $this->getUserPresentationName($user_a); + $user_b = $users[$user_b] ??= $this->getUserPresentationName($user_b); + return strcasecmp($user_a, $user_b) * $order_direction; + } , + default => null + }; + + if ($callable === null) { + continue; + } + + usort($records, $callable); + } + + return $records; + } + + protected function limitRecords(Range $range, array $records): array + { + return array_slice($records, $range->getStart(), $range->getLength()); + } + + public function getFilter(): ?Filter + { + $filter_inputs = $this->getFilterInputs(); + + $filter_inputs = $this->presetFilterInputs($filter_inputs); + + if ($filter_inputs === []) { + return null; + } + + return $this->ui_service->filter()->standard( + "{$this->getTableId()}_filter", + $this->ctrl->getLinkTargetByClass(ilBookingReservationsGUI::class), + $filter_inputs, + array_fill(0, count($filter_inputs), true), + true, + true + ); + } + + private function presetFilterInputs(array $filter_inputs): array + { + $query_params = $this->http->getRequest()->getQueryParams(); + + if ( + $this->access->canManageAllReservations($this->booking_pool->getRefId()) + || $this->access->canReadPublicLog($this->booking_pool->getRefId()) + ) { + $user_id = $query_params['user_id'] ?? null; + if (is_numeric($user_id)) { + $filter_inputs['user'] = $filter_inputs['user']->withValue((int) $user_id); + } + } elseif ($this->access->canManageOwnReservations($this->booking_pool->getRefId())) { + $filter_inputs['user'] = $filter_inputs['user']->withValue($this->user->getId()); + } + + $object_id = $query_params['object_id'] ?? null; + if (is_numeric($object_id)) { + $filter_inputs['object'] = $filter_inputs['object']->withValue((int) $object_id); + } + + if ($this->settings->getReservationPeriod() > 0) { + $filter_inputs['period'] = $filter_inputs['period']->withValue( + [ + new DateTimeImmutable('today 00:00:00'), + new DateTimeImmutable("today +{$this->settings->getReservationPeriod()} days 23:59:59") + ] + ); + } + + $period_from = $query_params['period_from'] ?? null; + $period_to = $query_params['period_to'] ?? null; + if (is_numeric($period_from) && is_numeric($period_to)) { + $filter_inputs['period'] = $filter_inputs['period']->withValue( + [ + new DateTimeImmutable("@{$period_from}"), + new DateTimeImmutable("@{$period_to}") + ] + ); + + } + + (new ilUIFilterServiceSessionGateway())->reset(static::ID . '_filter'); + + return $filter_inputs; + } + + public function getTotalRowCount( + mixed $additional_viewcontrol_data, + mixed $filter_data, + mixed $additional_parameters + ): ?int { + return count($this->bookings); + } + + public function getComponents(URLBuilder $url_builder): array + { + $filter = $this->getFilter(); + $table = $this->getTable($url_builder)->withFilter($filter); + return array_filter([$filter, $table, $this->ui_factory->divider()->horizontal(), $this->getExportDropdown()]); + } + + protected function acquireParameters(URLBuilder $url_builder): array + { + return $url_builder->acquireParameters( + [$this->getTableId()], + self::ROW_ID_PARAMETER, + self::ACTION_PARAMETER, + self::ACTION_TYPE_PARAMETER + ); + } + + private function getTable(URLBuilder $url_builder): Data + { + return + $this->ui_factory->table()->data( + $this, + $this->lng->txt('bookings'), + $this->getColumns() + ) + ->withActions($this->getTableActions()->getEnabledActions(...$this->acquireParameters($url_builder))) + ->withRequest($this->http->getRequest()) + ->withId($this->getTableId()); + } + + private function getExportDropdown(): StandardDropdown + { + $parameter = "bkrsv{$this->booking_pool->getRefId()}_xpt"; + $export_formats = [ilTable2GUI::EXPORT_EXCEL => 'tbl_export_excel' , ilTable2GUI::EXPORT_CSV => 'tbl_export_csv']; + $actions = []; + + foreach ($export_formats as $format => $caption_lng_id) { + $this->ctrl->setParameterByClass(ilBookingReservationsGUI::class, $parameter, $format); + $actions[] = $this->ui_factory->link()->standard( + $this->lng->txt($caption_lng_id), + $this->ctrl->getLinkTargetByClass( + ilBookingReservationsGUI::class, + ilBookingReservationsGUI::DEFAULT_CMD + ) + ); + $this->ctrl->setParameterByClass(ilBookingReservationsGUI::class, $parameter, null); + } + + return $this->ui_factory->dropdown()->standard($actions)->withLabel($this->lng->txt('export')); + } + + /** + * @return string[] + */ + protected function getUsers(?int $user_id = null): array + { + if (is_int($user_id)) { + return [$user_id => $this->getUserPresentationName($user_id)]; + } + + return array_map( + fn(array $participant): string => $this->getUserPresentationName($participant['user_id']), + $this->participants + ); + } + + /** + * @return array + */ + protected function getStatuses(): array + { + return [ + ilBookingReservation::STATUS_IN_USE => $this->lng->txt('book_not_cancelled'), + ilBookingReservation::STATUS_CANCELLED => $this->lng->txt('book_reservation_status_5') + ]; + } + + protected function getTableActions(): TableActions + { + return (new BookingTableActionsFactory( + $this->ctrl, + $this->lng, + $this->tpl, + $this->ui_factory, + $this->ui_renderer, + $this->refinery, + $this->http, + $this->access, + $this->reservation_repository, + $this->booking_pool, + $this->bookings + ))->getTableActions(); + } + + protected function getUserComponent(int $user_id) + { + $user_name = $this->getUserPresentationName($user_id); + if (!ilUserUtil::hasPublicProfile($user_id)) { + return $this->ui_factory->link()->standard($user_name, '')->withDisabled(true); + } + + $this->ctrl->setParameterByClass(PublicProfileGUI::class, 'user_id', $user_id); + + return $this->ui_factory->link()->standard( + $user_name, + $this->ctrl->getLinkTargetByClass( + [ilPublicProfileBaseClassGUI::class, PublicProfileGUI::class], + 'getHTML' + ) + ); + } + + private function resolveBookingPeriodBounds(mixed $period): ?array + { + if (!$period) { + return null; + } + + $from = $this->parseBookingPeriodValue($period[0] ?? null); + $to = $this->parseBookingPeriodValue($period[1] ?? null); + + if ($from === null && $to === null) { + return null; + } + + return [ + $from ?? PHP_INT_MIN, + $to ?? PHP_INT_MAX, + ]; + } + + private function parseBookingPeriodValue(mixed $value): ?int + { + if (!$value) { + return null; + } + + if ($value instanceof DateTimeImmutable) { + return $value->getTimestamp(); + } + + try { + return (new DateTimeImmutable((string) $value))->getTimestamp(); + } catch (Throwable) { + return null; + } + } +} diff --git a/components/ILIAS/BookingManager/src/Booking/Table/BookingsWithScheduleTable.php b/components/ILIAS/BookingManager/src/Booking/Table/BookingsWithScheduleTable.php new file mode 100644 index 000000000000..6acef4234ea8 --- /dev/null +++ b/components/ILIAS/BookingManager/src/Booking/Table/BookingsWithScheduleTable.php @@ -0,0 +1,166 @@ +limitRecords($range, $this->sortRecords($order, $this->loadRecords($filter_data))); + + foreach ($records as $record) { + $user_id = $record['user_id']; + + $record['date_to']++; + + $date_from = new DateTimeImmutable("@{$record['date_from']}"); + $time_slot = ilDatePresentation::formatPeriod( + new ilDateTime($record['date_from'], IL_CAL_UNIX), + new ilDateTime($record['date_to'], IL_CAL_UNIX), + true + ); + + yield $this->getTableActions()->onDataRow( + $row_builder->buildDataRow( + (string) $record['booking_reservation_id'], + [ + 'title' => (string) $record['title'], + 'status' => $record['status'] === ilBookingReservation::STATUS_CANCELLED + ? $this->lng->txt('book_reservation_status_5') + : '', + 'date' => $date_from, + 'week' => $date_from, + 'weekday' => $date_from, + 'time_slot' => $time_slot, + 'unit_count' => 1, + 'message' => (string) ($record['message'] ?? ''), + 'user' => $this->getUserComponent($user_id) + ] + ), + $record + ); + } + } + + protected function getColumns(): array + { + $columns = [ + 'title' => $this->column_factory->text($this->lng->txt('title')), + 'status' => $this->column_factory->text($this->lng->txt('status')), + 'date' => $this->column_factory->date( + $this->lng->txt('date'), + new DateFormat([ + DateFormat::DAY, + DateFormat::DOT, + DateFormat::SPACE, + DateFormat::MONTH_SPELLED_SHORT, + DateFormat::SPACE, + DateFormat::YEAR + ]) + ), + 'week' => $this->column_factory->date($this->lng->txt('week'), new DateFormat([DateFormat::WEEK])) + ->withIsOptional(true, true), + 'weekday' => $this->column_factory->date($this->lng->txt('cal_weekday'), new DateFormat([DateFormat::WEEKDAY_SHORT])) + ->withIsOptional(true, true), + 'time_slot' => $this->column_factory->text($this->lng->txt('book_schedule_slot')), + 'unit_count' => $this->column_factory->number($this->lng->txt('book_no_of_objects')), + 'message' => $this->column_factory->text($this->lng->txt('book_message'))->withIsSortable(false), + 'user' => $this->column_factory->link($this->lng->txt('user')) + ]; + + if (!$this->booking_pool->usesMessages()) { + unset($columns['message']); + } + + return $columns; + } + + protected function getFilterInputs(): array + { + $txt_user = $this->lng->txt('user'); + if ( + $this->access->canManageAllReservations($this->booking_pool->getRefId()) + || $this->access->canReadPublicLog($this->booking_pool->getRefId()) + ) { + $user_input = $this->input_factory->select($txt_user, $this->getUsers()); + } else { + $user_input = $this->input_factory + ->select($txt_user, $this->getUsers($this->user->getId())) + ->withValue($this->user->getId()) + ->withDisabled(true); + } + + return [ + 'object' => $this->input_factory->select( + $this->lng->txt('object'), + array_column($this->booking_items, 'title', 'booking_object_id') + ), + 'object_title_or_description' => $this->input_factory->text($this->lng->txt('book_object_title_or_description')), + 'period' => $this->input_factory->duration($this->lng->txt('book_filter_period')) + ->withUseTime(true) + ->withFormat($this->user->getDateTimeFormat()), + 'time_slot' => $this->input_factory->select($this->lng->txt('book_schedule_slot'), $this->getTimeSlots()), + 'past_bookings' => $this->input_factory->select( + $this->lng->txt('book_show_past_bookings'), + [1 => $this->lng->txt('book_past_bookings'), 2 => $this->lng->txt('book_present_bookings')] + ), + 'status' => $this->input_factory->select($this->lng->txt('status'), $this->getStatuses()), + 'user' => $user_input + ]; + } + + /** + * @return string[] + */ + protected function getTimeSlots(): array + { + $time_slots = []; + + foreach ($this->bookings as $booking) { + $date_from = new DateTimeImmutable("@{$booking['date_from']}"); + $booking['date_to']++; + $date_to = new DateTimeImmutable("@{$booking['date_to']}"); + $time_slot = "{$date_from->format('H:i')} - {$date_to->format('H:i')}"; + $time_slots[$time_slot] ??= $time_slot; + } + + return $time_slots; + } +} diff --git a/components/ILIAS/BookingManager/src/Booking/Table/BookingsWithoutScheduleTable.php b/components/ILIAS/BookingManager/src/Booking/Table/BookingsWithoutScheduleTable.php new file mode 100644 index 000000000000..e3c8483b99f4 --- /dev/null +++ b/components/ILIAS/BookingManager/src/Booking/Table/BookingsWithoutScheduleTable.php @@ -0,0 +1,97 @@ +limitRecords($range, $this->sortRecords($order, $this->loadRecords($filter_data))); + + foreach ($bookings as $record) { + $user_id = $record['user_id']; + + yield $this->getTableActions()->onDataRow( + $row_builder->buildDataRow( + (string) $record['booking_reservation_id'], + [ + 'title' => (string) $record['title'], + 'status' => $record['status'] === ilBookingReservation::STATUS_CANCELLED + ? $this->lng->txt('book_reservation_status_5') + : '', + 'user' => $this->getUserComponent($user_id) + ] + ), + $record + ); + } + } + + protected function getColumns(): array + { + return [ + 'title' => $this->column_factory->text($this->lng->txt('title')), + 'status' => $this->column_factory->text($this->lng->txt('status')), + 'user' => $this->column_factory->link($this->lng->txt('user')), + ]; + } + + protected function getFilterInputs(): array + { + $txt_user = $this->lng->txt('user'); + if ( + $this->access->canManageAllReservations($this->booking_pool->getRefId()) + || $this->access->canReadPublicLog($this->booking_pool->getRefId()) + ) { + $user_input = $this->input_factory->select($txt_user, $this->getUsers()); + } else { + $user_input = $this->input_factory + ->select($txt_user, $this->getUsers($this->user->getId())) + ->withValue($this->user->getId()) + ->withDisabled(true); + } + + return [ + 'object' => $this->input_factory->select( + $this->lng->txt('object'), + array_column($this->booking_items, 'title', 'booking_object_id') + ), + 'object_title_or_description' => $this->input_factory->text($this->lng->txt('book_object_title_or_description')), + 'status' => $this->input_factory->select($this->lng->txt('status'), $this->getStatuses()), + 'user' => $user_input + ]; + } +} From 4186bef45dde7bb3ca313cf94ed20b6110927fba Mon Sep 17 00:00:00 2001 From: Matheus Zych Date: Fri, 29 May 2026 11:34:14 +0200 Subject: [PATCH 8/8] BookingPool: Adds Booking Pool Bookable Item Table See: https://docu.ilias.de/go/wiki/wpage_7320_1357 Adds new KS Booking Pool Bookable Item table. --- .../BookableItemTableActionsFactory.php | 182 +++++++ .../Action/BookableItemTableBookAction.php | 462 ++++++++++++++++++ ...kableItemTableBookForParticipantAction.php | 162 ++++++ .../BookableItemTableBookingsAction.php | 131 +++++ .../BookableItemTableCancelBookingAction.php | 338 +++++++++++++ .../Action/BookableItemTableDeleteAction.php | 169 +++++++ .../Action/BookableItemTableEditAction.php | 101 ++++ .../BookableItem/Table/BookableItemTable.php | 355 ++++++++++++++ .../Table/BookableItemWithScheduleTable.php | 352 +++++++++++++ .../BookableItemWithoutScheduleTable.php | 135 +++++ 10 files changed, 2387 insertions(+) create mode 100644 components/ILIAS/BookingManager/src/BookableItem/Table/Action/BookableItemTableActionsFactory.php create mode 100644 components/ILIAS/BookingManager/src/BookableItem/Table/Action/BookableItemTableBookAction.php create mode 100644 components/ILIAS/BookingManager/src/BookableItem/Table/Action/BookableItemTableBookForParticipantAction.php create mode 100644 components/ILIAS/BookingManager/src/BookableItem/Table/Action/BookableItemTableBookingsAction.php create mode 100644 components/ILIAS/BookingManager/src/BookableItem/Table/Action/BookableItemTableCancelBookingAction.php create mode 100644 components/ILIAS/BookingManager/src/BookableItem/Table/Action/BookableItemTableDeleteAction.php create mode 100644 components/ILIAS/BookingManager/src/BookableItem/Table/Action/BookableItemTableEditAction.php create mode 100644 components/ILIAS/BookingManager/src/BookableItem/Table/BookableItemTable.php create mode 100644 components/ILIAS/BookingManager/src/BookableItem/Table/BookableItemWithScheduleTable.php create mode 100644 components/ILIAS/BookingManager/src/BookableItem/Table/BookableItemWithoutScheduleTable.php diff --git a/components/ILIAS/BookingManager/src/BookableItem/Table/Action/BookableItemTableActionsFactory.php b/components/ILIAS/BookingManager/src/BookableItem/Table/Action/BookableItemTableActionsFactory.php new file mode 100644 index 000000000000..8825dea200fb --- /dev/null +++ b/components/ILIAS/BookingManager/src/BookableItem/Table/Action/BookableItemTableActionsFactory.php @@ -0,0 +1,182 @@ +ctrl, + $this->lng, + $this->tpl, + $this->ui_factory, + $this->ui_renderer, + $this->refinery, + $this->http, + [ + self::ACTION_BOOK => $this->getBookAction(), + self::ACTION_BOOK_FOR_PARTICIPANT => $this->getBookForParticipantAction(), + self::ACTION_EDIT => $this->getEditAction(), + self::ACTION_BOOKINGS => $this->getBookingsAction(), + self::ACTION_DELETE => $this->getDeleteAction(), + self::ACTION_CANCEL_BOOKING => $this->getCancelBookingAction(), + ] + ); + } + + protected function getBookAction(): TableAction + { + return new BookableItemTableBookAction( + $this->ui_factory, + $this->ui_renderer, + $this->tpl, + $this->lng, + $this->http, + $this->refinery, + $this->ctrl, + $this->access, + $this->pool, + $this->process_manager, + $this->user, + $this->ref_id, + $this->booking_context_obj_id, + $this->bookable_items, + ); + } + + protected function getBookForParticipantAction(): TableAction + { + return new BookableItemTableBookForParticipantAction( + $this->ui_factory, + $this->lng, + $this->ctrl, + $this->http, + $this->refinery, + $this->access, + $this->pool, + $this->ref_id, + $this->active_management, + ); + } + + protected function getEditAction(): TableAction + { + return new BookableItemTableEditAction( + $this->ui_factory, + $this->lng, + $this->ctrl, + $this->http, + $this->access, + $this->ref_id, + $this->active_management, + ); + } + + protected function getBookingsAction(): TableAction + { + return new BookableItemTableBookingsAction( + $this->ui_factory, + $this->lng, + $this->ctrl, + $this->http, + $this->access, + $this->user, + $this->pool, + $this->ref_id, + $this->active_management, + ); + } + + protected function getDeleteAction(): TableAction + { + return new BookableItemTableDeleteAction( + $this->ui_factory, + $this->ui_renderer, + $this->lng, + $this->tpl, + $this->http, + $this->access, + $this->pool, + $this->ref_id, + $this->active_management, + ); + } + + protected function getCancelBookingAction(): TableAction + { + return new BookableItemTableCancelBookingAction( + $this->ui_factory, + $this->ui_renderer, + $this->lng, + $this->tpl, + $this->http, + $this->refinery, + $this->access, + $this->pool, + $this->user, + $this->ref_id, + $this->active_management, + $this->bookable_items, + ); + } +} diff --git a/components/ILIAS/BookingManager/src/BookableItem/Table/Action/BookableItemTableBookAction.php b/components/ILIAS/BookingManager/src/BookableItem/Table/Action/BookableItemTableBookAction.php new file mode 100644 index 000000000000..c0ae73b87720 --- /dev/null +++ b/components/ILIAS/BookingManager/src/BookableItem/Table/Action/BookableItemTableBookAction.php @@ -0,0 +1,462 @@ +pool->getScheduleType(); + + if ($schedule_type === ilObjBookingPool::TYPE_NO_SCHEDULE) { + return !$this->isUserBookingPoolLimitReached(); + } + + if ($schedule_type === ilObjBookingPool::TYPE_NO_SCHEDULE_PREFERENCES) { + return false; + } + + return $this->access->canManageOwnReservations($this->ref_id) || $this->access->canManageObjects($this->ref_id); + } + + public function allowActionForRecord(mixed $record): bool + { + $schedule_type = $this->pool->getScheduleType(); + if ($schedule_type === ilObjBookingPool::TYPE_NO_SCHEDULE) { + return !$this->isUserBookingPoolLimitReached() && !($record['has_user_active_booking'] ?? false); + } + + return $record['is_available'] ?? false; + } + + public function getTableAction( + URLBuilder $url_builder, + URLBuilderToken $row_id_token, + URLBuilderToken $action_token, + URLBuilderToken $action_type_token + ): Action { + return $this->ui_factory->table()->action()->standard( + $this->lng->txt($this->getActionLabel()), + $url_builder + ->withParameter($action_token, $this->getActionId()) + ->withParameter($action_type_token, self::SHOW_MODAL_ACTION), + $row_id_token + )->withAsync(); + } + + protected function showModal( + URLBuilder $url_builder, + URLBuilderToken $row_id_token, + URLBuilderToken $action_token, + URLBuilderToken $action_type_token + ): void { + $selected_records = $this->http->resolveRowParameters($row_id_token->getName()); + $all_records_selected = $selected_records === HttpService::ALL_OBJECTS; + if ($all_records_selected) { + $selected_records = array_keys($this->bookable_items); + } + + $selected_records = $this->resolveBookingEntriesPayload($this->lng, $selected_records); + $selected_records['entries'] = array_values(array_filter( + $selected_records['entries'], + fn(array $entry): bool => $this->allowActionForRecord($entry) + )); + + if ($this->pool->getScheduleType() === ilObjBookingPool::TYPE_NO_SCHEDULE) { + $remaining = $this->getRemainingBookingCapacity(); + if ($remaining !== null && count($selected_records['entries']) > $remaining) { + $this->http->sendAsync( + $this->ui_renderer->renderAsync( + $this->buildBookModalInformative( + $this->lng->txt('book_overall_limit_would_be_exceeded') + ) + ) + ); + return; + } + } + + $this->http->sendAsync( + $this->ui_renderer->renderAsync( + $this->getModal( + $url_builder + ->withParameter( + $row_id_token, + $all_records_selected + ? HttpService::ALL_OBJECTS + : array_column($selected_records['entries'], 'row_id') + ) + ->withParameter($action_token, $this->getActionId()) + ->withParameter($action_type_token, self::SUBMIT_MODAL_ACTION), + $selected_records, + $all_records_selected + ) + ) + ); + } + + public function getModal( + URLBuilder $url_builder, + array $selected_records, + bool $all_records_selected + ): ?Modal { + $entries = $selected_records['entries']; + if ($entries === []) { + return $this->buildBookModalInformative($this->lng->txt('no_valid_selection')); + } + + $grouped = []; + foreach ($entries as $entry) { + $grouped[$entry['booking_object_id']][] = $entry; + } + ksort($grouped); + + $skipped_descriptions = $selected_records['skipped_descriptions']; + $content = null; + if ($skipped_descriptions !== []) { + $content = [ + $this->ui_factory->messageBox()->confirmation( + $this->lng->txt('book_modal_warning_skipped_selections') + . $this->ui_renderer->render($this->ui_factory->listing()->unordered($skipped_descriptions)) + ) + ]; + } + + $form_components = []; + $field_factory = $this->ui_factory->input()->field(); + foreach ($grouped as $object_id => $entries) { + $section_input_components = []; + + foreach ($entries as $entry) { + if (!$entry['has_schedule']) { + continue; + } + + $max_quantity = $entry['max_quantity']; + + $section_input_components[$entry['row_id']] = $field_factory->numeric( + $this->formatBookModalSlotLabel($entry['slot_from'], $entry['slot_to']), + sprintf($this->lng->txt('book_objects_available'), $max_quantity) + ) + ->withValue(1) + ->withAdditionalTransformation( + $this->refinery->logical()->parallel( + [ + $this->refinery->int()->isGreaterThanOrEqual(1), + $this->refinery->int()->isLessThanOrEqual($max_quantity) + ] + ) + ) + ->withAdditionalOnLoadCode( + static fn(string $id): string + => " + var element = document.getElementById('{$id}').querySelector('div input'); + element.min = 1; + element.max = {$max_quantity}; + " + ); + } + + if ($this->pool->usesMessages()) { + $section_input_components['message'] = $field_factory->textarea( + $this->lng->txt('book_message'), + $this->lng->txt('book_message_info') + ); + } + + $form_components[$object_id] = $field_factory->section( + $section_input_components, + $entries[0]['title'], + $this->lng->txt('book_modal_enter_quantity_intro') + ); + } + + return $this->ui_factory->modal()->roundtrip( + $this->lng->txt('book_modal_booking_confirmation'), + $content, + $form_components, + $url_builder->buildURI()->__toString() + ); + } + + public function onSubmit( + URLBuilder $url_builder, + array $selected_records, + bool $all_records_selected + ): ?Modal { + if ($this->pool->getScheduleType() === ilObjBookingPool::TYPE_NO_SCHEDULE) { + $remaining = $this->getRemainingBookingCapacity(); + if ($remaining !== null && count($selected_records) > $remaining) { + return $this->buildBookModalInformative( + $this->lng->txt('book_overall_limit_warning') + ); + } + } + + /** @var RoundTrip $modal */ + $modal = $this + ->getModal( + $url_builder, + [ + 'entries' => $selected_records, + 'skipped_descriptions' => [] + ], + $all_records_selected + ); + $modal = $modal->withRequest($this->http->getRequest()); + + /** @var ?array $data */ + $data = $modal->getData(); + if ($data === null) { + return $modal->withOnLoad($modal->getShowSignal()); + } + + $booked_total = 0; + $unavailable = []; + + foreach ($data as $object_id => $section) { + $message = $section['message'] ?? ''; + unset($section['message']); + + $bookable_item = new ilBookingObject($object_id); + if ($section === [] && !$bookable_item->getScheduleId()) { + $section = [$object_id => 1]; + } + + foreach ($section as $row_id => $amount) { + $from_to = $bookable_item->getScheduleId() ? explode('_', $row_id) : []; + + $booked = $this->process_manager->bookAvailableObjects( + $bookable_item->getId(), + $this->user->getId(), + $this->user->getId(), + $this->booking_context_obj_id, + (int) ($from_to[1] ?? 0), + (int) ($from_to[2] ?? 0), + 0, // TODO + $amount, + null, // TODO + $message + ); + + if ($booked !== []) { + $booked_total += count($booked); + continue; + } + + $unavailable[] = $row_id; + } + } + + if ($unavailable !== []) { + $this->tpl->setOnScreenMessage( + ilGlobalTemplateInterface::MESSAGE_TYPE_FAILURE, + $this->lng->txt('book_some_reservations_unavailable'), + true + ); + } + + if ($booked_total === 0 && $unavailable === []) { + $this->tpl->setOnScreenMessage( + ilGlobalTemplateInterface::MESSAGE_TYPE_FAILURE, + $this->lng->txt('book_reservation_failed'), + true + ); + } + + if ($booked_total > 0) { + $this->tpl->setOnScreenMessage( + ilGlobalTemplateInterface::MESSAGE_TYPE_SUCCESS, + $this->lng->txt('book_reservation_confirmed'), + true + ); + } + + $this->ctrl->redirectByClass(ilBookingObjectGUI::class, 'render'); + return null; + } + + /** + * @param ?string[] $selected_ids + * @return array[] + */ + protected function resolveRecords(?array $selected_ids = null): array + { + return $this->resolveBookingEntriesPayload( + $this->lng, + $selected_ids ?? array_keys($this->bookable_items) + )['entries']; + } + + private function buildBookModalInformative(string $message): Modal + { + return $this->ui_factory->modal()->roundtrip( + $this->lng->txt('book_modal_booking_confirmation'), + [$this->ui_factory->messageBox()->failure($message)] + )->withAdditionalOnLoadCode(static fn(string $id): string => "il.repository.ui.initModal('$id');"); + } + + private function resolveBookingEntriesPayload(ilLanguage $lng, string|array $row_ids): array + { + if (!is_array($row_ids)) { + $row_ids = $row_ids === HttpService::ALL_OBJECTS ? array_keys($this->bookable_items) : []; + } + + $entries = []; + $skipped_descriptions = []; + + foreach ($row_ids as $row_id) { + $row_key = (string) $row_id; + $record = $this->bookable_items[$row_key] ?? null; + if ($record === null) { + $skipped_descriptions[] = sprintf($lng->txt('book_modal_skipped_unknown_item'), $row_key); + continue; + } + + $availability = (int) ($record['available'] ?? 0); + $has_schedule = ($record['schedule_id'] ?? 0) > 0; + $slot_from = (int) ($record['slot_from'] ?? 0); + $slot_to = (int) ($record['slot_to'] ?? 0); + + if ($availability <= 0) { + $skipped_descriptions[] = $has_schedule + ? sprintf( + '%s — %s', + $record['title'], + $this->formatBookModalSlotLabel($slot_from, $slot_to) + ) + : $record['title']; + continue; + } + + if (!$has_schedule && ($record['has_user_active_booking'] ?? false)) { + $skipped_descriptions[] = $record['title']; + continue; + } + + $entries[] = [ + 'row_id' => $row_key, + 'booking_object_id' => (int) $record['booking_object_id'], + 'title' => (string) $record['title'], + 'has_schedule' => $has_schedule, + 'has_user_booking' => (bool) ($record['has_user_booking'] ?? false), + 'slot_from' => $slot_from, + 'slot_to' => $slot_to, + 'max_quantity' => $availability, + 'is_available' => true, + ]; + } + + return [ + 'entries' => $entries, + 'skipped_descriptions' => $skipped_descriptions, + ]; + } + + private function formatBookModalSlotLabel(int $slot_from, int $slot_to): string + { + $this->lng->loadLanguageModule('dateplaner'); + + return ilDatePresentation::formatPeriod( + new ilDateTime($slot_from, IL_CAL_UNIX), + new ilDateTime($slot_to, IL_CAL_UNIX) + ); + } + + private function getRemainingBookingCapacity(): ?int + { + $limit = $this->pool->getOverallLimit(); + if ($limit === null || $limit === 0) { + return null; + } + + $current = ilBookingReservation::isBookingPoolLimitReachedByUser( + $this->user->getId(), + $this->pool->getId() + ); + + return max(0, $limit - $current); + } + + private function isUserBookingPoolLimitReached(): bool + { + return $this->getRemainingBookingCapacity() === 0; + } +} diff --git a/components/ILIAS/BookingManager/src/BookableItem/Table/Action/BookableItemTableBookForParticipantAction.php b/components/ILIAS/BookingManager/src/BookableItem/Table/Action/BookableItemTableBookForParticipantAction.php new file mode 100644 index 000000000000..a528ae3420e8 --- /dev/null +++ b/components/ILIAS/BookingManager/src/BookableItem/Table/Action/BookableItemTableBookForParticipantAction.php @@ -0,0 +1,162 @@ +pool->getScheduleType() === ilObjBookingPool::TYPE_NO_SCHEDULE_PREFERENCES) { + return false; + } + + return $this->active_management && $this->access->canManageObjects($this->ref_id); + } + + public function getTableAction( + URLBuilder $url_builder, + URLBuilderToken $row_id_token, + URLBuilderToken $action_token, + URLBuilderToken $action_type_token + ): Action { + return $this->ui_factory->table()->action()->single( + $this->lng->txt(self::ACTION_LABEL), + $url_builder + ->withParameter($action_token, self::ACTION_ID) + ->withParameter($action_type_token, 'redirect'), + $row_id_token + ); + } + + public function onExecute( + URLBuilder $url_builder, + URLBuilderToken $row_id_token, + URLBuilderToken $action_token, + URLBuilderToken $action_type_token + ): mixed { + $row_ids = $this->resolveRowIds($row_id_token->getName()); + if ($row_ids === []) { + return null; + } + + $first_parts = explode('_', $row_ids[0]); + $primary_object_id = (int) $first_parts[0]; + if ($primary_object_id <= 0) { + return null; + } + + $target_class = $this->pool->getScheduleType() === ilObjBookingPool::TYPE_FIX_SCHEDULE + ? ilBookingProcessWithScheduleGUI::class + : ilBookingProcessWithoutScheduleGUI::class; + + $this->ctrl->setParameterByClass($target_class, 'object_id', (string) $primary_object_id); + + if (isset($first_parts[1], $first_parts[2])) { + $this->ctrl->setParameterByClass( + $target_class, + 'slot', + $first_parts[1] . '_' . $first_parts[2] + ); + $this->ctrl->setParameterByClass($target_class, 'seed', date('Y-m-d', (int) $first_parts[1])); + } + + $this->ctrl->redirectByClass($target_class, 'assignParticipants'); + return null; + } + + public function allowActionForRecord(mixed $record): bool + { + if (!is_array($record)) { + return false; + } + + if ($this->pool->getOverallLimit() && (int) $record['available'] <= 0) { + return false; + } + + return (bool) $record['is_available']; + } + + /** + * @return string[] + */ + private function resolveRowIds(string $key): array + { + $value = $this->http->get( + $key, + $this->refinery->custom()->transformation( + static function (mixed $raw): array { + if ($raw === null || $raw === '') { + return []; + } + + if (is_array($raw)) { + return array_values(array_map('strval', $raw)); + } + + return [(string) $raw]; + } + ) + ) ?? []; + + return array_values(array_filter($value, static fn(string $v): bool => $v !== '')); + } +} diff --git a/components/ILIAS/BookingManager/src/BookableItem/Table/Action/BookableItemTableBookingsAction.php b/components/ILIAS/BookingManager/src/BookableItem/Table/Action/BookableItemTableBookingsAction.php new file mode 100644 index 000000000000..b1d93c91c374 --- /dev/null +++ b/components/ILIAS/BookingManager/src/BookableItem/Table/Action/BookableItemTableBookingsAction.php @@ -0,0 +1,131 @@ +active_management + && ($this->access->canManageOwnReservations($this->ref_id) || $this->access->canManageObjects($this->ref_id)); + } + + public function getTableAction( + URLBuilder $url_builder, + URLBuilderToken $row_id_token, + URLBuilderToken $action_token, + URLBuilderToken $action_type_token + ): Action { + return $this->ui_factory->table()->action()->single( + $this->lng->txt(self::ACTION_LABEL), + $url_builder + ->withParameter($action_token, self::ACTION_ID) + ->withParameter($action_type_token, 'redirect'), + $row_id_token + ); + } + + public function onExecute( + URLBuilder $url_builder, + URLBuilderToken $row_id_token, + URLBuilderToken $action_token, + URLBuilderToken $action_type_token + ): mixed { + $row_id = (string) $this->http->resolveRowParameter($row_id_token->getName()); + $object_id = (int) explode('_', $row_id)[0]; + + if ($object_id <= 0) { + return null; + } + + $this->ctrl->setParameterByClass(ilBookingReservationsGUI::class, 'object_id', (int) $object_id); + + if (!$this->access->canManageAllReservations($this->ref_id) && !$this->access->canReadPublicLog($this->ref_id)) { + $this->ctrl->setParameterByClass(ilBookingReservationsGUI::class, 'user_id', (int) $this->user->getId()); + } + + if ($this->pool->getScheduleType() === ilObjBookingPool::TYPE_FIX_SCHEDULE) { + $this->ctrl->setParameterByClass(ilBookingReservationsGUI::class, 'period_from', explode('_', $row_id)[1] ?? null); + $this->ctrl->setParameterByClass(ilBookingReservationsGUI::class, 'period_to', explode('_', $row_id)[2] ?? null); + } + + $this->ctrl->redirectByClass(ilBookingReservationsGUI::class, ''); + return null; + } + + public function allowActionForRecord(mixed $record): bool + { + $has_reservations = $record['has_reservations'] ?? false; + + if ( + $this->access->canManageAllReservations($this->ref_id) + || $this->access->canReadPublicLog($this->ref_id) + ) { + return $has_reservations; + } + + if ($this->access->canManageOwnReservations($this->ref_id)) { + return $has_reservations && ($record['has_user_booking'] ?? false); + } + + return false; + } +} diff --git a/components/ILIAS/BookingManager/src/BookableItem/Table/Action/BookableItemTableCancelBookingAction.php b/components/ILIAS/BookingManager/src/BookableItem/Table/Action/BookableItemTableCancelBookingAction.php new file mode 100644 index 000000000000..e790da5d23a1 --- /dev/null +++ b/components/ILIAS/BookingManager/src/BookableItem/Table/Action/BookableItemTableCancelBookingAction.php @@ -0,0 +1,338 @@ +access->canManageAllReservations($this->pool->getRefId()) + && !$this->access->canReadPublicLog($this->pool->getRefId()) + ) { + $filter['user_id'] = $this->user->getId(); + } + $this->bookings = array_column( + ilBookingReservation::getList(array_keys($this->bookable_items), 1000, 0, $filter)['data'], + null, + 'booking_reservation_id' + ); + } + + public function getActionId(): string + { + return self::ACTION_ID; + } + + public function getActionLabel(): string + { + return self::ACTION_LABEL; + } + + public function isAvailable(): bool + { + if ($this->pool->getScheduleType() === ilObjBookingPool::TYPE_NO_SCHEDULE_PREFERENCES) { + return false; + } + + return + $this->active_management + && ($this->access->canManageOwnReservations($this->ref_id) || $this->access->canManageObjects($this->ref_id)); + } + + public function getTableAction( + URLBuilder $url_builder, + URLBuilderToken $row_id_token, + URLBuilderToken $action_token, + URLBuilderToken $action_type_token + ): Action { + return $this->ui_factory->table()->action()->standard( + $this->lng->txt(self::ACTION_LABEL), + $url_builder + ->withParameter($action_token, self::ACTION_ID) + ->withParameter($action_type_token, self::SHOW_MODAL_ACTION), + $row_id_token + )->withAsync(); + } + + public function allowActionForRecord(mixed $record): bool + { + return $record['has_user_active_booking'] ?? false; + } + + public function onExecute( + URLBuilder $url_builder, + URLBuilderToken $row_id_token, + URLBuilderToken $action_token, + URLBuilderToken $action_type_token + ): ?Modal { + return match ($this->http->resolveRowParameter($action_type_token->getName())) { + self::SUBMIT_MODAL_ACTION => $this->submit($url_builder, $row_id_token, $action_token, $action_type_token), + default => $this->showModal( + $url_builder, + $row_id_token, + $action_token, + $action_type_token + ), + }; + } + + protected function getModal( + URLBuilder $url_builder, + array $selected_records, + bool $all_records_selected + ): ?Modal { + return $this->ui_factory->modal()->interruptive( + $this->lng->txt('book_confirm_cancel'), + $this->lng->txt('book_confirm_cancel_info'), + $url_builder->buildURI()->__toString() + )->withAffectedItems(array_map( + fn(array $record): InterruptiveItem => $this->ui_factory->modal()->interruptiveItem()->standard( + (string) $record['reservation_id'], + $this->buildItemDescription($record) + ), + $this->resolveRecords($selected_records) + ))->withActionButtonLabel($this->lng->txt('book_set_cancel')); + } + + protected function showModal( + URLBuilder $url_builder, + URLBuilderToken $row_id_token, + URLBuilderToken $action_token, + URLBuilderToken $action_type_token + ): void { + $selected_records = $this->http->resolveRowParameters($row_id_token->getName()); + $all_records_selected = $selected_records === HttpService::ALL_OBJECTS; + if ($all_records_selected) { + $selected_records = array_map( + fn(array $record): string => "{$record['object_id']}_{$record['date_from']}_{$record['date_to']}", + $this->bookings + ); + } + + $this->http->sendAsync( + $this->ui_renderer->renderAsync( + $this->getModal( + $url_builder + ->withParameter( + $row_id_token, + $all_records_selected + ? HttpService::ALL_OBJECTS + : $selected_records + ) + ->withParameter($action_token, self::ACTION_ID) + ->withParameter($action_type_token, self::SUBMIT_MODAL_ACTION), + $selected_records, + $all_records_selected + ) + ) + ); + } + + protected function onSubmit( + URLBuilder $url_builder, + array $selected_records, + bool $all_records_selected + ): ?Modal { + if (!$this->access->canManageOwnReservations($this->ref_id)) { + $this->tpl->setOnScreenMessage( + ilGlobalTemplateInterface::MESSAGE_TYPE_FAILURE, + $this->lng->txt('no_permission'), + true + ); + return null; + } + + $cancelled = 0; + foreach ($selected_records as $record) { + $reservation = new ilBookingReservation((int) $record['reservation_id']); + $reservation->setStatus(ilBookingReservation::STATUS_CANCELLED); + $reservation->update(); + $cancelled++; + } + + if ($cancelled === 0) { + $this->tpl->setOnScreenMessage( + ilGlobalTemplateInterface::MESSAGE_TYPE_FAILURE, + $this->lng->txt('no_valid_selection'), + true + ); + return null; + } + + $this->tpl->setOnScreenMessage( + ilGlobalTemplateInterface::MESSAGE_TYPE_SUCCESS, + $this->lng->txt('book_reservation_cancelled'), + true + ); + return null; + } + + /** + * @return string[] + */ + private function resolveRowIds(string $key): array + { + $value = $this->http->get( + $key, + $this->refinery->custom()->transformation( + static function (mixed $raw): array { + if ($raw === null || $raw === '') { + return []; + } + + if (is_array($raw)) { + return array_values(array_map('strval', $raw)); + } + + return [(string) $raw]; + } + ) + ) ?? []; + + return array_values(array_filter($value, static fn(string $v): bool => $v !== '')); + } + + /** + * @param string[] $selected_ids + * @return array[] + */ + protected function resolveRecords(?array $selected_ids = null): array + { + $titles = []; + foreach (ilBookingObject::getList($this->pool->getId()) as $item) { + $titles[(int) $item['booking_object_id']] = (string) $item['title']; + } + + if ($selected_ids === null) { + $selected_ids = array_map( + fn(array $record): string => "{$record['object_id']}_{$record['date_from']}_{$record['date_to']}", + $this->bookings + ); + } + + $result = []; + $user_id = $this->user->getId(); + foreach ($selected_ids as $row_id) { + $row_id = (string) $row_id; + $parts = explode('_', $row_id); + $object_id = (int) $parts[0]; + if ($object_id <= 0 || !isset($titles[$object_id])) { + continue; + } + + $reservations = ilBookingReservation::getList([$object_id], 1000, 0, []); + foreach ($reservations['data'] ?? [] as $reservation) { + if ((int) $reservation['user_id'] !== $user_id) { + continue; + } + + if ((int) $reservation['status'] === ilBookingReservation::STATUS_CANCELLED) { + continue; + } + + $slot_from = (int) $reservation['date_from']; + $slot_to = (int) $reservation['date_to']; + if (count($parts) === 3) { + if ($slot_from !== (int) $parts[1] || $slot_to !== (int) $parts[2]) { + continue; + } + } + + $result[] = [ + 'reservation_id' => (int) ($reservation['booking_reservation_id'] ?? $reservation['id'] ?? 0), + 'object_id' => $object_id, + 'title' => $titles[$object_id], + 'slot_from' => $slot_from, + 'slot_to' => $slot_to + 1, + 'has_schedule' => count($parts) === 3, + 'has_user_active_booking' => + $user_id === (int) $reservation['user_id'] + && (int) $reservation['status'] !== ilBookingReservation::STATUS_CANCELLED, + ]; + + if (count($parts) === 3) { + break; + } + } + } + + return $result; + } + + private function buildItemDescription(array $record): string + { + if (!$record['has_schedule']) { + return $record['title']; + } + + $this->lng->loadLanguageModule('dateplaner'); + $period = ilDatePresentation::formatPeriod( + new ilDateTime($record['slot_from'], IL_CAL_UNIX), + new ilDateTime($record['slot_to'] - 1, IL_CAL_UNIX) + ); + + return "{$record['title']} ({$period})"; + } +} diff --git a/components/ILIAS/BookingManager/src/BookableItem/Table/Action/BookableItemTableDeleteAction.php b/components/ILIAS/BookingManager/src/BookableItem/Table/Action/BookableItemTableDeleteAction.php new file mode 100644 index 000000000000..a7851cd24efb --- /dev/null +++ b/components/ILIAS/BookingManager/src/BookableItem/Table/Action/BookableItemTableDeleteAction.php @@ -0,0 +1,169 @@ +active_management && $this->access->canManageObjects($this->ref_id); + } + + public function getTableAction( + URLBuilder $url_builder, + URLBuilderToken $row_id_token, + URLBuilderToken $action_token, + URLBuilderToken $action_type_token + ): Action { + return $this->ui_factory->table()->action()->standard( + $this->lng->txt(self::ACTION_LABEL), + $url_builder + ->withParameter($action_token, self::ACTION_ID) + ->withParameter($action_type_token, self::SHOW_MODAL_ACTION), + $row_id_token + )->withAsync(); + } + + public function allowActionForRecord(mixed $record): bool + { + return true; + } + + public function getModal( + URLBuilder $url_builder, + array $selected_records, + bool $all_records_selected + ): ?Modal { + return $this->ui_factory->modal()->interruptive( + $this->lng->txt('confirm'), + $this->lng->txt('book_confirm_delete'), + $url_builder->buildURI()->__toString() + )->withAffectedItems( + array_map( + fn(array $record): InterruptiveItem => $this->ui_factory->modal()->interruptiveItem()->standard( + (string) $record['booking_object_id'], + (string) $record['title'] + ), + $selected_records + ) + )->withActionButtonLabel($this->lng->txt('delete')); + } + + public function onSubmit( + URLBuilder $url_builder, + array $selected_records, + bool $all_records_selected + ): ?Modal { + if (!$this->access->canManageObjects($this->ref_id)) { + $this->showErrorMessage($this->lng->txt('no_permission')); + return null; + } + + foreach ($selected_records as $record) { + $object_id = (int) ($record['booking_object_id'] ?? 0); + if ($object_id <= 0) { + continue; + } + + $object = new ilBookingObject($object_id); + $object->deleteReservationsAndCalEntries($object_id); + $object->delete(); + } + + $this->showSuccessMessage($this->lng->txt('book_object_deleted')); + return null; + } + + /** + * @param ?string[] $selected_ids + * @return array[] + */ + protected function resolveRecords(?array $selected_ids = null): array + { + $by_id = []; + foreach (ilBookingObject::getList($this->pool->getId()) as $item) { + $by_id[(int) $item['booking_object_id']] = [ + 'booking_object_id' => (int) $item['booking_object_id'], + 'title' => (string) $item['title'], + ]; + } + + if ($selected_ids === null) { + return array_values($by_id); + } + + $unique = array_values(array_unique(array_map('intval', $selected_ids))); + $result = []; + foreach ($unique as $object_id) { + if (!isset($by_id[$object_id])) { + continue; + } + + $result[] = $by_id[$object_id]; + } + + return $result; + } +} diff --git a/components/ILIAS/BookingManager/src/BookableItem/Table/Action/BookableItemTableEditAction.php b/components/ILIAS/BookingManager/src/BookableItem/Table/Action/BookableItemTableEditAction.php new file mode 100644 index 000000000000..296c2ea7fc76 --- /dev/null +++ b/components/ILIAS/BookingManager/src/BookableItem/Table/Action/BookableItemTableEditAction.php @@ -0,0 +1,101 @@ +active_management && $this->access->canManageObjects($this->ref_id); + } + + public function getTableAction( + URLBuilder $url_builder, + URLBuilderToken $row_id_token, + URLBuilderToken $action_token, + URLBuilderToken $action_type_token + ): Action { + return $this->ui_factory->table()->action()->single( + $this->lng->txt(self::ACTION_LABEL), + $url_builder + ->withParameter($action_token, self::ACTION_ID) + ->withParameter($action_type_token, 'edit'), + $row_id_token + ); + } + + public function onExecute( + URLBuilder $url_builder, + URLBuilderToken $row_id_token, + URLBuilderToken $action_token, + URLBuilderToken $action_type_token + ): mixed { + $row_id = (string) $this->http->resolveRowParameter($row_id_token->getName()); + $object_id = (int) explode('_', $row_id)[0]; + if ($object_id <= 0) { + return null; + } + + $this->ctrl->setParameterByClass(ilBookingObjectGUI::class, 'object_id', (string) $object_id); + $this->ctrl->redirectByClass(ilBookingObjectGUI::class, 'edit'); + return null; + } + + public function allowActionForRecord(mixed $record): bool + { + return true; + } +} diff --git a/components/ILIAS/BookingManager/src/BookableItem/Table/BookableItemTable.php b/components/ILIAS/BookingManager/src/BookableItem/Table/BookableItemTable.php new file mode 100644 index 000000000000..067f04767592 --- /dev/null +++ b/components/ILIAS/BookingManager/src/BookableItem/Table/BookableItemTable.php @@ -0,0 +1,355 @@ +> + */ + private array $reservation_cache = []; + + private ?TableActions $table_actions = null; + + public function __construct( + protected readonly UIFactory $ui_factory, + protected readonly UIRenderer $ui_renderer, + protected readonly ilLanguage $lng, + protected readonly HttpService $http, + protected readonly ilUIService $ui_service, + protected readonly ilCtrlInterface $ctrl, + protected readonly ilGlobalTemplateInterface $tpl, + protected readonly Refinery $refinery, + protected readonly AccessManager $access, + protected readonly ilObjBookingPool $pool, + protected readonly BookingProcessManager $process_manager, + protected readonly Settings $settings, + protected readonly ilObjUser $user, + protected readonly int $ref_id, + protected readonly bool $active_management, + protected readonly int $booking_context_obj_id, + ) { + } + + public function getTableId(): string + { + return static::ID; + } + + /** + * @return Component[] + */ + public function getComponents(URLBuilder $url_builder): array + { + $components = []; + + if ($this->pool->getScheduleType() === ilObjBookingPool::TYPE_NO_SCHEDULE) { + $booking_count = ilBookingReservation::isBookingPoolLimitReachedByUser( + $this->user->getId(), + $this->pool->getId() + ); + + if ($this->pool->getOverallLimit() <= $booking_count) { + $components[] = $this->ui_factory->messageBox()->info($this->lng->txt('book_overall_limit_warning')); + } + } + + $components[] = $filter = $this->getFilterComponent(); + $filter_data = $this->ui_service->filter()->getData($filter); + + $components[] = $this->ui_factory->table()->data( + $this, + $this->lng->txt('book_booking_objects'), + $this->getColumns() + ) + ->withActions($this->getTableActions()->getEnabledActions(...$this->acquireParameters($url_builder))) + ->withRequest($this->http->getRequest()) + ->withId($this->getTableId()) + ->withFilter($filter_data) + ->withOrder(new Order($this instanceof BookableItemWithScheduleTable ? 'date_time' : 'title', Order::ASC)); + + return array_filter($components); + } + + public function getTotalRowCount( + mixed $additional_viewcontrol_data, + mixed $filter_data, + mixed $additional_parameters + ): ?int { + return count($this->loadRecords($filter_data)); + } + + public function getRows( + DataRowBuilder $row_builder, + array $visible_column_ids, + Range $range, + Order $order, + mixed $additional_viewcontrol_data, + mixed $filter_data, + mixed $additional_parameters + ): Generator { + $records = $this->limitRecords($range, $this->sortRecords($order, $this->loadRecords($filter_data))); + + foreach ($records as $record) { + yield $this->getTableActions()->onDataRow( + $row_builder->buildDataRow((string) $record['row_id'], $this->buildRowCells($record)), + $record + ); + } + } + + /** + * @return array + */ + abstract protected function getColumns(): array; + + /** + * @return array + */ + abstract protected function getFilterInputs(): array; + + /** + * @return array[] + */ + abstract protected function loadRecords(mixed $filter_data): array; + + /** + * @return array + */ + abstract protected function buildRowCells(array $record): array; + + /** + * @param array[] $records + * @return array[] + */ + protected function sortRecords(Order $order, array $records): array + { + $order_data = $order->get(); + if ($order_data === []) { + return $records; + } + + foreach ($order_data as $key => $direction) { + $order_direction = $direction === Order::DESC ? -1 : 1; + $callable = $this->getSortCallable($key, $order_direction); + + if ($callable === null) { + continue; + } + + usort($records, $callable); + } + + return $records; + } + + protected function getSortCallable(string $key, int $direction): ?callable + { + return match ($key) { + 'availability' => static fn(array $a, array $b): int + => ((int) $a['available'] <=> (int) $b['available']) * $direction, + 'title' => static fn(array $a, array $b): int + => strcasecmp((string) $a['title'], (string) $b['title']) * $direction, + 'description' => static fn(array $a, array $b): int + => strcasecmp((string) $a['description'], (string) $b['description']) * $direction, + default => null, + }; + } + + /** + * @param array[] $records + * @return array[] + */ + protected function limitRecords(Range $range, array $records): array + { + return array_slice($records, $range->getStart(), $range->getLength()); + } + + protected function getFilterComponent(): FilterComponent + { + $filter_inputs = $this->getFilterInputs(); + + $filter_inputs = $this->presetFilterInputs($filter_inputs); + + return $this->ui_service->filter()->standard( + "bookable_item_filter_{$this->pool->getId()}", + $this->ctrl->getLinkTargetByClass(ilBookingObjectGUI::class), + $filter_inputs, + array_fill(0, count($filter_inputs), true), + true, + true + ); + } + + private function presetFilterInputs(array $filter_inputs): array + { + if ($this->settings->getReservationPeriod() > 0) { + $filter_inputs['period'] = $filter_inputs['period']->withValue( + [ + new DateTimeImmutable('today 00:00:00'), + new DateTimeImmutable("today +{$this->settings->getReservationPeriod()} days 23:59:59") + ] + ); + } + + return $filter_inputs; + } + + /** + * @return array{URLBuilder, URLBuilderToken, URLBuilderToken, URLBuilderToken} + */ + protected function acquireParameters(URLBuilder $url_builder): array + { + return $url_builder->acquireParameters( + [$this->getTableId()], + self::ROW_ID_PARAMETER, + self::ACTION_PARAMETER, + self::ACTION_TYPE_PARAMETER + ); + } + + protected function getTableActions(): TableActions + { + return $this->table_actions ??= (new BookableItemTableActionsFactory( + $this->ctrl, + $this->lng, + $this->tpl, + $this->ui_factory, + $this->ui_renderer, + $this->refinery, + $this->http, + $this->access, + $this->pool, + $this->process_manager, + $this->user, + $this->ref_id, + $this->active_management, + $this->booking_context_obj_id, + array_column($this->loadRecords([]), null, 'row_id'), + ))->getTableActions(); + } + + /** + * @return array[] + */ + protected function loadFilteredBookingObjects( + ?string $title_filter, + ?string $description_filter, + ?array $object_ids_filter + ): array { + $items = ilBookingObject::getList($this->pool->getId()); + $title_filter = mb_strtolower($title_filter ?? '') ?: null; + $description_filter = mb_strtolower($description_filter ?? '') ?: null; + $object_ids_filter = $object_ids_filter ?: null; + + $filtered = []; + foreach ($items as $item) { + $object_id = (int) $item['booking_object_id']; + + if ($object_ids_filter !== null && !in_array($object_id, $object_ids_filter, true)) { + continue; + } + + if ($title_filter !== null && !str_contains(mb_strtolower((string) $item['title']), $title_filter)) { + continue; + } + + if ($description_filter !== null && !str_contains(mb_strtolower((string) ($item['description'] ?? '')), $description_filter)) { + continue; + } + + $filtered[] = $item; + } + + return $filtered; + } + + /** + * @return array{user_id: int, status: ?int, date_from: int, date_to: int}[] + */ + protected function getReservationsForObject(int $booking_object_id): array + { + if (isset($this->reservation_cache[$booking_object_id])) { + return $this->reservation_cache[$booking_object_id]; + } + + $list = ilBookingReservation::getList([$booking_object_id], 1000, 0, []); + $this->reservation_cache[$booking_object_id] = array_values(array_map( + static fn(array $row): array => [ + 'user_id' => (int) $row['user_id'], + 'status' => isset($row['status']) ? (int) $row['status'] : null, + 'date_from' => (int) $row['date_from'], + 'date_to' => (int) $row['date_to'], + ], + $list['data'] ?? [] + )); + + return $this->reservation_cache[$booking_object_id]; + } + + protected function buildAvailabilityCell(int $available, int $total): string + { + $icon = $this->ui_factory->symbol()->icon()->custom( + ilUtil::getImagePath($available > 0 ? 'standard/icon_ok.svg' : 'standard/icon_not_ok.svg'), + $this->lng->txt($available > 0 ? 'book_book' : 'book_no_objects') + ); + return "{$this->ui_renderer->render($icon)} ({$available} / {$total})"; + } +} diff --git a/components/ILIAS/BookingManager/src/BookableItem/Table/BookableItemWithScheduleTable.php b/components/ILIAS/BookingManager/src/BookableItem/Table/BookableItemWithScheduleTable.php new file mode 100644 index 000000000000..52e04bdf11b7 --- /dev/null +++ b/components/ILIAS/BookingManager/src/BookableItem/Table/BookableItemWithScheduleTable.php @@ -0,0 +1,352 @@ + 'monday', + 'tu' => 'tuesday', + 'we' => 'wednesday', + 'th' => 'thursday', + 'fr' => 'friday', + 'sa' => 'saturday', + 'su' => 'sunday', + ]; + + /** + * @return array + */ + protected function getColumns(): array + { + $column_factory = $this->ui_factory->table()->column(); + return [ + 'availability' => $column_factory->text($this->lng->txt('book_table_col_availability'))->withIsSortable(true), + 'date_time' => $column_factory->text($this->lng->txt('book_table_col_datetime'))->withIsSortable(true), + 'title' => $column_factory->text($this->lng->txt('title'))->withIsSortable(true), + 'description' => $column_factory->text($this->lng->txt('description'))->withIsSortable(true), + ]; + } + + /** + * @return array + */ + protected function getFilterInputs(): array + { + $bookable_items = []; + foreach (ilBookingObject::getList($this->pool->getId()) as $item) { + $bookable_items[(int) $item['booking_object_id']] = (string) $item['title']; + } + + $field_factory = $this->ui_factory->input()->field(); + return [ + 'title' => $field_factory->text($this->lng->txt('title')), + 'description' => $field_factory->text($this->lng->txt('description')), + 'objects' => $field_factory->multiSelect($this->lng->txt('book_filter_objects'), $bookable_items), + 'period' => $field_factory->duration($this->lng->txt('book_filter_period')) + ->withUseTime(true) + ->withFormat($this->user->getDateTimeFormat()), + ]; + } + + /** + * @return array[] + */ + protected function loadRecords(mixed $filter_data): array + { + $filter_data = is_array($filter_data) ? $filter_data : []; + + $period_bounds = $this->resolvePeriod($filter_data); + + $booking_objects = $this->loadFilteredBookingObjects( + $this->stringFilter($filter_data, 'title'), + $this->stringFilter($filter_data, 'description'), + $this->arrayFilter($filter_data, 'objects') + ); + + $rows = []; + + foreach ($booking_objects as $item) { + $schedule_id = (int) $item['schedule_id']; + if ($schedule_id === 0) { + continue; + } + + $object_id = (int) $item['booking_object_id']; + $schedule = new ilBookingSchedule($schedule_id); + $slots = $this->enumerateSlots( + $schedule, + $period_bounds[0] ?? null, + $period_bounds[1] ?? null + ); + foreach ($slots as $slot) { + $rows[] = $this->composeRow($item, $object_id, $slot['from'], $slot['to']); + } + } + + return $rows; + } + + /** + * @return array + */ + protected function buildRowCells(array $record): array + { + return [ + 'availability' => $this->buildAvailabilityCell((int) $record['available'], (int) $record['nr_items']), + 'date_time' => $this->formatDateTime((int) $record['slot_from'], (int) $record['slot_to']), + 'title' => (string) $record['title'], + 'description' => nl2br((string) $record['description']), + ]; + } + + protected function getSortCallable(string $key, int $direction): ?callable + { + return $key === 'date_time' + ? static fn(array $a, array $b): int => ((int) $a['slot_from'] <=> (int) $b['slot_from']) * $direction + : parent::getSortCallable($key, $direction); + } + + /** + * @param array{int, int}|null $period_bounds null = no period filter (clip by schedule availability only) + * @return iterable + */ + private function enumerateSlots(ilBookingSchedule $schedule, ?int $period_start = null, ?int $period_end = null): array + { + $definition = $schedule->getDefinition(); + if ($definition === []) { + return []; + } + + $now = time(); + $deadline = $schedule->getDeadline(); + $deadline_timestamp = $now + ($deadline * 3600); + + $availability_from = $schedule->getAvailabilityFrom()?->get(IL_CAL_UNIX); + $availability_to = $schedule->getAvailabilityTo()?->get(IL_CAL_UNIX); + + $slots = []; + + foreach ($definition as $weekday_key => $day_slots) { + $next_week_day = new DateTimeImmutable('next ' . self::WEEKDAYS_MAP[$weekday_key]); + + for ($i = 0; $i < self::MAX_WEEKS_IN_THE_FUTURE; $i++) { + foreach ($day_slots as $time_range) { + [$start_string, $end_string] = explode('-', $time_range); + [$start_hour, $start_minute] = explode(':', $start_string); + [$end_hour, $end_minute] = explode(':', $end_string); + + $start_timestamp = $next_week_day->setTime((int) $start_hour, (int) $start_minute, 0)->getTimestamp(); + $end_timestamp = $next_week_day->setTime((int) $end_hour, (int) $end_minute, 0)->getTimestamp(); + + if ( + (is_int($availability_from) && $start_timestamp < $availability_from) + || (is_int($availability_to) && $end_timestamp > $availability_to) + ) { + continue; + } + + if ( + ($deadline === 0 && $start_timestamp < $now) + || ($deadline === -1 && $end_timestamp < $now) + || ($start_timestamp < $deadline_timestamp) + ) { + continue; + } + + if ( + (is_int($period_start) && $start_timestamp < $period_start) + || (is_int($period_end) && $end_timestamp > $period_end) + ) { + continue; + } + + $slots[] = [ + 'from' => $start_timestamp, + 'to' => $end_timestamp, + ]; + } + + $next_week_day = $next_week_day->modify('+1 week'); + } + } + + return $slots; + } + + /** + * @param array $item + * @return array + */ + private function composeRow(array $item, int $object_id, int $slot_from, int $slot_to): array + { + $user_has_booking = false; + $user_has_active_booking = false; + foreach ($this->getReservationsForObject($object_id) as $reservation) { + if ( + $reservation['user_id'] !== $this->user->getId() + || $reservation['date_from'] !== $slot_from + || $reservation['date_to'] !== $slot_to + ) { + continue; + } + + $user_has_booking = true; + if ($reservation['status'] === ilBookingReservation::STATUS_CANCELLED) { + continue; + } + + $user_has_active_booking = true; + break; + } + + $availability = ilBookingReservation::getAvailableObject( + [$object_id], + $slot_from, + $slot_to, + false, + true + ); + $available = (int) array_sum($availability); + + return [ + 'row_id' => "{$object_id}_{$slot_from}_{$slot_to}", + 'booking_object_id' => $object_id, + 'title' => (string) $item['title'], + 'description' => (string) ($item['description'] ?? ''), + 'nr_items' => (int) $item['nr_items'], + 'available' => $available, + 'is_available' => $available > 0, + 'has_user_booking' => $user_has_booking, + 'has_user_active_booking' => $user_has_active_booking, + 'has_reservations' => $this->hasActiveReservations($object_id, $slot_from, $slot_to), + 'slot_from' => $slot_from, + 'slot_to' => $slot_to, + 'schedule_id' => (int) $item['schedule_id'], + 'post_text' => (string) ($item['post_text'] ?? ''), + ]; + } + + private function hasActiveReservations(int $object_id, int $slot_from, int $slot_to): bool + { + return array_any( + $this->getReservationsForObject($object_id), + static fn(array $reservation): bool => + $reservation['status'] !== ilBookingReservation::STATUS_CANCELLED + && $reservation['date_from'] === $slot_from + && $reservation['date_to'] === $slot_to + ); + } + + public function formatDateTime(int $slot_from, int $slot_to): string + { + $this->lng->loadLanguageModule('dateplaner'); + + return ilDatePresentation::formatPeriod( + new ilDateTime($slot_from, IL_CAL_UNIX, 'UTC'), + new ilDateTime($slot_to, IL_CAL_UNIX, 'UTC') + ); + } + + private function stringFilter(array $filter_data, string $key): ?string + { + return trim((string) ($filter_data[$key] ?? '')) ?: null; + } + + /** + * @return ?int[] + */ + private function arrayFilter(array $filter_data, string $key): ?array + { + if (!isset($filter_data[$key]) || !is_array($filter_data[$key]) || $filter_data[$key] === []) { + return null; + } + + return array_values(array_map('intval', $filter_data[$key])); + } + + /** + * @return array{int, int}|null null when the period filter is not set — rows are not clipped by a default window + */ + private function resolvePeriod(array $filter_data): ?array + { + $period = $filter_data['period'] ?? null; + if (!is_array($period) || count($period) < 2) { + return null; + } + + $from = $this->parsePeriodEndpoint($period[0] ?? null); + $to = $this->parsePeriodEndpoint($period[1] ?? null); + + if ($from === null && $to === null) { + return null; + } + + $default_start = $this->defaultPeriodStart()->getTimestamp(); + + return [ + $from ?? $default_start, + $to ?? PHP_INT_MAX, + ]; + } + + private function defaultPeriodStart(): DateTimeImmutable + { + return new DateTimeImmutable('today', new DateTimeZone($this->userTimeZoneId())); + } + + private function userTimeZoneId(): string + { + return $this->user->getTimeZone() ?: date_default_timezone_get(); + } + + private function parsePeriodEndpoint(mixed $value): ?int + { + if ($value === null || $value === '') { + return null; + } + + if ($value instanceof DateTimeImmutable) { + return $value->getTimestamp(); + } + + try { + return (new DateTimeImmutable((string) $value))->getTimestamp(); + } catch (Throwable) { + return null; + } + } +} diff --git a/components/ILIAS/BookingManager/src/BookableItem/Table/BookableItemWithoutScheduleTable.php b/components/ILIAS/BookingManager/src/BookableItem/Table/BookableItemWithoutScheduleTable.php new file mode 100644 index 000000000000..b924db05679f --- /dev/null +++ b/components/ILIAS/BookingManager/src/BookableItem/Table/BookableItemWithoutScheduleTable.php @@ -0,0 +1,135 @@ + + */ + protected function getColumns(): array + { + $column_factory = $this->ui_factory->table()->column(); + return [ + 'availability' => $column_factory->text($this->lng->txt('book_table_col_availability'))->withIsSortable(true), + 'title' => $column_factory->text($this->lng->txt('title'))->withIsSortable(true), + 'description' => $column_factory->text($this->lng->txt('description'))->withIsSortable(true), + ]; + } + + /** + * @return array + */ + protected function getFilterInputs(): array + { + $bookable_items = []; + foreach (ilBookingObject::getList($this->pool->getId()) as $item) { + $bookable_items[(int) $item['booking_object_id']] = (string) $item['title']; + } + + $field_factory = $this->ui_factory->input()->field(); + return [ + 'title' => $field_factory->text($this->lng->txt('title')), + 'description' => $field_factory->text($this->lng->txt('description')), + 'objects' => $field_factory->multiSelect($this->lng->txt('book_filter_objects'), $bookable_items), + ]; + } + + /** + * @return array[] + */ + protected function loadRecords(mixed $filter_data): array + { + $filter_data = is_array($filter_data) ? $filter_data : []; + + $title_filter = trim((string) ($filter_data['title'] ?? '')) ?: null; + $description_filter = trim((string) ($filter_data['description'] ?? '')) ?: null; + $object_ids_filter = ($filter_data['objects'] ?? []) !== [] + ? array_map('intval', $filter_data['objects']) + : null; + + $rows = []; + foreach ($this->loadFilteredBookingObjects($title_filter, $description_filter, $object_ids_filter) as $item) { + $object_id = (int) $item['booking_object_id']; + $available = ilBookingReservation::numAvailableFromObjectNoSchedule($object_id); + $user_has_booking = false; + $user_has_active_booking = false; + + foreach ($this->getReservationsForObject($object_id) as $reservation) { + $user_id = (int) $reservation['user_id']; + if ($user_id !== $this->user->getId()) { + continue; + } + + $user_has_booking = true; + + if ($reservation['status'] === ilBookingReservation::STATUS_CANCELLED) { + continue; + } + + $user_has_active_booking = true; + break; + } + + $rows[] = [ + 'row_id' => (string) $object_id, + 'booking_object_id' => $object_id, + 'title' => (string) $item['title'], + 'description' => (string) ($item['description'] ?? ''), + 'nr_items' => (int) $item['nr_items'], + 'available' => $available, + 'is_available' => $available > 0, + 'has_user_booking' => $user_has_booking, + 'has_user_active_booking' => $user_has_active_booking, + 'has_reservations' => $this->hasActiveReservations($object_id), + 'post_text' => (string) ($item['post_text'] ?? ''), + ]; + } + + return $rows; + } + + /** + * @return array + */ + protected function buildRowCells(array $record): array + { + return [ + 'availability' => $this->buildAvailabilityCell((int) $record['available'], (int) $record['nr_items']), + 'title' => (string) $record['title'], + 'description' => nl2br((string) $record['description']), + ]; + } + + private function hasActiveReservations(int $object_id): bool + { + return array_any( + $this->getReservationsForObject($object_id), + static fn(array $reservation): bool => $reservation['status'] !== ilBookingReservation::STATUS_CANCELLED + ); + } +}