diff --git a/components/ILIAS/BookingManager/BookingProcess/class.ilBookingProcessWithScheduleGUI.php b/components/ILIAS/BookingManager/BookingProcess/class.ilBookingProcessWithScheduleGUI.php index 16021d7f8446..61e0afd4afcb 100755 --- a/components/ILIAS/BookingManager/BookingProcess/class.ilBookingProcessWithScheduleGUI.php +++ b/components/ILIAS/BookingManager/BookingProcess/class.ilBookingProcessWithScheduleGUI.php @@ -175,7 +175,7 @@ public function week(): void // ok $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_table") => $list_link, $this->lng->txt("book_week") => $week_link ], $this->lng->txt("book_view"))->withActive($this->lng->txt("book_week")); $bar->addComponent($mode_control); diff --git a/components/ILIAS/BookingManager/Objects/class.ilBookingObjectGUI.php b/components/ILIAS/BookingManager/Objects/class.ilBookingObjectGUI.php index 0b858bfc4d0f..3ffdb43f5435 100755 --- a/components/ILIAS/BookingManager/Objects/class.ilBookingObjectGUI.php +++ b/components/ILIAS/BookingManager/Objects/class.ilBookingObjectGUI.php @@ -14,7 +14,10 @@ * https://www.ilias.de * https://github.com/ILIAS-eLearning * - *********************************************************************/ + ******************************************************************** */ + +use ILIAS\BookingManager\BookableItem\BookableItemTable; +use ILIAS\BookingManager\BookableItem\BookableItemTableData; /** * @author Jörg Lützenkirchen @@ -129,12 +132,12 @@ public function isManagementActivated(): bool return $this->management; } - protected function getPoolRefId(): int + public function getPoolRefId(): int { return $this->pool_gui->getRefId(); } - protected function getPoolObjId(): int + public function getPoolObjId(): int { return $this->pool_gui->getObject()->getId(); } @@ -142,7 +145,7 @@ protected function getPoolObjId(): int /** * Has booking pool a schedule? */ - protected function hasPoolSchedule(): bool + public function hasPoolSchedule(): bool { return ($this->pool_gui->getObject()->getScheduleType() === ilObjBookingPool::TYPE_FIX_SCHEDULE); } @@ -150,13 +153,28 @@ protected function hasPoolSchedule(): bool /** * Get booking pool overall limit */ - protected function getPoolOverallLimit(): ?int + public function getPoolOverallLimit(): ?int { return $this->hasPoolSchedule() ? null : $this->pool_gui->getObject()->getOverallLimit(); } + public function getPool(): ilObjBookingPool + { + return $this->pool; + } + + public function getBookingGuiService(): \ILIAS\BookingManager\InternalGUIService + { + return $this->gui; + } + + public function getPoolUsesPreferences(): bool + { + return $this->pool_uses_preferences; + } + /** * @throws ilCtrlException */ @@ -230,57 +248,90 @@ protected function returnToPreferences(): void /** * Render list of booking objects - * uses ilBookingObjectsTableGUI */ public function render(): void { $this->showNoScheduleMessage(); + if (\ilSession::has('book_bulk_flash')) { + $this->tpl->setOnScreenMessage( + (string) (\ilSession::get('book_bulk_flash_type') ?? 'info'), + (string) \ilSession::get('book_bulk_flash'), + true + ); + \ilSession::clear('book_bulk_flash'); + \ilSession::clear('book_bulk_flash_type'); + } $tpl = $this->tpl; - $ilCtrl = $this->ctrl; $lng = $this->lng; - $bar = ""; if ($this->isManagementActivated() && $this->access->canManageObjects($this->getPoolRefId())) { $bar = new ilToolbarGUI(); - $bar->addButton($lng->txt('book_add_object'), $ilCtrl->getLinkTarget($this, 'create')); + $bar->addButton($lng->txt('book_add_object'), $this->ctrl->getLinkTarget($this, 'create')); // bulk creation $this->bulk_creation_gui->modifyToolbar($bar); - - if ($this->hasPoolSchedule()) { - $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")); - $bar->addComponent($mode_control); - } + $this->addTableWeekViewControlToToolbar($bar); + $bar = $bar->getHTML(); + } elseif ($this->hasPoolSchedule() && $this->getAccessHandler()->checkAccess('read', '', $this->getPoolRefId())) { + $bar = new ilToolbarGUI(); + $this->addTableWeekViewControlToToolbar($bar); $bar = $bar->getHTML(); } $tpl->setPermanentLink('book', $this->getPoolRefId()); + $bookable = BookableItemTable::forObjectList($this); + $tpl->setContent( + $bar . $this->gui->ui()->renderer()->render( + $bookable->getComponents($bookable->getActionUrlBuilderForExecuteTableAction()) + ) + ); + } - $table = new ilBookingObjectsTableGUI($this, 'render', $this->getPoolRefId(), $this->getPoolObjId(), $this->hasPoolSchedule(), $this->getPoolOverallLimit(), $this->isManagementActivated()); - $tpl->setContent($bar . $table->getHTML()); + public function executeTableAction(): void + { + $this->showNoScheduleMessage(); + $t = BookableItemTable::forObjectList($this); + $t->execute($t->getActionUrlBuilderForExecuteTableAction()); + $this->ctrl->redirect($this, 'render'); + } + + protected function getAccessHandler(): \ilAccessHandler + { + global $DIC; + return $DIC->access(); + } + + protected function addTableWeekViewControlToToolbar(ilToolbarGUI $bar): void + { + if (!$this->hasPoolSchedule()) { + return; + } + if (!$this->getAccessHandler()->checkAccess('read', '', $this->getPoolRefId())) { + return; + } + $bar->addSeparator(); + $table_link = $this->ctrl->getLinkTarget($this, "render"); + $week_link = $this->ctrl->getLinkTargetByClass("ilBookingProcessWithScheduleGUI", "week"); + $bar->addComponent( + $this->gui->ui()->factory()->viewControl()->mode( + [ + $this->lng->txt("book_table") => $table_link, + $this->lng->txt("book_week") => $week_link + ], + $this->lng->txt("book_view") + )->withActive($this->lng->txt("book_table")) + ); } public function applyFilter(): void { - $table = new ilBookingObjectsTableGUI($this, 'render', $this->getPoolRefId(), $this->getPoolObjId(), $this->hasPoolSchedule(), $this->getPoolOverallLimit(), $this->isManagementActivated()); - $table->resetOffset(); - $table->writeFilterToSession(); $this->render(); } public function resetFilter(): void { - $table = new ilBookingObjectsTableGUI($this, 'render', $this->getPoolRefId(), $this->getPoolObjId(), $this->hasPoolSchedule(), $this->getPoolOverallLimit(), $this->isManagementActivated()); - $table->resetOffset(); - $table->resetFilter(); $this->render(); } @@ -611,4 +662,483 @@ public function deliverInfo(): void $this->objects_manager->deliverObjectInfo($id); } + + /** + * Legacy entry point: bulk selection now opens an async modal (see outputBulkBookModal()). + */ + public function bulkBookForm(): void + { + $this->ctrl->redirect($this, 'render'); + } + + /** + * Renders the bulk-booking modal (async response for data table action). + * + * @param list $row_ids + */ + public function outputBulkBookModal(array $row_ids): void + { + $this->lng->loadLanguageModule('book'); + $f = $this->gui->ui()->factory(); + if (!$this->access->canManageOwnReservations($this->getPoolRefId())) { + $this->gui->send( + $this->gui->ui()->renderer()->render( + $f->messageBox()->failure($this->lng->txt('no_permission')) + ) + ); + } + if ($row_ids === []) { + $this->lng->loadLanguageModule('common'); + $this->gui->send( + $this->gui->ui()->renderer()->render( + $f->messageBox()->info($this->lng->txt('no_checkbox')) + ) + ); + } + $selected = $row_ids; + $available = $this->filterBulkRowIdsToBookable($row_ids); + if ($available === []) { + $this->gui->send( + $this->gui->ui()->renderer()->render( + $f->messageBox()->info($this->lng->txt('book_bulk_all_unavailable')) + ) + ); + } + $skipped = count($selected) - count($available); + $form = $this->buildBulkBookForm($available); + $header = $skipped > 0 + ? $f->messageBox()->info( + sprintf($this->lng->txt('book_bulk_omitted_unavailable'), (string) $skipped) + ) + : null; + $this->sendBulkBookModal($form, $header); + } + + /** + * Async bulk-booking modal: optional UI message box above the form (same as modal+form in Repository GUI). + * + * @param \ILIAS\UI\Component\Component|null $header e.g. messageBox()->info(…) + */ + protected function sendBulkBookModal( + \ILIAS\Repository\Form\FormAdapterGUI $form, + ?\ILIAS\UI\Component\Component $header = null + ): void { + $f = $this->gui->ui()->factory(); + $r = $this->gui->ui()->renderer(); + if ($this->ctrl->isAsynch()) { + $form = $form->asyncModal(); + } else { + $form = $form->syncModal(); + } + $this->lng->loadLanguageModule('common'); + $form_std = $form->getForm(); + $async = $form->isSentAsync() ? 'true' : 'false'; + $on_form_submit_click = "il.repository.ui.submitModalForm(event,$async); return false;"; + $button = $f->button()->standard( + $form->getSubmitLabel(), + '#' + )->withOnLoadCode(function ($id) use ($on_form_submit_click) { + return "$('#$id').click(function(event) {" . $on_form_submit_click . "});"; + }); + $modal = $f->modal()->roundtrip( + $this->lng->txt('book_confirm_booking_schedule_number_of_objects'), + $header, + $form_std->getInputs(), + $form_std->getPostURL() + ) + ->withActionButtons([$button]) + ->withCancelButtonLabel($this->lng->txt('cancel')) + ->withAdditionalOnLoadCode(function ($id) { + return "il.repository.ui.initModal('$id');"; + }); + $this->gui->send($r->renderAsync($modal)); + } + + /** + * Group bulk row IDs by object: each group is one bookable item, sorted by item title; within + * a fixed-schedule object, time slots are sorted by start time. + * + * @param list $row_ids + * @return list}> + */ + protected function groupBulkRowIdsByObject(array $row_ids): array + { + $schedule_by_obj = []; + $nosc = []; + foreach (array_values($row_ids) as $row_id) { + $p = BookableItemTableData::parseRowIdForBulk((string) $row_id); + if ($p === null) { + continue; + } + if (!empty($p['is_slot']) && $p['from'] !== null && $p['to'] !== null) { + $oid = (int) $p['object_id']; + if (!isset($schedule_by_obj[$oid])) { + $schedule_by_obj[$oid] = []; + } + $schedule_by_obj[$oid][] = (string) $row_id; + } else { + $nosc[] = (string) $row_id; + } + } + foreach ($schedule_by_obj as $oid => &$list) { + usort( + $list, + static function (string $a, string $b): int { + $pa = BookableItemTableData::parseRowIdForBulk($a); + $pb = BookableItemTableData::parseRowIdForBulk($b); + if ($pa === null || $pb === null) { + return 0; + } + return ((int) ($pa['from'] ?? 0)) <=> ((int) ($pb['from'] ?? 0)); + } + ); + } + unset($list); + + $groups = []; + foreach (array_keys($schedule_by_obj) as $oid) { + $groups[] = [ + 'object_id' => (int) $oid, + 'is_slot' => true, + 'row_ids' => $schedule_by_obj[$oid], + ]; + } + foreach ($nosc as $row_id) { + $p = BookableItemTableData::parseRowIdForBulk($row_id); + if ($p === null) { + continue; + } + $groups[] = [ + 'object_id' => (int) $p['object_id'], + 'is_slot' => false, + 'row_ids' => [(string) $row_id], + ]; + } + usort( + $groups, + function (array $a, array $b): int { + $oa = new ilBookingObject($a['object_id']); + $ob = new ilBookingObject($b['object_id']); + $c = strcasecmp($oa->getTitle(), $ob->getTitle()); + if ($c !== 0) { + return $c; + } + return $a['object_id'] <=> $b['object_id']; + } + ); + return $groups; + } + + /** + * @param list $row_ids + * @return list + */ + protected function flattenBulkRowIdGroups(array $row_ids): array + { + $flat = []; + foreach ($this->groupBulkRowIdsByObject($row_ids) as $g) { + foreach ($g['row_ids'] as $rid) { + $flat[] = (string) $rid; + } + } + return $flat; + } + + /** + * @param list $row_ids + */ + protected function buildBulkBookForm( + array $row_ids + ): \ILIAS\Repository\Form\FormAdapterGUI { + global $DIC; + $this->lng->loadLanguageModule('book'); + $reservation = $DIC->bookingManager()->internal()->domain()->reservations(); + + $ordered_ids = $this->flattenBulkRowIdGroups($row_ids); + $ids_json = (string) json_encode($ordered_ids, JSON_UNESCAPED_SLASHES); + $form = $this->gui + ->form([self::class], 'bulkBookConfirmed', $this->lng->txt('save')) + ->asyncModal() + ->hidden('bulk_ids', $ids_json) + ->hidden('origin_cmd', 'render'); + $msg_label = (string) $this->lng->txt('book_message'); + $msg_by = (string) $this->lng->txt('book_bulk_message_byline'); + + foreach ($this->groupBulkRowIdsByObject($row_ids) as $g) { + $oid = (int) $g['object_id']; + $obj = new ilBookingObject($oid); + $item_title = $obj->getTitle(); + if ($g['is_slot']) { + $section_info = (string) $this->lng->txt('book_confirm_booking_schedule_number_of_objects_info'); + $form = $form->section('obj_' . $oid, (string) $item_title, $section_info); + foreach ($g['row_ids'] as $i => $row_id) { + $p = BookableItemTableData::parseRowIdForBulk($row_id); + if ($p === null || empty($p['is_slot']) || $p['from'] === null || $p['to'] === null) { + continue; + } + $from = (int) $p['from']; + $to_disp = (int) $p['to'] - 1; + $counter = $reservation->getAvailableNr($oid, $from, $to_disp); + $period = ilDatePresentation::formatPeriod( + new ilDateTime($from, IL_CAL_UNIX), + new ilDateTime($to_disp, IL_CAL_UNIX) + ); + $form = $form->number("nr_{$oid}_{$i}", (string) $period, '', 1, 0, $counter); + } + $form = $form->textarea( + 'message_' . $oid, + $msg_label, + $msg_by + ); + } else { + $form = $form->section('nosc_' . $oid, (string) $item_title, ''); + $form = $form->textarea( + 'message_' . $oid, + $msg_label, + $msg_by + ); + } + } + + return $form; + } + + /** + * @param list $row_ids + * @return list + */ + protected function filterBulkRowIdsToBookable(array $row_ids): array + { + $out = []; + foreach ($row_ids as $row_id) { + if (!$this->isBulkRowIdBookableNow((string) $row_id)) { + continue; + } + $out[] = (string) $row_id; + } + return $out; + } + + protected function isBulkRowIdBookableNow(string $row_id): bool + { + $p = BookableItemTableData::parseRowIdForBulk($row_id); + if ($p === null) { + return false; + } + if (!empty($p['is_slot']) && $p['from'] !== null && $p['to'] !== null) { + $check = \ilBookingReservation::getAvailableObject( + [(int) $p['object_id']], + (int) $p['from'], + (int) $p['to'] - 1, + false, + true + ); + + return array_sum($check) > 0; + } + return \ilBookingReservation::numAvailableFromObjectNoSchedule((int) $p['object_id']) >= 1; + } + + /** + * Hidden fields use {@see \ILIAS\Repository\Form\FormAdapterGUI::hidden} with dedicated names, so the + * POST key is the literal string "form/bulk_ids" (UI groups use slash-separated paths, not form[bulk_ids]). + * Without dedicated names, the first hidden was "form/input_0". + */ + protected function getBulkRowIdsFromRequestBody(array $body): array + { + $candidates = []; + foreach ( + [ + 'bulk_ids', + 'form/bulk_ids', + 'form/input_0', + ] as $k + ) { + if (isset($body[$k]) && $body[$k] !== '' && $body[$k] !== null) { + $candidates[] = (string) $body[$k]; + } + } + if (isset($body['form']) && is_array($body['form']) && array_key_exists('bulk_ids', $body['form'])) { + $candidates[] = (string) $body['form']['bulk_ids']; + } + foreach ($candidates as $raw) { + $decoded = json_decode($raw, true); + if (is_array($decoded) && $this->isBulkBookingRowIdJsonList($decoded)) { + return $decoded; + } + } + + return $this->findBulkBookingRowIdsJsonInRequestArray($body); + } + + /** + * @param list $decoded + */ + protected function isBulkBookingRowIdJsonList(array $decoded): bool + { + if ($decoded === []) { + return false; + } + foreach ($decoded as $id) { + if (!is_string($id) || !str_starts_with($id, 'bobj-')) { + return false; + } + } + return true; + } + + /** + * @return list + */ + protected function findBulkBookingRowIdsJsonInRequestArray(array $body): array + { + foreach ($body as $v) { + if (!is_string($v) || $v === '' || $v[0] !== '[') { + continue; + } + $decoded = json_decode($v, true); + if (is_array($decoded) && $this->isBulkBookingRowIdJsonList($decoded)) { + return $decoded; + } + } + foreach ($body as $v) { + if (!is_array($v)) { + continue; + } + foreach ($v as $vv) { + if (!is_string($vv) || $vv === '' || $vv[0] !== '[') { + continue; + } + $decoded = json_decode($vv, true); + if (is_array($decoded) && $this->isBulkBookingRowIdJsonList($decoded)) { + return $decoded; + } + } + } + return []; + } + + public function bulkBookConfirmed(): void + { + global $DIC; + $this->lng->loadLanguageModule('book'); + if (!$this->access->canManageOwnReservations($this->getPoolRefId())) { + $this->ctrl->redirect($this, 'render'); + } + $body = (array) $DIC->http()->request()->getParsedBody(); + $row_ids = $this->getBulkRowIdsFromRequestBody($body); + if ($row_ids === []) { + $this->gui->send( + $this->gui->ui()->renderer()->render( + $this->gui->ui()->factory()->messageBox()->failure( + $this->lng->txt('book_reservation_failed') + ) + ) + ); + } + $form = $this->buildBulkBookForm($row_ids); + if (!$form->isValid()) { + $this->sendBulkBookModal($form); + } + $data_ids = $form->getData('bulk_ids'); + $parsed_ids = is_string($data_ids) ? json_decode($data_ids, true) : null; + if (!is_array($parsed_ids) || $parsed_ids === []) { + $this->sendBulkBookModal($this->buildBulkBookForm($row_ids)); + } + $process = $DIC->bookingManager()->internal()->domain()->process(); + $ok = 0; + $skip = 0; + $uid = $this->user->getId(); + foreach ($this->groupBulkRowIdsByObject($parsed_ids) as $g) { + $oid = (int) $g['object_id']; + $msg = (string) ($form->getData('message_' . $oid) ?? ''); + if ($g['is_slot']) { + foreach ($g['row_ids'] as $i => $row_id) { + $row_id = (string) $row_id; + $p = BookableItemTableData::parseRowIdForBulk($row_id); + if ($p === null) { + $skip++; + continue; + } + if (!$this->access->canManageReservationForUser($this->getPoolRefId(), $uid)) { + $skip++; + continue; + } + if (empty($p['is_slot']) || $p['from'] === null || $p['to'] === null) { + $skip++; + continue; + } + $check = \ilBookingReservation::getAvailableObject( + [$p['object_id']], + (int) $p['from'], + (int) $p['to'] - 1, + false, + true + ); + if (!array_sum($check)) { + $skip++; + continue; + } + $nr = (int) ($form->getData('nr_' . $oid . '_' . $i) ?? 0); + if ($nr < 0) { + $skip++; + continue; + } + if ($nr === 0) { + continue; + } + $booked = $process->bookAvailableObjects( + (int) $p['object_id'], + $uid, + $uid, + (int) $this->context_obj_id, + (int) $p['from'], + (int) $p['to'], + 0, + $nr, + null, + $msg + ); + if ($booked === []) { + $skip++; + } else { + $ok += count($booked); + } + } + } else { + $row_id = (string) ($g['row_ids'][0] ?? ''); + $p = BookableItemTableData::parseRowIdForBulk($row_id); + if ($p === null) { + $skip++; + continue; + } + if (!$this->access->canManageReservationForUser($this->getPoolRefId(), $uid)) { + $skip++; + continue; + } + if (\ilBookingReservation::numAvailableFromObjectNoSchedule($oid) < 1) { + $skip++; + continue; + } + $process->bookSingle( + $oid, + $uid, + $uid, + (int) $this->context_obj_id, + null, + null, + null, + $msg + ); + $ok++; + } + } + $message = sprintf($this->lng->txt('book_bulk_result'), (string) $ok, (string) $skip); + \ilSession::set('book_bulk_flash', $message); + \ilSession::set('book_bulk_flash_type', $ok > 0 ? 'success' : 'info'); + $back = $this->ctrl->getLinkTarget($this, 'render'); + $this->gui->send( + "" + ); + } } 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..8e3cde5a0ec5 100755 --- a/components/ILIAS/BookingManager/Participants/class.ilBookingParticipantGUI.php +++ b/components/ILIAS/BookingManager/Participants/class.ilBookingParticipantGUI.php @@ -16,6 +16,19 @@ * *********************************************************************/ +use ILIAS\BookingManager\HttpService; +use ILIAS\BookingManager\Participant\ParticipantRepository; +use ILIAS\BookingManager\Participant\ParticipantTable; +use ILIAS\BookingManager\Participant\ParticipantTableBookForParticipantAction; +use ILIAS\BookingManager\Participant\ParticipantTableDeleteAction; +use ILIAS\BookingManager\Participant\ParticipantTableEditBookingAction; +use ILIAS\BookingManager\Common\Table\TableActions; +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 @@ -23,8 +36,6 @@ */ 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; @@ -37,12 +48,20 @@ class ilBookingParticipantGUI protected int $ref_id; protected int $pool_id; + private readonly Refinery $refinery; + private readonly UIFactory $ui_factory; + private readonly UIRenderer $ui_renderer; + private readonly HttpService $http_service; + 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(); @@ -52,7 +71,15 @@ public function __construct( ->internal() ->gui() ->standardRequest(); - + $this->refinery = $DIC->refinery(); + $this->ui_factory = $DIC->ui()->factory(); + $this->ui_renderer = $DIC->ui()->renderer(); + $this->http_service = 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(); @@ -90,28 +117,39 @@ public function executeCommand(): void } } + public function executeTableAction(): void + { + $this + ->configureParticipantTable() + ->execute($this->getTableActionUrlBuilder()); + + $this->render(); + } + /** * Render list of booking participants. - * uses ilBookingParticipantsTableGUI */ public function render(): 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->tpl->setContent($table->getHTML()); + if (!$this->access->canManageParticipants($this->ref_id)) { + return; } + 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 + ) + ); + + $this->tpl->setContent( + $this->ui_renderer->render( + $this->configureParticipantTable()->getComponents($this->getTableActionUrlBuilder()) + ) + ); } public function addUserFromAutoCompleteObject(): bool @@ -164,30 +202,6 @@ public function addParticipantObject( return true; } - public function applyParticipantsFilter(): void - { - $this->applyFilterAction(self::FILTER_ACTION_APPLY); - } - - public function resetParticipantsFilter(): void - { - $this->applyFilterAction(self::FILTER_ACTION_RESET); - } - - 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(); - } - public function assignObjects(): void { $this->tabs->clearTargets(); @@ -197,4 +211,65 @@ public function assignObjects(): void $this->tpl->setContent($table->getHTML()); } + + private function configureParticipantTable(): ParticipantTable + { + return new ParticipantTable( + $this->ui_factory, + $this->lng, + new TableActions( + $this->ctrl, + $this->lng, + $this->tpl, + $this->ui_factory, + $this->ui_renderer, + $this->refinery, + $this->http_service, + [ + ParticipantTableBookForParticipantAction::ACTION_ID => new ParticipantTableBookForParticipantAction( + $this->ui_factory, + $this->lng, + $this->access, + $this->ctrl, + $this->http_service, + $this->ref_id, + $this->pool_id + ), + ParticipantTableEditBookingAction::ACTION_ID => new ParticipantTableEditBookingAction( + $this->ui_factory, + $this->lng, + $this->access, + $this->ctrl, + $this->http_service, + $this->ref_id, + $this->pool_id + ), + ParticipantTableDeleteAction::ACTION_ID => new ParticipantTableDeleteAction( + $this->ui_factory, + $this->lng, + $this->access, + $this->tpl, + $this->http_service, + $this->participant_repository, + $this->ref_id, + $this->pool_id + ), + ] + ), + $this->http_service, + $this->ui_service, + $this->pool_id, + $this->http_service->getRequest() + ); + } + + private function getTableActionUrlBuilder(): URLBuilder + { + return new URLBuilder($this->data_factory->uri( + ILIAS_HTTP_PATH . '/' . $this->ctrl->getLinkTargetByClass( + self::class, + 'executeTableAction' + ) + )); + } } diff --git a/components/ILIAS/BookingManager/Schedule/class.ilBookingScheduleGUI.php b/components/ILIAS/BookingManager/Schedule/class.ilBookingScheduleGUI.php index c1eebbaee03a..504eba492711 100755 --- a/components/ILIAS/BookingManager/Schedule/class.ilBookingScheduleGUI.php +++ b/components/ILIAS/BookingManager/Schedule/class.ilBookingScheduleGUI.php @@ -16,6 +16,19 @@ * *********************************************************************/ +use ILIAS\BookingManager\HttpService; +use ILIAS\BookingManager\Schedule\ScheduleManager; +use ILIAS\BookingManager\Schedule\ScheduleTable; +use ILIAS\BookingManager\Common\Table\TableActions; +use ILIAS\BookingManager\Schedule\ScheduleTableDeleteAction; +use ILIAS\BookingManager\Schedule\ScheduleTableEditAction; +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 * @@ -35,6 +48,14 @@ class ilBookingScheduleGUI protected int $schedule_id; protected int $ref_id; + private readonly Refinery $refinery; + private readonly UIFactory $ui_factory; + private readonly UIRenderer $ui_renderer; + private readonly HttpService $http_service; + private readonly BookingManager $booking_manager; + private readonly ilToolbarGUI $toolbar; + private readonly Factory $data_factory; + public function __construct( ilObjBookingPoolGUI $a_parent_obj ) { @@ -47,6 +68,14 @@ public function __construct( $this->access = $DIC->bookingManager()->internal()->domain()->access(); $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_service = 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() @@ -71,37 +100,55 @@ public function executeCommand(): void switch ($next_class) { default: $cmd = $ilCtrl->getCmd("render"); - $this->$cmd(); + 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->ctrl->redirectByClass( + ilBookingScheduleGUI::class, + '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()) + ) + ); } /** @@ -258,8 +305,8 @@ public function save(): void $this->formToObject($form, $obj); $obj->save(); - $this->tpl->setOnScreenMessage('success', $lng->txt("book_schedule_added")); - $this->render(); + $this->tpl->setOnScreenMessage('success', $lng->txt("book_schedule_added"), true); + $this->ctrl->redirect($this, 'render'); } else { $form->setValuesByPost(); $tpl->setContent($form->getHTML()); @@ -277,8 +324,8 @@ public function update(): void $this->formToObject($form, $obj); $obj->update(); - $this->tpl->setOnScreenMessage('success', $lng->txt("book_schedule_updated")); - $this->render(); + $this->tpl->setOnScreenMessage('success', $lng->txt("book_schedule_updated"), true); + $this->ctrl->redirect($this, 'render'); } else { $form->setValuesByPost(); $tpl->setContent($form->getHTML()); @@ -342,43 +389,69 @@ protected function formToObject( $schedule->setDefinitionBySlots($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"); - - - $conf = new ilConfirmationGUI(); - $conf->setFormAction($ilCtrl->getFormAction($this)); - $conf->setHeaderText($lng->txt('book_confirm_delete')); - - $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'); - - $tpl->setContent($conf->getHTML()); + return new ScheduleTable( + $this->ui_factory, + $this->lng, + new TableActions( + $this->ctrl, + $this->lng, + $this->tpl, + $this->ui_factory, + $this->ui_renderer, + $this->refinery, + $this->http_service, + [ + ScheduleTableEditAction::ACTION_ID => new ScheduleTableEditAction( + $this->ui_factory, + $this->lng, + $this->access, + $this->ctrl, + $this->http_service, + $this->ref_id + ), + ScheduleTableDeleteAction::ACTION_ID => new ScheduleTableDeleteAction( + $this->ui_factory, + $this->lng, + $this->access, + $this->tpl, + $this->http_service, + $schedule_manager, + $this->ref_id + ), + ] + ), + $schedule_manager, + $this->http_service + ); } /** - * Delete schedule + * @param ScheduleManager $schedule_manager + * @param int $pool_id + * + * @return void */ - public function delete(): void + private function checkForInfoMessageAboutMissingBookableItems(ScheduleManager $schedule_manager, int $pool_id): void { - $ilCtrl = $this->ctrl; - $lng = $this->lng; - - $obj = new ilBookingSchedule($this->schedule_id); - $obj->delete(); + $schedule_data = $schedule_manager->getScheduleData(); + if (count($schedule_data) > 0) { + if (!count(ilBookingObject::getList($pool_id))) { + $this->tpl->setOnScreenMessage('info', $this->lng->txt("book_type_warning")); + } + } + } - $this->tpl->setOnScreenMessage('success', $lng->txt('book_schedule_deleted'), true); - $ilCtrl->redirect($this, 'render'); + private function getTableActionUrlBuilder(): URLBuilder + { + // Use current request URI so it can read existing parameters + // This ensures tokens work correctly when actions are rendered + return new URLBuilder($this->data_factory->uri( + ILIAS_HTTP_PATH . '/' . $this->ctrl->getLinkTargetByClass( + self::class, + 'executeTableAction' + ) + )); } } diff --git a/components/ILIAS/BookingManager/src/BookableItem/BookableItemTablBookAction.php b/components/ILIAS/BookingManager/src/BookableItem/BookableItemTablBookAction.php new file mode 100644 index 000000000000..54a8dce784cc --- /dev/null +++ b/components/ILIAS/BookingManager/src/BookableItem/BookableItemTablBookAction.php @@ -0,0 +1,155 @@ +access->canManageOwnReservations($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('book_bulk_book'), + $url_builder->withParameter( + $action_token, + self::ACTION_ID + ), + $row_id_token + )->withAsync(); + } + + public function onExecute( + URLBuilder $url_builder, + URLBuilderToken $row_id_token, + URLBuilderToken $action_token, + URLBuilderToken $action_type_token + ): mixed { + $this->object_gui->outputBulkBookModal( + $this->expandAllObjectsPlaceholder($this->readRowIdsFromRequest($row_id_token->getName())) + ); + return null; + } + + /** + * @return list + */ + private function readRowIdsFromRequest(string $param_name): array + { + $wrapper = $this->http->wrapper(); + if (!$wrapper->query()->has($param_name) && !$wrapper->post()->has($param_name)) { + return []; + } + $bag = $wrapper->query()->has($param_name) ? $wrapper->query() : $wrapper->post(); + $tokens = $bag->retrieve( + $param_name, + $this->refinery->custom()->transformation(function ($v) { + if (is_array($v)) { + return $v; + } + if ($v === null || $v === '') { + return []; + } + return [$v]; + }) + ); + if (!is_array($tokens)) { + return $tokens ? [(string) $tokens] : []; + } + $out = []; + foreach ($tokens as $t) { + if ((string) $t !== '') { + $out[] = (string) $t; + } + } + return $out; + } + + /** + * @param list $ids + * @return list + */ + private function expandAllObjectsPlaceholder(array $ids): array + { + foreach ($ids as $id) { + if ($id === BookableItemTable::ROW_ID_ALL_OBJECTS) { + return $this->data->getAllRowIdStringsForFilter( + ($this->get_filter_data)() + ); + } + } + return $ids; + } + + public function allowActionForRecord(mixed $record): bool + { + if (!\is_array($record) || !isset($record['row'], $record['current_user_bookings'])) { + return false; + } + return $this->data->rowStillAllowsUserBulkBook( + $record['row'], + (int) $record['current_user_bookings'] + ); + } + + public function getSelectionErrorMessage(): ?string + { + return null; + } +} diff --git a/components/ILIAS/BookingManager/src/BookableItem/BookableItemTable.php b/components/ILIAS/BookingManager/src/BookableItem/BookableItemTable.php new file mode 100644 index 000000000000..fa67ed5f045b --- /dev/null +++ b/components/ILIAS/BookingManager/src/BookableItem/BookableItemTable.php @@ -0,0 +1,567 @@ + + */ + public function getComponents(URLBuilder $url_builder): array + { + $filter = $this->getFilterComponent($this->ctrl->getLinkTarget($this->parent, 'render')); + $columns = $this->getColumns(); + if (!$this->has_schedule) { + unset($columns[BookableItemTableData::COL_TIME]); + } + + $table = $this->ui_factory->table()->data($this, $this->lng->txt('book_booking_objects'), $columns) + ->withActions($this->table_actions->getEnabledActions(...$this->acquireParameters($url_builder))) + ->withRequest($this->request) + ->withId(self::ID) + ->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(is_array($filter_data) ? $filter_data : null)); + } + + 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 { + $rows = $this->loadRecords(is_array($filter_data) ? $filter_data : null); + + $order_data = $order->get(); + if ($order_data !== []) { + $order_field = array_keys($order_data)[0]; + $order_direction = $order_data[$order_field]; + + usort($rows, function (array $a, array $b) use ($order_field, $order_direction) { + $a_val = $a[$order_field] ?? ''; + $b_val = $b[$order_field] ?? ''; + + $result = $a_val <=> $b_val; + return $order_direction === Order::ASC ? $result : -$result; + }); + } + + $offset = $range->getStart(); + $length = $range->getLength(); + $rows = array_slice($rows, $offset, $length); + + [$may_edit, $may_assign, $current_user_bookings] = $this->data->getActionContextForRows(); + + foreach ($rows as $row) { + $row_id = $this->data->formatRowId($row); + $cells = $this->data->buildRowCells($row, $current_user_bookings); + + yield $this->table_actions->onDataRow( + $row_builder->buildDataRow($row_id, $cells), + [ + 'row' => $row, + 'may_edit' => $may_edit, + 'may_assign' => $may_assign, + 'current_user_bookings' => $current_user_bookings, + ] + ); + } + } + + /** + * @return array + */ + private function getColumns(): array + { + return [ + BookableItemTableData::COL_AVAIL => $this->ui_factory->table()->column()->text( + $this->lng->txt('book_table_col_availability') + )->withIsSortable(false), + BookableItemTableData::COL_TIME => $this->ui_factory->table()->column()->text( + $this->lng->txt('book_table_col_datetime') + )->withIsSortable($this->has_schedule), + BookableItemTableData::COL_TITLE => $this->ui_factory->table()->column()->text($this->lng->txt('title')), + BookableItemTableData::COL_DESC => $this->ui_factory->table()->column()->text($this->lng->txt('description')), + ]; + } + + /** + * @return array + */ + private function loadRecords(?array $filter_data): array + { + if (!$this->has_schedule) { + return $this->loadNoScheduleRows($filter_data); + } + + [$start, $end] = $this->parsePeriod($filter_data); + + return $this->getSlotRows( + $this->pool_id, + $start, + $end, + isset($filter_data[self::FILTER_TITLE]) && $filter_data[self::FILTER_TITLE] !== '' ? $filter_data[self::FILTER_TITLE] : null, + isset($filter_data[self::FILTER_DESC]) && $filter_data[self::FILTER_DESC] !== '' ? $filter_data[self::FILTER_DESC] : null, + isset($filter_data[self::FILTER_OBJECTS]) && !empty($filter_data[self::FILTER_OBJECTS]) + ? array_map('intval', $filter_data[self::FILTER_OBJECTS]) + : null + ); + } + + + /** + * @return list> + */ + public function getSlotRows( + int $pool_id, + ilDate $period_start, + ilDate $period_end, + ?string $title_filter, + ?string $description_filter, + ?array $bookable_item_ids_filter + ): array { + $rows = []; + $map = ['mo', 'tu', 'we', 'th', 'fr', 'sa', 'su']; + + foreach (ilBookingObject::getList($pool_id) as $obj_row) { + $booking_object_id = (int) $obj_row['booking_object_id']; + if ($bookable_item_ids_filter !== null && $bookable_item_ids_filter !== [] && !in_array($booking_object_id, $bookable_item_ids_filter, true)) { + continue; + } + + if ( + $title_filter !== null + && $title_filter !== '' + && !str_contains(strtolower((string) $obj_row['title']), strtolower($title_filter)) + ) { + continue; + } + + if ($description_filter !== null && $description_filter !== '') { + $desc = (string) ($obj_row['description'] ?? ''); + if (!str_contains(strtolower($desc), strtolower($description_filter))) { + continue; + } + } + + $schedule = new ilBookingSchedule((new ilBookingObject($booking_object_id))->getScheduleId()); + $availability_from = $schedule->getAvailabilityFrom() && !$schedule->getAvailabilityFrom()->isNull() + ? $schedule->getAvailabilityFrom()->get(IL_CAL_DATE) + : null; + $availability_to = $schedule->getAvailabilityTo() && !$schedule->getAvailabilityTo()->isNull() + ? $schedule->getAvailabilityTo()->get(IL_CAL_DATE) + : null; + + $end_date_str = $period_end->get(IL_CAL_DATE); + $day = clone $period_start; + while (true) { + if ($day->get(IL_CAL_DATE) > $end_date_str) { + break; + } + $date_info = $day->get(IL_CAL_FKT_GETDATE, '', 'UTC'); + + if ($availability_from || $availability_to) { + $today = $day->get(IL_CAL_DATE); + if ($availability_from && $availability_from > $today) { + $day->increment(IL_CAL_DAY, 1); + continue; + } + if ($availability_to && $availability_to < $today) { + $day->increment(IL_CAL_DAY, 1); + continue; + } + } + + $slots = []; + if (isset($schedule->getDefinition()[$map[$date_info['isoday'] - 1]])) { + foreach ($schedule->getDefinition()[$map[$date_info['isoday'] - 1]] as $slot) { + $slot = explode('-', $slot); + $slots[] = [ + 'from' => str_replace(':', '', $slot[0]), + 'to' => str_replace(':', '', $slot[1]), + ]; + } + } + + foreach ($slots as $slot) { + $slot_from = mktime( + (int) substr($slot['from'], 0, 2), + (int) substr($slot['from'], 2, 2), + 0, + (int) $date_info['mon'], + (int) $date_info['mday'], + (int) $date_info['year'] + ); + $slot_to = mktime( + (int) substr($slot['to'], 0, 2), + (int) substr($slot['to'], 2, 2), + 0, + (int) $date_info['mon'], + (int) $date_info['mday'], + (int) $date_info['year'] + ); + + $nr_available = ilBookingReservation::getAvailableObject( + [$booking_object_id], + $slot_from, + $slot_to - 1, + false, + true + ); + + if ($schedule->getDeadline() >= 0) { + if ($slot_from < (time() + $schedule->getDeadline() * 60 * 60)) { + continue; + } + } elseif ($slot_to < time()) { + continue; + } + + $rows[] = [ + 'booking_object_id' => $booking_object_id, + 'title' => $obj_row['title'], + 'description' => $obj_row['description'] ?? '', + 'slot_from' => $slot_from, + 'slot_to' => $slot_to, + 'nr_available' => array_sum($nr_available), + 'nr_items' => (int) $obj_row['nr_items'], + 'post_text' => $obj_row['post_text'] ?? '', + 'post_file' => $obj_row['post_file'] ?? null, + 'obj_info_rid' => $obj_row['obj_info_rid'] ?? null, + ]; + } + + $day->increment(IL_CAL_DAY, 1); + } + } + + usort($rows, static fn(array $a, array $b): int => + $a['slot_from'] === $b['slot_from'] + ? strcmp((string) $a['title'], (string) $b['title']) + : $a['slot_from'] <=> $b['slot_from']); + + return $rows; + } + + private function getFilterComponent(string $action): FilterStandard + { + $field_factory = $this->ui_factory->input()->field(); + + $filter_inputs = [ + self::FILTER_TITLE => $field_factory->text($this->lng->txt('title')), + self::FILTER_DESC => $field_factory->text($this->lng->txt('description')), + self::FILTER_OBJECTS => $field_factory->multiSelect( + $this->lng->txt('book_filter_objects'), + array_map( + static fn(array $item): string => $item['title'], + ilBookingObject::getList($this->pool_id) + ) + ), + ]; + + if ($this->has_schedule) { + $filter_inputs[self::FILTER_PERIOD] = $field_factory->duration($this->lng->txt('book_filter_period')) + ->withFormat($this->data->getUser()->getDateTimeFormat()) + ->withUseTime(true) + ->withLabels($this->lng->txt('book_filter_start_date'), $this->lng->txt('book_filter_end_date')); + } + + return $this->ui_service->filter()->standard( + self::FILTER_ID, + $action, + $filter_inputs, + array_fill(0, count($filter_inputs), true), + false, + true + ); + } + + /** + * Filter data in the same shape as passed to {@see getRows()}. + */ + public function getFilterDataForActions(): ?array + { + return $this->ui_service->filter()->getData( + $this->getFilterComponent($this->ctrl->getLinkTarget($this->parent, 'render')) + ); + } + + /** + * @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 + ); + } + + /** + * @return array{0: ilDate, 1: ilDate} + */ + private function parsePeriod(?array $filter_data): array + { + [$default_start, $default_end] = $this->defaultPeriodFromUserWeek(); + if ( + $filter_data === null + || !isset($filter_data[self::FILTER_PERIOD][0]) + || !isset($filter_data[self::FILTER_PERIOD][1]) + ) { + return [$default_start, $default_end]; + } + + [$start, $end] = $filter_data[self::FILTER_PERIOD]; + $start = new ilDate((new DateTime($start))->getTimestamp(), IL_CAL_UNIX); + $end = new ilDate((new DateTime($end))->getTimestamp(), IL_CAL_UNIX); + + return $start > $end ? [$default_start, $default_end] : [$start, $end]; + } + + /** + * @return array{0: ilDate, 1: ilDate} + */ + private function defaultPeriodFromUserWeek(): array + { + $weekday_list = ilCalendarUtil::_buildWeekDayList( + new ilDate(time(), IL_CAL_UNIX), + ilCalendarUserSettings::_getInstanceByUserId($this->user->getId())->getWeekStart() + )->get(); + + return [ + new ilDate(current($weekday_list)->get(IL_CAL_UNIX), IL_CAL_UNIX), + new ilDate(end($weekday_list)->get(IL_CAL_UNIX), IL_CAL_UNIX), + ]; + } + + public function getActionUrlBuilderForExecuteTableAction(): URLBuilder + { + $data_factory = new Factory(); + return new URLBuilder($data_factory->uri( + ILIAS_HTTP_PATH . '/' . $this->ctrl->getLinkTarget( + $this->parent, + 'executeTableAction' + ) + )); + } + + public static function forObjectList(ilBookingObjectGUI $gui): self + { + global $DIC; + $refinery = $DIC->refinery(); + $http_service = new HttpService($DIC->http(), $refinery); + $internal_gui = $gui->getBookingGuiService(); + $ref_id = $gui->getPoolRefId(); + $has_schedule = $gui->hasPoolSchedule(); + $data = new BookableItemTableData( + $gui, + $gui->getPool(), + $ref_id, + $gui->getPoolObjId(), + $has_schedule, + $gui->getPoolOverallLimit(), + $gui->isManagementActivated(), + $internal_gui->process()->getProcessClass($has_schedule), + $DIC->bookingManager()->internal()->domain()->access(), + $DIC->user(), + $DIC->ui()->factory(), + $DIC->ui()->renderer(), + $DIC->language(), + $DIC->ctrl(), + $gui->getPoolUsesPreferences() + ); + $bookable_table = null; + $process_with = $internal_gui->process()->getProcessClass(true); + $process_without = $internal_gui->process()->getProcessClass(false); + $table = new self( + $DIC->ui()->factory(), + $DIC->language(), + new TableActions( + $DIC->ctrl(), + $DIC->language(), + $DIC['tpl'], + $DIC->ui()->factory(), + $DIC->ui()->renderer(), + $refinery, + $http_service, + [ + BookableItemTableBookAction::ACTION_ID => new BookableItemTableBookAction( + $DIC->ui()->factory(), + $DIC->language(), + $DIC->bookingManager()->internal()->domain()->access(), + $gui, + $DIC->http(), + $refinery, + $ref_id, + static function () use (&$bookable_table): ?array { + return $bookable_table?->getFilterDataForActions(); + }, + $data + ), + BookableItemTableAssignParticipantAction::ACTION_ID => new BookableItemTableAssignParticipantAction( + $DIC->ui()->factory(), + $DIC->language(), + $DIC->bookingManager()->internal()->domain()->access(), + $DIC->ctrl(), + $http_service, + $gui, + $ref_id, + $process_with, + $process_without, + $data + ), + BookableItemTableEditAction::ACTION_ID => new BookableItemTableEditAction( + $DIC->ui()->factory(), + $DIC->language(), + $DIC->bookingManager()->internal()->domain()->access(), + $DIC->ctrl(), + $http_service, + $gui, + $ref_id + ), + BookableItemTableDeleteAction::ACTION_ID => new BookableItemTableDeleteAction( + $DIC->ui()->factory(), + $DIC->language(), + $DIC->bookingManager()->internal()->domain()->access(), + $DIC['tpl'], + $DIC->http(), + $refinery, + $gui, + $ref_id, + static function () use (&$bookable_table): ?array { + return $bookable_table?->getFilterDataForActions(); + }, + $data + ), + BookableItemTableLogAction::ACTION_ID => new BookableItemTableLogAction( + $DIC->ui()->factory(), + $DIC->language(), + $DIC->bookingManager()->internal()->domain()->access(), + $DIC->ctrl(), + $http_service, + $gui, + $ref_id, + $data + ), + BookableItemTableCancelBookingAction::ACTION_ID => new BookableItemTableCancelBookingAction( + $DIC->ui()->factory(), + $DIC->language(), + $DIC->bookingManager()->internal()->domain()->access(), + $DIC->ctrl(), + $http_service, + $gui, + $ref_id, + $has_schedule, + $data + ), + ] + ), + $http_service, + $DIC->uiService(), + $http_service->getRequest(), + $gui, + $DIC->ctrl(), + $DIC->user(), + $gui->getPool(), + $ref_id, + $gui->getPoolObjId(), + $has_schedule, + $gui->getPoolUsesPreferences(), + $data + ); + $bookable_table = $table; + return $table; + } +} diff --git a/components/ILIAS/BookingManager/src/BookableItem/BookableItemTableAssignParticipantAction.php b/components/ILIAS/BookingManager/src/BookableItem/BookableItemTableAssignParticipantAction.php new file mode 100644 index 000000000000..81adb7834aa7 --- /dev/null +++ b/components/ILIAS/BookingManager/src/BookableItem/BookableItemTableAssignParticipantAction.php @@ -0,0 +1,136 @@ +object_gui->isManagementActivated() + && $this->access->canManageAllReservations($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_participant'), + $url_builder + ->withParameter($action_token, self::ACTION_ID) + ->withParameter($action_type_token, 'assign'), + $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_service->resolveRowParameter($row_id_token->getName()); + $p = BookableItemTableData::parseRowIdForBulk($row_id); + if ($p === null) { + $this->ctrl->redirect($this->object_gui, 'render'); + return null; + } + $oid = (int) $p['object_id']; + $this->ctrl->setParameter($this->object_gui, 'object_id', (string) $oid); + + if ($this->data->isSchedulePool()) { + if (empty($p['is_slot']) || $p['from'] === null || $p['to'] === null) { + $this->ctrl->redirect($this->object_gui, 'render'); + return null; + } + $day_seed = (new ilDateTime((int) $p['from'], IL_CAL_UNIX))->get(IL_CAL_DATE); + $this->ctrl->setParameter($this->object_gui, 'sseed', $day_seed); + $this->ctrl->setParameterByClass($this->process_with, 'sseed', $day_seed); + $this->ctrl->redirectByClass($this->process_with, 'assignParticipants'); + return null; + } + + $this->ctrl->redirectByClass($this->process_without, 'assignParticipants'); + return null; + } + + public function allowActionForRecord(mixed $record): bool + { + if (!\is_array($record) || !isset($record['row'], $record['may_assign'], $record['current_user_bookings']) + || !(bool) $record['may_assign'] + || isset($record['row']['full_up'])) { + return false; + } + $r = $record['row']; + $not_yet = isset($r['not_yet']) ? (string) $r['not_yet'] : ''; + if ($this->data->isSchedulePool()) { + return $this->data->rowStillAllowsUserBulkBook($r, (int) $record['current_user_bookings']); + } + $n = 0; + foreach ($this->data->reservationsForObject((int) $r['booking_object_id']) as $i) { + if ((int) $i['status'] !== ilBookingReservation::STATUS_CANCELLED) { + $n++; + } + } + return (int) $r['nr_items'] > $n && $not_yet === ''; + } + + public function getSelectionErrorMessage(): ?string + { + return null; + } +} diff --git a/components/ILIAS/BookingManager/src/BookableItem/BookableItemTableCancelBookingAction.php b/components/ILIAS/BookingManager/src/BookableItem/BookableItemTableCancelBookingAction.php new file mode 100644 index 000000000000..5525357a3091 --- /dev/null +++ b/components/ILIAS/BookingManager/src/BookableItem/BookableItemTableCancelBookingAction.php @@ -0,0 +1,104 @@ +pool_has_schedule + && $this->access->canManageOwnReservations($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_set_cancel'), + $url_builder + ->withParameter($action_token, self::ACTION_ID) + ->withParameter($action_type_token, 'cancel'), + $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_service->resolveRowParameter($row_id_token->getName()); + $p = BookableItemTableData::parseRowIdForBulk($row_id); + if ($p === null) { + $this->ctrl->redirect($this->object_gui, 'render'); + return null; + } + $this->ctrl->setParameterByClass('ilbookingreservationsgui', 'object_id', (string) $p['object_id']); + $this->ctrl->redirectByClass('ilbookingreservationsgui', 'rsvConfirmCancelUser'); + return null; + } + + public function allowActionForRecord(mixed $record): bool + { + return \is_array($record) && isset($record['row']) + && $this->data->currentUserHasActiveBookingOnRow($record['row']); + } + + public function getSelectionErrorMessage(): ?string + { + return null; + } +} diff --git a/components/ILIAS/BookingManager/src/BookableItem/BookableItemTableData.php b/components/ILIAS/BookingManager/src/BookableItem/BookableItemTableData.php new file mode 100644 index 000000000000..1557e2bbcb2c --- /dev/null +++ b/components/ILIAS/BookingManager/src/BookableItem/BookableItemTableData.php @@ -0,0 +1,491 @@ +>|null + */ + protected ?array $cache_rows = null; + /** + * @var array + */ + protected array $cache_keys = []; + /** + * @var array>> + */ + protected array $reservation_cache = []; + + public function __construct( + protected ilBookingObjectGUI $parent, + protected ilObjBookingPool $pool, + protected int $ref_id, + protected int $pool_id, + protected bool $has_schedule, + protected ?int $overall_limit, + protected bool $active_management, + protected string $process_class, + protected AccessManager $access, + protected ilObjUser $user, + protected UIFactory $ui_factory, + protected UIRenderer $ui_renderer, + protected ilLanguage $lng, + protected ilCtrl $ctrl, + protected bool $pool_uses_preferences = false + ) { + } + + public function hasDateTimeColumn(): bool + { + return $this->has_schedule; + } + + public function getCtrl(): ilCtrl + { + return $this->ctrl; + } + + public function getUser(): ilObjUser + { + return $this->user; + } + + /** + * @return array{0: bool, 1: bool, 2: int} may_edit, may_assign, current_user_bookings + */ + public function getActionContextForRows(): array + { + $may_edit = $this->active_management && $this->access->canManageObjects($this->ref_id); + $may_assign = $this->active_management && $this->access->canManageAllReservations($this->ref_id); + $current_user_bookings = 0; + if (!$this->has_schedule && $this->overall_limit) { + foreach ($this->reservation_cache as $obj_rsv) { + foreach ($obj_rsv as $rsv) { + if ((int) $rsv['status'] !== ilBookingReservation::STATUS_CANCELLED && + (int) $rsv['user_id'] === $this->user->getId()) { + $current_user_bookings++; + } + } + } + } + return [$may_edit, $may_assign, $current_user_bookings]; + } + + public function withReservationCache( + int $object_id, + array $reservations + ): self { + $c = clone $this; + $c->reservation_cache[$object_id] = $reservations; + return $c; + } + + public function isSchedulePool(): bool + { + return $this->has_schedule; + } + + public function getRefId(): int + { + return $this->ref_id; + } + + /** + * @return list> + */ + public function reservationsForObject(int $objectId): array + { + return $this->reservation_cache[$objectId] ?? []; + } + + public function userOverallLimit(): ?int + { + return $this->overall_limit; + } + + /** + * Same rule as the former per-row bulk-book flag: self-service may add a booking on this row. + */ + public function rowStillAllowsUserBulkBook(array $r, int $poolUserBookings): bool + { + if (!$this->access->canManageOwnReservations($this->ref_id)) { + return false; + } + $not_yet = isset($r['full_up']) ? (string) $r['full_up'] : (isset($r['not_yet']) ? (string) $r['not_yet'] : ''); + if ($not_yet !== '') { + return false; + } + $oid = (int) $r['booking_object_id']; + if ($this->has_schedule) { + foreach ($this->reservation_cache[$oid] ?? [] as $i) { + if ((int) $i['status'] === ilBookingReservation::STATUS_CANCELLED + || (int) $i['user_id'] !== (int) $this->user->getId()) { + continue; + } + if ((int) $i['date_from'] === (int) $r['slot_from'] && (int) $i['date_to'] === (int) $r['slot_to']) { + return false; + } + } + return (int) ($r['nr_available'] ?? 0) > 0; + } + $n = 0; + foreach ($this->reservation_cache[$oid] ?? [] as $i) { + if ((int) $i['status'] !== ilBookingReservation::STATUS_CANCELLED) { + $n++; + } + } + if ((int) $r['nr_items'] <= $n) { + return false; + } + if ($this->overall_limit !== null && $poolUserBookings >= (int) $this->overall_limit) { + return false; + } + foreach ($this->reservation_cache[$oid] ?? [] as $i) { + if ((int) $i['status'] !== ilBookingReservation::STATUS_CANCELLED + && (int) $i['user_id'] === (int) $this->user->getId()) { + return false; + } + } + return true; + } + + public function currentUserHasActiveBookingOnRow(array $r): bool + { + $oid = (int) $r['booking_object_id']; + if ($this->has_schedule) { + foreach ($this->reservation_cache[$oid] ?? [] as $i) { + if ((int) $i['status'] === ilBookingReservation::STATUS_CANCELLED + || (int) $i['user_id'] !== (int) $this->user->getId()) { + continue; + } + if ((int) $i['date_from'] === (int) $r['slot_from'] && (int) $i['date_to'] === (int) $r['slot_to']) { + return true; + } + } + return false; + } + foreach ($this->reservation_cache[$oid] ?? [] as $i) { + if ((int) $i['status'] !== ilBookingReservation::STATUS_CANCELLED + && (int) $i['user_id'] === (int) $this->user->getId()) { + return true; + } + } + return false; + } + + /** + * @return list> + */ + public function loadAllRows( + ?array $filter_data + ): array { + if ($this->cache_rows !== null) { + return $this->cache_rows; + } + if ($this->has_schedule) { + [$p_start, $p_end] = $this->parsePeriod($filter_data); + $title_f = $this->parseTextFilter($filter_data, BookableItemTable::FILTER_TITLE); + $desc_f = $this->parseTextFilter($filter_data, BookableItemTable::FILTER_DESC); + $obj_f = $this->parseObjectFilter($filter_data); + $this->cache_rows = ilBookableItemsSlotQuery::getSlotRows( + $this->pool_id, + $p_start, + $p_end, + $title_f, + $desc_f, + $obj_f + ); + } else { + $this->cache_rows = $this->loadNoScheduleRows($filter_data); + } + foreach ($this->cache_rows as $r) { + $oid = (int) $r['booking_object_id']; + if (!isset($this->reservation_cache[$oid])) { + $item_rsv = ilBookingReservation::getList([$oid], 1000, 0, []); + $this->reservation_cache[$oid] = $item_rsv['data']; + } + } + return $this->cache_rows; + } + + /** + * @return list> + */ + protected function loadNoScheduleRows( + ?array $filter_data + ): array { + $data = ilBookingObject::getList($this->pool_id, null); + $title_f = $this->parseTextFilter($filter_data, BookableItemTable::FILTER_TITLE); + if ($title_f !== null && $title_f !== '') { + $data = array_values(array_filter($data, static function (array $row) use ($title_f) { + return str_contains(strtolower((string) $row['title']), strtolower($title_f)); + })); + } + $desc_f = $this->parseTextFilter($filter_data, BookableItemTable::FILTER_DESC); + $obj_f = $this->parseObjectFilter($filter_data); + $rows = []; + foreach ($data as $item) { + $oid = (int) $item['booking_object_id']; + if ($obj_f !== null && $obj_f !== [] && !in_array($oid, $obj_f, true)) { + continue; + } + if ($desc_f !== null && $desc_f !== '' && + !str_contains(strtolower((string) ($item['description'] ?? '')), strtolower($desc_f))) { + continue; + } + $item['is_slot'] = false; + $item['slot_from'] = null; + $item['slot_to'] = null; + if (isset($item['full_up']) || isset($item['not_yet'])) { + // keep flags from getList? getList for no schedule may not set these + } + $rows[] = $item; + } + return $rows; + } + + /** + * Date/time column: weekday (long), calendar date, time range (same day as {@see ilDatePresentation::formatPeriod} end). + */ + protected function formatScheduleSlotForTable(int $slot_from, int $slot_to): string + { + $this->lng->loadLanguageModule('dateplaner'); + $tz = $this->user->getTimeZone(); + $start = new ilDateTime($slot_from, IL_CAL_UNIX, $tz); + $end = new ilDateTime($slot_to - 1, IL_CAL_UNIX, $tz); + if (!ilDateTime::_equals($start, $end, IL_CAL_DAY, $tz)) { + return ilDatePresentation::formatPeriod($start, $end); + } + $wd_keys = [ + 0 => 'Su_long', + 1 => 'Mo_long', + 2 => 'Tu_long', + 3 => 'We_long', + 4 => 'Th_long', + 5 => 'Fr_long', + 6 => 'Sa_long', + ]; + $date_info = $start->get(IL_CAL_FKT_GETDATE, '', $tz); + $wday = (int) $date_info['wday']; + $weekday = $this->lng->txt($wd_keys[$wday]); + $date_part = (int) $date_info['mday'] . '. ' . + ilCalendarUtil::_numericMonthToString((int) $date_info['mon'], false, $this->lng) . ' ' . + (int) $date_info['year']; + if ($this->user->getTimeFormat() === ilCalendarSettings::TIME_FORMAT_12) { + $t0 = $start->get(IL_CAL_FKT_DATE, 'g:ia', $tz); + $t1 = $end->get(IL_CAL_FKT_DATE, 'g:ia', $tz); + } else { + $t0 = $start->get(IL_CAL_FKT_DATE, 'H:i', $tz); + $t1 = $end->get(IL_CAL_FKT_DATE, 'H:i', $tz); + } + + return $weekday . ', ' . $date_part . ', ' . $t0 . ' - ' . $t1; + } + + /** + * @param array $row + * @return array + */ + public function buildRowCells(array $row, int $current_user_bookings): array + { + $oid = (int) $row['booking_object_id']; + $a_set = $row; + $booking_possible = $this->access->canManageOwnReservations($this->ref_id); + $has_booking = false; + if ($this->has_schedule) { + $res = $this->reservation_cache[$oid] ?? []; + foreach ($res as $item) { + if ((int) $item['status'] === ilBookingReservation::STATUS_CANCELLED) { + continue; + } + if ((int) $item['user_id'] !== (int) $this->user->getId()) { + continue; + } + if ((int) $item['date_from'] === (int) $row['slot_from'] && + (int) $item['date_to'] === (int) $row['slot_to']) { + $has_booking = true; + break; + } + } + } else { + $res = $this->reservation_cache[$oid] ?? []; + $cnt = 0; + foreach ($res as $item) { + if ((int) $item['status'] !== ilBookingReservation::STATUS_CANCELLED) { + $cnt++; + if ((int) $item['user_id'] === (int) $this->user->getId()) { + $has_booking = true; + } + } + } + $nr = (int) $a_set['nr_items']; + if ($nr <= $cnt || ($this->overall_limit && $current_user_bookings >= (int) $this->overall_limit)) { + $booking_possible = false; + } + if ($has_booking) { + $booking_possible = false; + } + } + $not_yet = ''; + if (isset($a_set['full_up'])) { + $booking_possible = false; + $not_yet = (string) $a_set['full_up']; + } elseif (isset($a_set['not_yet'])) { + $not_yet = (string) $a_set['not_yet']; + } + $avail = ''; + if ($this->has_schedule) { + $ok = $booking_possible && (int) $row['nr_available'] > 0 && $not_yet === ''; + $g = $ok + ? $this->ui_factory->symbol()->icon()->custom( + ilUtil::getImagePath('standard/icon_ok.svg'), + $this->lng->txt('book_book'), + 'small' + ) + : $this->ui_factory->symbol()->icon()->custom( + ilUtil::getImagePath('standard/icon_not_ok.svg'), + $this->lng->txt('book_no_objects'), + 'small' + ); + $avail = $this->ui_renderer->render($g) . ' ' . (int) $row['nr_available'] . ' / ' . (int) $row['nr_items']; + if ($not_yet !== '') { + $avail .= ' ' . $not_yet; + } + if (!$ok) { + $booking_possible = false; + } + } else { + $cnt = 0; + foreach ($this->reservation_cache[$oid] ?? [] as $item) { + if ((int) $item['status'] !== ilBookingReservation::STATUS_CANCELLED) { + $cnt++; + } + } + $nr = (int) $a_set['nr_items']; + $avail_n = $nr - $cnt; + $ok = $avail_n > 0 && $booking_possible && $not_yet === ''; + $g = $ok + ? $this->ui_factory->symbol()->icon()->custom( + ilUtil::getImagePath('standard/icon_ok.svg'), + (string) $avail_n, + 'small' + ) + : $this->ui_factory->symbol()->icon()->custom( + ilUtil::getImagePath('standard/icon_not_ok.svg'), + (string) $avail_n, + 'small' + ); + $avail = $this->ui_renderer->render($g) . ' ' . $avail_n . ' / ' . (int) $a_set['nr_items']; + } + $time_str = '—'; + if ($this->has_schedule) { + $time_str = $this->formatScheduleSlotForTable( + (int) $row['slot_from'], + (int) $row['slot_to'] + ); + } + $cells = [ + self::COL_AVAIL => $avail, + ]; + if ($this->has_schedule) { + $cells[self::COL_TIME] = $time_str; + } + $cells[self::COL_TITLE] = (string) $a_set['title']; + $cells[self::COL_DESC] = nl2br((string) ($a_set['description'] ?? '')); + return $cells; + } + + /** + * @param array $row + */ + public function formatRowId(array $row): string + { + $oid = (int) $row['booking_object_id']; + if ($this->has_schedule) { + return 'bobj-' . $oid . '-' . (int) $row['slot_from'] . '-' . (int) $row['slot_to']; + } + return 'bobj-' . $oid; + } + + /** + * All row id strings for the current filter (same rows as the table, ignoring pagination). + * Used when the data table multi-action "all objects" sends the placeholder {@see BookableItemTable::ROW_ID_ALL_OBJECTS}. + * + * @param array|null $filter_data same shape as passed to {@see getRows()} + * @return list + */ + public function getAllRowIdStringsForFilter(?array $filter_data): array + { + $this->cache_rows = null; + $this->reservation_cache = []; + $rows = $this->loadAllRows($filter_data); + $ids = []; + foreach ($rows as $row) { + $ids[] = $this->formatRowId($row); + } + return $ids; + } + + /** + * @return array{object_id: int, from: ?int, to: ?int, is_slot: bool}|null + */ + public static function parseRowIdForBulk(string $row_id): ?array + { + $p = explode('-', $row_id); + if (count($p) === 2 && $p[0] === 'bobj') { + return ['object_id' => (int) $p[1], 'from' => null, 'to' => null, 'is_slot' => false]; + } + if (count($p) === 4 && $p[0] === 'bobj') { + return [ + 'object_id' => (int) $p[1], + 'from' => (int) $p[2], + 'to' => (int) $p[3], + 'is_slot' => true, + ]; + } + return null; + } +} diff --git a/components/ILIAS/BookingManager/src/BookableItem/BookableItemTableDeleteAction.php b/components/ILIAS/BookingManager/src/BookableItem/BookableItemTableDeleteAction.php new file mode 100644 index 000000000000..35233a3419fd --- /dev/null +++ b/components/ILIAS/BookingManager/src/BookableItem/BookableItemTableDeleteAction.php @@ -0,0 +1,324 @@ +object_gui->isManagementActivated() + && $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('delete'), + $url_builder + ->withParameter($action_token, self::ACTION_ID) + ->withParameter($action_type_token, self::SHOW_MODAL_ACTION), + $row_id_token + )->withAsync(); + } + + public function onExecute( + URLBuilder $url_builder, + URLBuilderToken $row_id_token, + URLBuilderToken $action_token, + URLBuilderToken $action_type_token + ): mixed { + $action_type = (string) $this->resolveStringParameter( + $action_type_token->getName(), + self::SHOW_MODAL_ACTION + ); + if ($action_type === self::SUBMIT_MODAL_ACTION) { + return $this->submit($url_builder, $row_id_token, $action_token, $action_type_token); + } + return $this->showDeleteModal($url_builder, $row_id_token, $action_token, $action_type_token); + } + + public function allowActionForRecord(mixed $record): bool + { + if (!\is_array($record) || !isset($record['row'], $record['may_edit']) || !(bool) $record['may_edit']) { + return false; + } + foreach ($this->data->reservationsForObject((int) $record['row']['booking_object_id']) as $i) { + if ((int) $i['status'] !== ilBookingReservation::STATUS_CANCELLED) { + return false; + } + } + return true; + } + + public function getSelectionErrorMessage(): ?string + { + return $this->lng->txt('no_valid_selection'); + } + + private function showDeleteModal( + URLBuilder $url_builder, + URLBuilderToken $row_id_token, + URLBuilderToken $action_token, + URLBuilderToken $action_type_token + ): ?Modal { + $this->lng->loadLanguageModule('book'); + $this->lng->loadLanguageModule('common'); + $row_ids = $this->readRowIdsFromRequest($row_id_token->getName()); + $row_ids = $this->expandAllObjectsPlaceholder($row_ids); + if ($row_ids === []) { + $this->tpl->setOnScreenMessage( + ilGlobalTemplateInterface::MESSAGE_TYPE_INFO, + $this->lng->txt('no_checkbox'), + true + ); + return null; + } + $records = $this->resolveDeletableObjectRecords($row_ids); + if ($records === []) { + $this->tpl->setOnScreenMessage( + ilGlobalTemplateInterface::MESSAGE_TYPE_INFO, + $this->getSelectionErrorMessage(), + true + ); + return null; + } + + return $this->ui_factory->modal()->interruptive( + $this->lng->txt('confirm'), + $this->lng->txt('book_confirm_delete'), + $url_builder + ->withParameter($row_id_token, $row_ids) + ->withParameter($action_token, self::ACTION_ID) + ->withParameter($action_type_token, self::SUBMIT_MODAL_ACTION) + ->buildURI() + ->__toString() + )->withAffectedItems( + array_map( + function (array $r) { + return $this->ui_factory->modal()->interruptiveItem()->standard( + (string) $r['booking_object_id'], + (string) $r['title'] + ); + }, + $records + ) + )->withActionButtonLabel($this->lng->txt('delete')); + } + + private function submit( + URLBuilder $_url_builder, + URLBuilderToken $row_id_token, + URLBuilderToken $_action_token, + URLBuilderToken $_action_type_token + ): ?Modal { + $this->lng->loadLanguageModule('book'); + $this->lng->loadLanguageModule('common'); + if (!$this->object_gui->isManagementActivated() + || !$this->access->canManageObjects($this->ref_id)) { + $this->tpl->setOnScreenMessage( + ilGlobalTemplateInterface::MESSAGE_TYPE_FAILURE, + $this->lng->txt('no_permission'), + true + ); + return null; + } + $row_ids = $this->readRowIdsFromRequest($row_id_token->getName()); + $row_ids = $this->expandAllObjectsPlaceholder($row_ids); + if ($row_ids === []) { + $this->tpl->setOnScreenMessage( + ilGlobalTemplateInterface::MESSAGE_TYPE_FAILURE, + $this->getSelectionErrorMessage(), + true + ); + return null; + } + $records = $this->resolveDeletableObjectRecords($row_ids); + if ($records === []) { + $this->tpl->setOnScreenMessage( + ilGlobalTemplateInterface::MESSAGE_TYPE_FAILURE, + $this->getSelectionErrorMessage(), + true + ); + return null; + } + foreach ($records as $r) { + $oid = (int) $r['booking_object_id']; + $obj = new ilBookingObject($oid); + $obj->deleteReservationsAndCalEntries($oid); + $obj->delete(); + } + $this->tpl->setOnScreenMessage( + ilGlobalTemplateInterface::MESSAGE_TYPE_SUCCESS, + $this->lng->txt('book_object_deleted'), + true + ); + return null; + } + + /** + * @param list $row_ids + * @return list + */ + private function resolveDeletableObjectRecords(array $row_ids): array + { + $by_object = []; + foreach ($row_ids as $row_id) { + $p = BookableItemTableData::parseRowIdForBulk($row_id); + if ($p === null) { + continue; + } + $oid = (int) $p['object_id']; + if (isset($by_object[$oid])) { + continue; + } + if ($this->objectHasActiveReservations($oid)) { + continue; + } + $by_object[$oid] = [ + 'booking_object_id' => $oid, + 'title' => (new ilBookingObject($oid))->getTitle(), + ]; + } + return array_values($by_object); + } + + private function objectHasActiveReservations(int $booking_object_id): bool + { + $res = ilBookingReservation::getList([$booking_object_id], 1000, 0, []); + foreach ($res['data'] ?? [] as $row) { + if ((int) $row['status'] !== ilBookingReservation::STATUS_CANCELLED) { + return true; + } + } + return false; + } + + private function resolveStringParameter(string $name, string $default): string + { + $w = $this->http->wrapper(); + if ($w->query()->has($name)) { + return (string) $w->query()->retrieve( + $name, + $this->refinery->kindlyTo()->string() + ); + } + if ($w->post()->has($name)) { + return (string) $w->post()->retrieve( + $name, + $this->refinery->kindlyTo()->string() + ); + } + return $default; + } + + /** + * @return list + */ + private function readRowIdsFromRequest(string $param_name): array + { + $wrapper = $this->http->wrapper(); + if (!$wrapper->query()->has($param_name) && !$wrapper->post()->has($param_name)) { + return []; + } + $bag = $wrapper->query()->has($param_name) ? $wrapper->query() : $wrapper->post(); + $tokens = $bag->retrieve( + $param_name, + $this->refinery->custom()->transformation(function ($v) { + if (is_array($v)) { + return $v; + } + if ($v === null || $v === '') { + return []; + } + return [$v]; + }) + ); + if (!is_array($tokens)) { + return $tokens ? [(string) $tokens] : []; + } + $out = []; + foreach ($tokens as $t) { + if ((string) $t !== '') { + $out[] = (string) $t; + } + } + return $out; + } + + /** + * @param list $ids + * @return list + */ + private function expandAllObjectsPlaceholder(array $ids): array + { + foreach ($ids as $id) { + if ($id === BookableItemTable::ROW_ID_ALL_OBJECTS) { + return $this->data->getAllRowIdStringsForFilter( + ($this->get_filter_data)() + ); + } + } + return $ids; + } +} diff --git a/components/ILIAS/BookingManager/src/BookableItem/BookableItemTableEditAction.php b/components/ILIAS/BookingManager/src/BookableItem/BookableItemTableEditAction.php new file mode 100644 index 000000000000..6453a480717c --- /dev/null +++ b/components/ILIAS/BookingManager/src/BookableItem/BookableItemTableEditAction.php @@ -0,0 +1,101 @@ +object_gui->isManagementActivated() + && $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('edit'), + $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_service->resolveRowParameter($row_id_token->getName()); + $p = BookableItemTableData::parseRowIdForBulk($row_id); + if ($p === null) { + $this->ctrl->redirect($this->object_gui, 'render'); + return null; + } + $this->ctrl->setParameter($this->object_gui, 'object_id', (string) $p['object_id']); + $this->ctrl->redirect($this->object_gui, 'edit'); + return null; + } + + public function allowActionForRecord(mixed $record): bool + { + return \is_array($record) && (bool) ($record['may_edit'] ?? false); + } + + public function getSelectionErrorMessage(): ?string + { + return null; + } +} diff --git a/components/ILIAS/BookingManager/src/BookableItem/BookableItemTableLogAction.php b/components/ILIAS/BookingManager/src/BookableItem/BookableItemTableLogAction.php new file mode 100644 index 000000000000..683e401a3dc5 --- /dev/null +++ b/components/ILIAS/BookingManager/src/BookableItem/BookableItemTableLogAction.php @@ -0,0 +1,105 @@ +access->canManageOwnReservations($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_log'), + $url_builder + ->withParameter($action_token, self::ACTION_ID) + ->withParameter($action_type_token, 'log'), + $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_service->resolveRowParameter($row_id_token->getName()); + $p = BookableItemTableData::parseRowIdForBulk($row_id); + if ($p === null) { + $this->ctrl->redirect($this->object_gui, 'render'); + return null; + } + $this->ctrl->setParameterByClass('ilBookingReservationsGUI', 'object_id', (string) $p['object_id']); + $this->ctrl->redirectByClass('ilBookingReservationsGUI', 'log'); + return null; + } + + public function allowActionForRecord(mixed $record): bool + { + if (!\is_array($record) || !isset($record['row'], $record['may_edit'])) { + return false; + } + return (bool) $record['may_edit'] + || $this->data->currentUserHasActiveBookingOnRow($record['row']); + } + + public function getSelectionErrorMessage(): ?string + { + return null; + } +} 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..759d45e97562 --- /dev/null +++ b/components/ILIAS/BookingManager/src/Common/Table/Table.php @@ -0,0 +1,17 @@ + + */ + 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..7fd2d83f23d2 --- /dev/null +++ b/components/ILIAS/BookingManager/src/Common/Table/TableAction.php @@ -0,0 +1,57 @@ +table_actions->execute(...$this->acquireParameters($url_builder)); + } + + abstract protected function acquireParameters(URLBuilder $url_builder): array; +} 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..be2e0174391f --- /dev/null +++ b/components/ILIAS/BookingManager/src/Common/Table/TableActionModalTrait.php @@ -0,0 +1,111 @@ +http_service->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, + ): ?Modal { + $selected_ids = $this->http_service->resolveRowParameters($row_id_token->getName()); + + $selected_records = $selected_ids === [] ? [] : array_filter( + $this->resolveRecords($selected_ids === 'ALL_OBJECTS' ? [] : $selected_ids), + static fn(array $record): bool => !isset($record['is_used']) ? true : !$record['is_used'] + ); + + return $this->getModal( + $url_builder + ->withParameter($row_id_token, $selected_ids) + ->withParameter($action_token, $this->getActionId()) + ->withParameter($action_type_token, self::SUBMIT_MODAL_ACTION), + $selected_records, + false + ); + } + + protected function submit( + URLBuilder $url_builder, + URLBuilderToken $row_id_token, + URLBuilderToken $action_token, + URLBuilderToken $action_type_token, + ): ?Modal { + $selected_ids = $this->http_service->resolveRowParameters($row_id_token->getName()); + + if ($selected_ids === []) { + $this->showErrorMessage($this->getSelectionErrorMessage()); + return null; + } + + $selected_records = array_filter( + $this->resolveRecords($selected_ids === 'ALL_OBJECTS' ? [] : $selected_ids), + static fn(array $record): bool => !isset($record['is_used']) ? true : !$record['is_used'] + ); + + return $this->onSubmit( + $url_builder + ->withParameter($row_id_token, $selected_ids) + ->withParameter($action_token, $this->getActionId()) + ->withParameter($action_type_token, self::SUBMIT_MODAL_ACTION), + $selected_records, + false + ); + } + + 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); + } + + /** + * @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): 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..d8bfed52b78f --- /dev/null +++ b/components/ILIAS/BookingManager/src/Common/Table/TableActions.php @@ -0,0 +1,128 @@ + $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_service, + 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( + function (TableAction $action) use ( + $url_builder, + $row_id_token, + $action_token, + $action_type_token + ): ?Action { + if (!$action->isAvailable()) { + return null; + } + + return $action->getTableAction( + $url_builder, + $row_id_token, + $action_token, + $action_type_token + ); + }, + $this->actions + ) + ); + } + + public function execute( + URLBuilder $url_builder, + URLBuilderToken $row_id_token, + URLBuilderToken $action_token, + URLBuilderToken $action_type_token + ): ?Modal { + if (!$this->http_service->has($action_token->getName())) { + return null; + } + + $action_id = $this->http_service->resolveRowParameter($action_token->getName()); + + if ($action_id === null || !isset($this->actions[$action_id])) { + return null; + } + + $action = $this->actions[$action_id]; + $response = $action->onExecute( + $url_builder, + $row_id_token, + $action_token, + $action_type_token + ); + + if ($response instanceof Modal) { + $this->http_service->sendAsync( + $this->ui_renderer->renderAsync( + $response + ) + ); + } + + return null; + } + + public function onDataRow(DataRow $row, mixed $record): DataRow + { + return array_reduce( + array_keys($this->actions), + fn(DataRow $c, string $v): DataRow => $this->actions[$v]->allowActionForRecord($record) + ? $c + : $c->withDisabledAction($v), + $row + ); + } +} diff --git a/components/ILIAS/BookingManager/src/HttpService.php b/components/ILIAS/BookingManager/src/HttpService.php new file mode 100644 index 000000000000..b379b2bf04db --- /dev/null +++ b/components/ILIAS/BookingManager/src/HttpService.php @@ -0,0 +1,83 @@ +http->request(); + } + + public function getRefId(): int + { + return $this->get('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(fn(array $v): string|int => $v[0]) + ])); + } + + public function resolveRowParameters(string $key): array|string + { + return $this->get($key, $this->refinery->custom()->transformation( + static fn(array|string $value): array|string => $value === 'ALL_OBJECTS' || $value[0] === 'ALL_OBJECTS' + ? 'ALL_OBJECTS' + : array_map('intval', $value) + )) ?? []; + } + + public function get(string $key, Transformation $t): mixed + { + + $wrapper = $this->http->wrapper(); + if ($wrapper->post()->has($key)) { + return $wrapper->post()->retrieve($key, $t); + } + if ($wrapper->query()->has($key)) { + return $wrapper->query()->retrieve($key, $t); + } + return null; + } + + /** + * @param Stream|string|mixed $response + */ + public function sendAsync(mixed $response): void + { + if (is_string($response)) { + $response = Streams::ofString($response); + } elseif (is_resource($response)) { + $response = Streams::ofResource($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/Participant/ParticipantRepository.php b/components/ILIAS/BookingManager/src/Participant/ParticipantRepository.php new file mode 100644 index 000000000000..b4d48236104a --- /dev/null +++ b/components/ILIAS/BookingManager/src/Participant/ParticipantRepository.php @@ -0,0 +1,34 @@ +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/ParticipantTable.php b/components/ILIAS/BookingManager/src/Participant/ParticipantTable.php new file mode 100644 index 000000000000..3388b972626c --- /dev/null +++ b/components/ILIAS/BookingManager/src/Participant/ParticipantTable.php @@ -0,0 +1,224 @@ +, obj_count: int, object_ids: array} + */ + public function __construct( + private readonly UIFactory $ui_factory, + private readonly ilLanguage $lng, + private readonly TableActions $table_actions, + private readonly HttpService $http_service, + private readonly ilUIService $ui_service, + private readonly int $pool_id, + private readonly ServerRequestInterface $request + ) { + } + + /** + * @return array<\ILIAS\UI\Component\Component> + */ + 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->table_actions->getEnabledActions(...$this->acquireParameters($url_builder)) + ) + ->withRequest($this->request) + ->withId(self::ID) + ->withFilter($this->ui_service->filter()->getData($filter)); + + return [ + $filter, + $table + ]; + } + + public function getTotalRowCount( + mixed $additional_viewcontrol_data, + mixed $filter_data, + mixed $additional_parameters + ): ?int { + $data = $this->loadRecords($filter_data); + return count($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 { + $data = $this->loadRecords($filter_data); + + $order_data = $order->get(); + if (!empty($order_data)) { + $order_field = array_keys($order_data)[0]; + $order_direction = $order_data[$order_field]; + + usort($data, function ($a, $b) use ($order_field, $order_direction) { + $a_val = $a[$order_field] ?? ''; + $b_val = $b[$order_field] ?? ''; + + $result = $a_val <=> $b_val; + return $order_direction === Order::ASC ? $result : -$result; + }); + } + + $offset = $range->getStart(); + $length = $range->getLength(); + $data = array_slice($data, $offset, $length); + + foreach ($data as $record) { + $bookable_items = implode(', ', $record['object_title'] ?? []); + + yield $this->table_actions->onDataRow( + $row_builder->buildDataRow( + (string) $record['user_id'], + [ + 'name' => $record['name'] ?? '', + 'bookable_item' => $bookable_items, + ] + ), + $record + ); + } + } + + /** + * @return array + */ + private function getColumns(): array + { + return [ + 'name' => $this->ui_factory->table()->column()->text($this->lng->txt('name')) + ->withIsSortable(true), + 'bookable_item' => $this->ui_factory->table()->column()->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); + } + + private function getFilterComponent(string $action): FilterComponent + { + $field_factory = $this->ui_factory->input()->field(); + + // Bookable Item dropdown + $bookable_items = []; + foreach (\ilBookingObject::getList($this->pool_id) as $item) { + $bookable_items[$item['booking_object_id']] = $item['title']; + } + + $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 + ); + } +} diff --git a/components/ILIAS/BookingManager/src/Participant/ParticipantTableBookForParticipantAction.php b/components/ILIAS/BookingManager/src/Participant/ParticipantTableBookForParticipantAction.php new file mode 100644 index 000000000000..3127326fb4f6 --- /dev/null +++ b/components/ILIAS/BookingManager/src/Participant/ParticipantTableBookForParticipantAction.php @@ -0,0 +1,112 @@ +, obj_count: int, object_ids: array} + * @implements TableAction + */ +class ParticipantTableBookForParticipantAction implements TableAction +{ + public const ACTION_ID = 'book_for_participant'; + + public function __construct( + private readonly UIFactory $ui_factory, + private readonly ilLanguage $lng, + private readonly AccessManager $access, + private readonly ilCtrlInterface $ctrl, + private readonly HttpService $http_service, + private readonly int $ref_id, + private readonly int $pool_id + ) { + } + + public function getActionId(): string + { + return self::ACTION_ID; + } + + 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_service->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); + $total_objects = \ilBookingObject::getNumberOfObjectsForPool($this->pool_id); + return $obj_count < $total_objects; + } + + public function getSelectionErrorMessage(): ?string + { + return null; + } +} diff --git a/components/ILIAS/BookingManager/src/Participant/ParticipantTableDeleteAction.php b/components/ILIAS/BookingManager/src/Participant/ParticipantTableDeleteAction.php new file mode 100644 index 000000000000..b872914d8785 --- /dev/null +++ b/components/ILIAS/BookingManager/src/Participant/ParticipantTableDeleteAction.php @@ -0,0 +1,169 @@ +, obj_count: int, object_ids: array} + * @implements TableAction + */ +class ParticipantTableDeleteAction implements TableAction +{ + use TableActionModalTrait; + + public const ACTION_ID = 'delete'; + + public function __construct( + private readonly UIFactory $ui_factory, + private readonly ilLanguage $lng, + private readonly AccessManager $access, + private readonly ilGlobalTemplateInterface $tpl, + private readonly HttpService $http_service, + 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 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('book_remove_participants'), + $url_builder + ->withParameter($action_token, self::ACTION_ID) + ->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->tpl->setOnScreenMessage( + \ilGlobalTemplateInterface::MESSAGE_TYPE_FAILURE, + $this->lng->txt('no_permission'), + true + ); + return null; + } + + foreach ($selected_records as $record) { + $this->deleteParticipant((int) $record['user_id']); + } + + $this->tpl->setOnScreenMessage( + \ilGlobalTemplateInterface::MESSAGE_TYPE_SUCCESS, + $this->lng->txt('book_participant_removed'), + true + ); + + return null; + } + + public function getSelectionErrorMessage(): ?string + { + return $this->lng->txt('book_table_no_valid_selection'); + } + + protected function resolveRecords(array $selected_ids): array + { + $all_participants = \ilBookingParticipant::getList($this->pool_id); + + if ($selected_ids === []) { + return array_values($all_participants); + } + + $records = []; + foreach ($selected_ids as $user_id) { + $key = $this->pool_id . '_' . $user_id; + if (isset($all_participants[$key])) { + $records[] = $all_participants[$key]; + } + } + + return $records; + } + + private function deleteParticipant(int $user_id): void + { + $this->participant_repository->delete($user_id, $this->pool_id); + } +} diff --git a/components/ILIAS/BookingManager/src/Participant/ParticipantTableEditBookingAction.php b/components/ILIAS/BookingManager/src/Participant/ParticipantTableEditBookingAction.php new file mode 100644 index 000000000000..e17d04fae47b --- /dev/null +++ b/components/ILIAS/BookingManager/src/Participant/ParticipantTableEditBookingAction.php @@ -0,0 +1,136 @@ +, obj_count: int, object_ids: array} + * @implements TableAction + */ +class ParticipantTableEditBookingAction implements TableAction +{ + public const ACTION_ID = 'edit_booking'; + + public function __construct( + private readonly UIFactory $ui_factory, + private readonly ilLanguage $lng, + private readonly AccessManager $access, + private readonly ilCtrlInterface $ctrl, + private readonly HttpService $http_service, + private readonly int $ref_id, + private readonly int $pool_id + ) { + } + + public function getActionId(): string + { + return self::ACTION_ID; + } + + 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 { + $user_id = $this->http_service->resolveRowParameter($row_id_token->getName()); + + $bp = new \ilObjBookingPool($this->pool_id, false); + $obj_count = $this->getObjectCountForUser((int) $user_id); + + if ($obj_count === 1 && $bp->getScheduleType() === \ilObjBookingPool::TYPE_NO_SCHEDULE) { + // Single object, no schedule - direct deassign + $object_ids = $this->getObjectIdsForUser((int) $user_id); + if (!empty($object_ids)) { + $this->ctrl->setParameterByClass('ilbookingreservationsgui', 'bkusr', (string) $user_id); + $this->ctrl->setParameterByClass('ilbookingreservationsgui', 'object_id', (string) $object_ids[0]); + $this->ctrl->setParameterByClass('ilbookingreservationsgui', 'part_view', \ilBookingParticipantGUI::PARTICIPANT_VIEW); + $this->ctrl->redirectByClass('ilbookingreservationsgui', 'rsvConfirmCancelUser'); + } + } else { + // Multiple objects or schedule - show log + $this->ctrl->setParameterByClass('ilbookingreservationsgui', 'user_id', (string) $user_id); + $this->ctrl->redirectByClass('ilbookingreservationsgui', 'log'); + } + + return null; + } + + /** + * @param ParticipantRecord $record + */ + public function allowActionForRecord(mixed $record): bool + { + $obj_count = (int) ($record['obj_count'] ?? 0); + return $obj_count > 0; + } + + public function getSelectionErrorMessage(): ?string + { + return null; + } + + private function getObjectCountForUser(int $user_id): int + { + $data = \ilBookingParticipant::getList($this->pool_id, ['user_id' => $user_id]); + $user_data = $data[$this->pool_id . '_' . $user_id] ?? null; + return (int) ($user_data['obj_count'] ?? 0); + } + + /** + * @return array + */ + private function getObjectIdsForUser(int $user_id): array + { + $data = \ilBookingParticipant::getList($this->pool_id, ['user_id' => $user_id]); + $user_data = $data[$this->pool_id . '_' . $user_id] ?? null; + return $user_data['object_ids'] ?? []; + } +} diff --git a/components/ILIAS/BookingManager/src/Schedule/ScheduleTable.php b/components/ILIAS/BookingManager/src/Schedule/ScheduleTable.php new file mode 100644 index 000000000000..0c0261a603dd --- /dev/null +++ b/components/ILIAS/BookingManager/src/Schedule/ScheduleTable.php @@ -0,0 +1,156 @@ + + */ + public function getComponents(URLBuilder $url_builder): array + { + return [ + $this->ui_factory->table()->data( + $this, + $this->lng->txt('book_schedules'), + $this->getColumns() + ) + ->withActions( + $this->table_actions->getEnabledActions(...$this->acquireParameters($url_builder)) + ) + ->withRequest($this->http_service->getRequest()) + ->withId(self::ID) + ]; + } + + public function getTotalRowCount( + mixed $additional_viewcontrol_data, + mixed $filter_data, + mixed $additional_parameters + ): ?int { + $data = $this->schedule_manager->getScheduleData(); + return count($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 { + $data = $this->schedule_manager->getScheduleData(); + + // Apply sorting if needed + $order_data = $order->get(); + if (!empty($order_data)) { + $order_field = array_keys($order_data)[0]; + $order_direction = $order_data[$order_field]; + + usort($data, function ($a, $b) use ($order_field, $order_direction) { + $a_val = $a[$order_field] ?? ''; + $b_val = $b[$order_field] ?? ''; + + $result = $a_val <=> $b_val; + return $order_direction === Order::ASC ? $result : -$result; + }); + } + + // Apply range (pagination) + $offset = $range->getStart(); + $length = $range->getLength(); + $data = array_slice($data, $offset, $length); + + foreach ($data as $record) { + $is_used = (bool) ($record['is_used'] ?? false); + + yield $this->table_actions->onDataRow( + $row_builder->buildDataRow( + (string) $record['booking_schedule_id'], + [ + 'title' => $record['title'] ?? '', + 'is_used' => $is_used, + ] + ), + $record + ); + } + } + + /** + * @return array + */ + private function getColumns(): array + { + return [ + 'title' => $this->ui_factory->table()->column()->text($this->lng->txt('title')) + ->withIsSortable(true), + 'is_used' => $this->ui_factory->table()->column()->boolean( + $this->lng->txt('book_is_used'), + $this->lng->txt('yes'), + $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 + ); + } +} diff --git a/components/ILIAS/BookingManager/src/Schedule/ScheduleTableDeleteAction.php b/components/ILIAS/BookingManager/src/Schedule/ScheduleTableDeleteAction.php new file mode 100644 index 000000000000..0d6f813fb348 --- /dev/null +++ b/components/ILIAS/BookingManager/src/Schedule/ScheduleTableDeleteAction.php @@ -0,0 +1,145 @@ + + */ +class ScheduleTableDeleteAction implements TableAction +{ + use TableActionModalTrait; + + public const string ACTION_ID = 'delete'; + + public function __construct( + private readonly UIFactory $ui_factory, + private readonly ilLanguage $lng, + private readonly AccessManager $access, + private readonly ilGlobalTemplateInterface $tpl, + private readonly HttpService $http_service, + private readonly ScheduleManager $schedule_manager, + private readonly int $ref_id, + ) { + } + + public function getActionId(): string + { + return self::ACTION_ID; + } + + 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('delete'), + $url_builder + ->withParameter($action_token, self::ACTION_ID) + ->withParameter($action_type_token, self::SHOW_MODAL_ACTION), + $row_id_token + )->withAsync(); + } + + /** + * @param ScheduleRecord $record + */ + public function allowActionForRecord(mixed $record): bool + { + return !($record['is_used'] ?? 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) => $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) { + $schedule = new \ilBookingSchedule($record['booking_schedule_id']); + $schedule->delete(); + } + + $this->showSuccessMessage($this->lng->txt('book_schedule_deleted')); + return null; + } + + public function getSelectionErrorMessage(): ?string + { + return $this->lng->txt('no_valid_selection'); + } + + protected function resolveRecords(array $selected_ids): array + { + $schedules = $this->schedule_manager->getScheduleData(); + + if ($selected_ids === []) { + return $schedules; + } + + return array_filter(array_map(fn($id) => $schedules[$id] ?? null, $selected_ids)); + } +} diff --git a/components/ILIAS/BookingManager/src/Schedule/ScheduleTableEditAction.php b/components/ILIAS/BookingManager/src/Schedule/ScheduleTableEditAction.php new file mode 100644 index 000000000000..2ab298410419 --- /dev/null +++ b/components/ILIAS/BookingManager/src/Schedule/ScheduleTableEditAction.php @@ -0,0 +1,107 @@ + + */ +class ScheduleTableEditAction implements TableAction +{ + public const string ACTION_ID = '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_service, + private readonly int $ref_id + ) { + } + + public function getActionId(): string + { + return self::ACTION_ID; + } + + 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('edit'), + $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 { + $record_id = $this->http_service->resolveRowParameter($row_id_token->getName()); + + $this->ctrl->setParameterByClass( + \ilBookingScheduleGUI::class, + 'schedule_id', + $record_id + ); + $this->ctrl->redirectByClass( + \ilBookingScheduleGUI::class, + 'edit' + ); + } + + /** + * @param ScheduleRecord $record + */ + public function allowActionForRecord(mixed $record): bool + { + return true; + } + + public function getSelectionErrorMessage(): ?string + { + return null; + } +} 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/lang/ilias_de.lang b/lang/ilias_de.lang index 7d608730932d..55cc2d3c52c1 100644 --- a/lang/ilias_de.lang +++ b/lang/ilias_de.lang @@ -2552,9 +2552,14 @@ book#:#book_booking#:#Buchung book#:#book_booking_information#:#Information book#:#book_booking_objects#:#Angebote book#:#book_booking_reminders#:#Anstehende Buchungen +book#:#book_bulk_all_unavailable#:#Keine der ausgewählten Einträge kann noch gebucht werden. +book#:#book_bulk_book#:#Ausgewählte buchen book#:#book_bulk_confirmation#:#Bitte überprüfen Sie die angegebenen Werte. book#:#book_bulk_creation#:#Massenerstellung book#:#book_bulk_data#:#Daten zu Angeboten +book#:#book_bulk_message_byline#:#Sie können hier eine Mitteilung für die Verantwortlichen des Buchungspools hinterlassen. +book#:#book_bulk_omitted_unavailable#:#%s ausgewählte(r) Eintrag/Einträge ist/sind nicht mehr verfügbar und wurde/wurden nicht übernommen. Sie können unten die verbleibenden Einträge buchen. +book#:#book_bulk_result#:#Gebucht: %s, übersprungen: %s. book#:#book_cal_entry#:#Buchung: book#:#book_confirm_booking#:#Buchung vornehmen book#:#book_confirm_booking_for_users#:#Sind Sie sicher, dass Sie dieses Angebot für die folgenden Personen buchen wollen? @@ -2564,6 +2569,9 @@ book#:#book_confirm_booking_schedule_number_of_objects_info#:#Bitte wählen Sie 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_delete_participant#:#Sind Sie sicher, dass Sie den folgenden Teilnehmer löschen möchten? Dies wird auch alle zugehörigen Buchungen löschen. +book#:#book_confirm_delete_participants#:#Sind Sie sicher, dass Sie die folgenden Teilnehmer löschen möchten? Dies wird auch alle zugehörigen Buchungen löschen. +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_deadline#:#Deadline @@ -2577,7 +2585,11 @@ 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 @@ -2616,8 +2628,11 @@ 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_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_deleted#:#Der ausgewählte Teilnehmer und alle zugehörigen Buchungen wurden erfolgreich gelöscht. +book#:#book_participant_removed#:#Der ausgewählte Teilnehmer wurde aus dem Buchungspool entfernt. book#:#book_period#:#Dauer book#:#book_pool_added#:#Ein Buchungspool wurde angelegt. book#:#book_pool_selection#:#Auswahl Buchungspool @@ -2643,6 +2658,8 @@ 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_participant#:#Teilnehmer entfernen +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 @@ -2678,6 +2695,9 @@ 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_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. diff --git a/lang/ilias_en.lang b/lang/ilias_en.lang index 1fcbe16b7213..7c9bf1b4eff6 100644 --- a/lang/ilias_en.lang +++ b/lang/ilias_en.lang @@ -2553,9 +2553,14 @@ book#:#book_booking#:#Booking book#:#book_booking_information#:#Information book#:#book_booking_objects#:#Bookable Items book#:#book_booking_reminders#:#Upcoming Bookings +book#:#book_bulk_all_unavailable#:#None of the selected items can be booked anymore. +book#:#book_bulk_book#:#Book selected book#:#book_bulk_confirmation#:#Please check that all values are listed correctly. book#:#book_bulk_creation#:#Bulk Creation book#:#book_bulk_data#:#Item Data +book#:#book_bulk_message_byline#:#You may enter a message for the booking organiser here. +book#:#book_bulk_omitted_unavailable#:#%s selected item(s) are no longer available and were not included. You can book the items listed below. +book#:#book_bulk_result#:#Booked: %s, skipped: %s. book#:#book_cal_entry#:#Booking: book#:#book_confirm_booking#:#Confirm Booking book#:#book_confirm_booking_for_users#:#Are you sure you want to book this item for the following user(s)? @@ -2565,6 +2570,9 @@ book#:#book_confirm_booking_schedule_number_of_objects_info#:#Please enter the n 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_delete_participant#:#Are you sure you want to delete the following participant? This will also delete all related bookings. +book#:#book_confirm_delete_participants#:#Are you sure you want to delete the following participants? 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_deadline#:#Deadline @@ -2578,7 +2586,11 @@ 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 @@ -2617,8 +2629,11 @@ 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_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_deleted#:#The selected participant and all related bookings have been deleted successfully. +book#:#book_participant_removed#:#The selected participant has been removed from the booking pool. book#:#book_period#:#Period book#:#book_pool_added#:#Booking pool successfully created. book#:#book_pool_selection#:#Booking Pool Selection @@ -2644,6 +2659,8 @@ 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_participant#:#Remove Participant +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 @@ -2679,6 +2696,9 @@ 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_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.