diff --git a/config/routes.php b/config/routes.php index a28e697d..771509dc 100644 --- a/config/routes.php +++ b/config/routes.php @@ -157,6 +157,17 @@ $route->addGroup( } ); + // Rooms + $route->addGroup( + '/rooms', + function (RouteCollector $route): void { + $route->get('', 'Admin\\RoomsController@index'); + $route->post('', 'Admin\\RoomsController@delete'); + $route->get('/edit[/{room_id:\d+}]', 'Admin\\RoomsController@edit'); + $route->post('/edit[/{room_id:\d+}]', 'Admin\\RoomsController@save'); + } + ); + // User $route->addGroup( '/user/{user_id:\d+}', diff --git a/includes/controller/angeltypes_controller.php b/includes/controller/angeltypes_controller.php index 0c8f15fb..bdc00572 100644 --- a/includes/controller/angeltypes_controller.php +++ b/includes/controller/angeltypes_controller.php @@ -2,6 +2,7 @@ use Engelsystem\Helpers\Carbon; use Engelsystem\Models\AngelType; +use Engelsystem\Models\Room; use Engelsystem\Models\UserAngelType; use Engelsystem\ShiftsFilter; use Engelsystem\ShiftsFilterRenderer; @@ -239,9 +240,13 @@ function angeltype_controller_shiftsFilterDays(AngelType $angeltype) function angeltype_controller_shiftsFilter(AngelType $angeltype, $days) { $request = request(); + $roomIds = Room::query() + ->select('id') + ->pluck('id') + ->toArray(); $shiftsFilter = new ShiftsFilter( auth()->can('user_shifts_admin'), - Room_ids(), + $roomIds, [$angeltype->id] ); $selected_day = date('Y-m-d'); diff --git a/includes/controller/public_dashboard_controller.php b/includes/controller/public_dashboard_controller.php index 47476262..d4f73434 100644 --- a/includes/controller/public_dashboard_controller.php +++ b/includes/controller/public_dashboard_controller.php @@ -2,6 +2,7 @@ use Engelsystem\Models\AngelType; use Engelsystem\Models\News; +use Engelsystem\Models\Room; use Engelsystem\Models\Shifts\Shift; use Engelsystem\ShiftsFilter; @@ -24,7 +25,7 @@ function public_dashboard_controller() } $angelTypes = collect(unrestricted_angeltypes()); - $rooms = $requestRooms ?: Rooms()->pluck('id')->toArray(); + $rooms = $requestRooms ?: Room::orderBy('name')->get()->pluck('id')->toArray(); $angelTypes = $requestAngelTypes ?: $angelTypes->pluck('id')->toArray(); $filterValues = [ 'userShiftsAdmin' => false, diff --git a/includes/controller/rooms_controller.php b/includes/controller/rooms_controller.php index 1a253c30..2136565a 100644 --- a/includes/controller/rooms_controller.php +++ b/includes/controller/rooms_controller.php @@ -73,8 +73,8 @@ function rooms_controller(): array return match ($action) { 'view' => room_controller(), - 'list' => throw_redirect(page_link_to('admin_rooms')), - default => throw_redirect(page_link_to('admin_rooms')), + 'list' => throw_redirect(page_link_to('admin/rooms')), + default => throw_redirect(page_link_to('admin/rooms')), }; } diff --git a/includes/controller/shifts_controller.php b/includes/controller/shifts_controller.php index b865673b..2899f39e 100644 --- a/includes/controller/shifts_controller.php +++ b/includes/controller/shifts_controller.php @@ -67,7 +67,7 @@ function shift_edit_controller() } $rooms = []; - foreach (Rooms() as $room) { + foreach (Room::orderBy('name')->get() as $room) { $rooms[$room->id] = $room->name; } $angeltypes = AngelType::all()->pluck('name', 'id')->toArray(); diff --git a/includes/includes.php b/includes/includes.php index 5317b4ca..e16c4170 100644 --- a/includes/includes.php +++ b/includes/includes.php @@ -12,7 +12,6 @@ $includeFiles = [ __DIR__ . '/../includes/sys_template.php', __DIR__ . '/../includes/model/NeededAngelTypes_model.php', - __DIR__ . '/../includes/model/Room_model.php', __DIR__ . '/../includes/model/ShiftEntry_model.php', __DIR__ . '/../includes/model/Shifts_model.php', __DIR__ . '/../includes/model/ShiftsFilter.php', @@ -61,7 +60,6 @@ $includeFiles = [ __DIR__ . '/../includes/pages/admin_arrive.php', __DIR__ . '/../includes/pages/admin_free.php', __DIR__ . '/../includes/pages/admin_groups.php', - __DIR__ . '/../includes/pages/admin_rooms.php', __DIR__ . '/../includes/pages/admin_shifts.php', __DIR__ . '/../includes/pages/admin_user.php', __DIR__ . '/../includes/pages/guest_login.php', diff --git a/includes/model/Room_model.php b/includes/model/Room_model.php deleted file mode 100644 index 28c8827b..00000000 --- a/includes/model/Room_model.php +++ /dev/null @@ -1,122 +0,0 @@ -where('name', $name) - ->where('id', '!=', $room_id) - ->count(); - if ($roomCount) { - $valid = false; - } - - return new ValidationResult($valid, $name); -} - -/** - * returns a list of rooms. - * - * @return Room[]|Collection - */ -function Rooms() -{ - return Room::orderBy('name')->get(); -} - -/** - * Returns Room id array - * - * @return int[] - */ -function Room_ids() -{ - return Room::query() - ->select('id') - ->pluck('id') - ->toArray(); -} - -/** - * Delete a room - * - * @param Room $room - */ -function Room_delete(Room $room) -{ - $room->delete(); - engelsystem_log('Room deleted: ' . $room->name); -} - -/** - * Create a new room - * - * @param string $name Name of the room - * @param string|null $map_url URL to a map tha can be displayed in an iframe - * @param string|null $description - * - * @return null|int - */ -function Room_create(string $name, string $map_url = null, string $description = null, string $dect = null) -{ - $room = new Room(); - $room->name = $name; - $room->description = $description; - $room->map_url = $map_url; - $room->dect = $dect; - $room->save(); - - engelsystem_log( - 'Room created: ' . $name - . ', dect: ' . $dect - . ', map_url: ' . $map_url - . ', description: ' . $description - ); - - return $room->id; -} - -/** - * Update a room - * - * @param int $room_id The rooms id - * @param string $name Name of the room - * @param string|null $map_url URL to a map tha can be displayed in an iframe - * @param string|null $description Markdown description - */ -function Room_update( - int $room_id, - string $name, - string $map_url = null, - string $description = null, - string $dect = null -) { - $room = Room::find($room_id); - $room->name = $name; - $room->description = $description ?: null; - $room->map_url = $map_url ?: null; - $room->dect = $dect ?: null; - $room->save(); - - engelsystem_log( - 'Room updated: ' . $name . - ', dect: ' . $dect . - ', map_url: ' . $map_url . - ', description: ' . $description - ); -} diff --git a/includes/pages/admin_rooms.php b/includes/pages/admin_rooms.php deleted file mode 100644 index 3113877b..00000000 --- a/includes/pages/admin_rooms.php +++ /dev/null @@ -1,238 +0,0 @@ - Room_name_render($room), - 'dect' => icon_bool($room->dect), - 'map_url' => icon_bool($room->map_url), - 'actions' => table_buttons([ - button( - page_link_to('admin_rooms', ['show' => 'edit', 'id' => $room->id]), - icon('pencil') . __('edit'), - 'btn-sm' - ), - button( - page_link_to('admin_rooms', ['show' => 'delete', 'id' => $room->id]), - icon('trash') . __('delete'), - 'btn-sm' - ), - ]), - ]; - } - - $room = null; - if ($request->has('show')) { - $msg = ''; - $name = ''; - $map_url = null; - $description = null; - $dect = null; - $room_id = 0; - - $angeltypes_source = AngelType::all(); - $angeltypes = []; - $angeltypes_count = []; - foreach ($angeltypes_source as $angeltype) { - $angeltypes[$angeltype->id] = $angeltype->name; - $angeltypes_count[$angeltype->id] = 0; - } - - if (test_request_int('id')) { - $room = Room::find($request->input('id')); - if (!$room) { - throw_redirect(page_link_to('admin_rooms')); - } - - $room_id = $room->id; - $name = $room->name; - $map_url = $room->map_url; - $description = $room->description; - $dect = $room->dect; - - $angeltypes_count = NeededAngelType::whereRoomId($room_id) - ->pluck('count', 'angel_type_id') - ->toArray() + $angeltypes_count; - } - if ($request->input('show') == 'edit') { - if ($request->hasPostData('submit')) { - $valid = true; - - if ($request->has('name') && strlen(strip_request_tags('name')) > 0) { - $result = Room_validate_name(strip_request_tags('name'), $room_id); - if (!$result->isValid()) { - $valid = false; - $msg .= error(__('This name is already in use.'), true); - } else { - $name = $result->getValue(); - } - } else { - $valid = false; - $msg .= error(__('Please enter a name.'), true); - } - - if ($request->has('map_url')) { - $map_url = strip_request_item('map_url'); - } - - if ($request->has('description')) { - $description = strip_request_item_nl('description'); - } - if ($request->has('dect')) { - $dect = strip_request_item_nl('dect'); - } - - foreach ($angeltypes as $angeltype_id => $angeltype) { - $angeltypes_count[$angeltype_id] = 0; - $queryKey = 'angeltype_count_' . $angeltype_id; - if (!$request->has($queryKey)) { - continue; - } - - if (preg_match('/^\d{1,4}$/', $request->input($queryKey))) { - $angeltypes_count[$angeltype_id] = $request->input($queryKey); - } else { - $valid = false; - $msg .= error(sprintf( - __('Please enter needed angels for type %s.'), - $angeltype - ), true); - } - } - - if ($valid) { - if (empty($room_id)) { - $room_id = Room_create($name, $map_url, $description, $dect); - } else { - Room_update($room_id, $name, $map_url, $description, $dect); - } - - NeededAngelType::whereRoomId($room_id)->delete(); - $needed_angeltype_info = []; - foreach ($angeltypes_count as $angeltype_id => $angeltype_count) { - $angeltype = AngelType::find($angeltype_id); - if (!empty($angeltype) && $angeltype_count > 0) { - $neededAngelType = new NeededAngelType(); - $neededAngelType->room_id = $room_id; - $neededAngelType->angelType()->associate($angeltype); - $neededAngelType->count = $angeltype_count; - $neededAngelType->save(); - - $needed_angeltype_info[] = $angeltype->name . ': ' . $angeltype_count; - } - } - - engelsystem_log( - 'Set needed angeltypes of room ' . $name - . ' to: ' . join(', ', $needed_angeltype_info) - ); - success(__('Room saved.')); - throw_redirect(page_link_to('admin_rooms')); - } - } - $angeltypes_count_form = []; - foreach ($angeltypes as $angeltype_id => $angeltypeName) { - $angeltypes_count_form[] = div('col-lg-4 col-md-6 col-xs-6', [ - form_spinner('angeltype_count_' . $angeltype_id, $angeltypeName, $angeltypes_count[$angeltype_id]), - ]); - } - - return page_with_title(admin_rooms_title(), [ - buttons([ - button(page_link_to('admin_rooms'), __('back'), 'back'), - ]), - $msg, - form([ - div('row', [ - div('col-md-6', [ - form_text('name', __('Name'), $name, false, 35), - form_text('dect', __('DECT'), $dect), - form_text('map_url', __('Map URL'), $map_url), - form_info('', __('The map url is used to display an iframe on the room page.')), - form_textarea('description', __('Description'), $description), - form_info('', __('Please use markdown for the description.')), - ]), - div('col-md-6', [ - div('row', [ - div('col-md-12', [ - form_info(__('Needed angels:')), - ]), - join($angeltypes_count_form), - ]), - ]), - ]), - form_submit('submit', __('Save')), - ]), - ], true); - } elseif ($request->input('show') == 'delete') { - if ($request->hasPostData('ack')) { - $room = Room::find($room_id); - $shifts = $room->shifts; - foreach ($shifts as $shift) { - $shift = Shift($shift); - foreach ($shift->shiftEntries as $entry) { - event('shift.entry.deleting', [ - 'user' => User::find($entry['user_id']), - 'start' => $shift->start, - 'end' => $shift->end, - 'name' => $shift->shiftType->name, - 'title' => $shift->title, - 'type' => $entry->angelType->name, - 'room' => $room, - 'freeloaded' => (bool) $entry['freeloaded'], - ]); - } - } - - Room_delete($room); - - success(sprintf(__('Room %s deleted.'), $name)); - throw_redirect(page_link_to('admin_rooms')); - } - - return page_with_title(admin_rooms_title(), [ - buttons([ - button(page_link_to('admin_rooms'), __('back'), 'back'), - ]), - sprintf(__('Do you want to delete room %s?'), $name), - form([ - form_submit('ack', __('Delete'), 'delete btn-danger'), - ], page_link_to('admin_rooms', ['show' => 'delete', 'id' => $room_id])), - ], true); - } - } - - return page_with_title(admin_rooms_title(), [ - buttons([ - button(page_link_to('admin_rooms', ['show' => 'edit']), __('add')), - ]), - msg(), - table([ - 'name' => __('Name'), - 'dect' => __('DECT'), - 'map_url' => __('Map'), - 'actions' => '', - ], $rooms), - ], true); -} diff --git a/includes/pages/admin_shifts.php b/includes/pages/admin_shifts.php index 339d76ce..7fa8299f 100644 --- a/includes/pages/admin_shifts.php +++ b/includes/pages/admin_shifts.php @@ -43,11 +43,8 @@ function admin_shifts() $shift_over_midnight = true; // Locations laden - $rooms = Rooms(); - $room_array = []; - foreach ($rooms as $room) { - $room_array[$room->id] = $room->name; - } + $rooms = Room::orderBy('name')->get(); + $room_array = $rooms->pluck('name', 'id')->toArray(); // Load angeltypes /** @var AngelType[] $types */ diff --git a/includes/pages/user_shifts.php b/includes/pages/user_shifts.php index 127f0270..15b5cd22 100644 --- a/includes/pages/user_shifts.php +++ b/includes/pages/user_shifts.php @@ -115,7 +115,7 @@ function update_ShiftsFilter(ShiftsFilter $shiftsFilter, $user_shifts_admin, $da */ function load_rooms() { - $rooms = Rooms(); + $rooms = Room::orderBy('name')->get(); if ($rooms->isEmpty()) { error(__('The administration has not configured any rooms yet.')); throw_redirect(page_link_to('/')); diff --git a/includes/sys_menu.php b/includes/sys_menu.php index 7dcb3c91..806694cc 100644 --- a/includes/sys_menu.php +++ b/includes/sys_menu.php @@ -1,6 +1,7 @@ ['Answer questions', 'question.edit'], 'shifttypes' => 'Shifttypes', 'admin_shifts' => 'Create shifts', - 'admin_rooms' => 'Rooms', + 'admin/rooms' => ['room.rooms', 'admin_rooms'], 'admin_groups' => 'Grouprights', 'admin/schedule' => ['schedule.import', 'schedule.import'], 'admin/logs' => ['log.log', 'admin_log'], @@ -152,10 +153,10 @@ function make_room_navigation($menu) } // Get a list of all rooms - $rooms = Rooms(); + $rooms = Room::orderBy('name')->get(); $room_menu = []; if (auth()->can('admin_rooms')) { - $room_menu[] = toolbar_dropdown_item(page_link_to('admin_rooms'), __('Manage rooms'), false, 'list'); + $room_menu[] = toolbar_dropdown_item(page_link_to('admin/rooms'), __('Manage rooms'), false, 'list'); } if (count($room_menu) > 0) { $room_menu[] = toolbar_dropdown_item_divider(); diff --git a/includes/view/Rooms_view.php b/includes/view/Rooms_view.php index c7cefde7..7832a854 100644 --- a/includes/view/Rooms_view.php +++ b/includes/view/Rooms_view.php @@ -61,15 +61,10 @@ function Room_view(Room $room, ShiftsFilterRenderer $shiftsFilterRenderer, Shift $assignNotice, auth()->can('admin_rooms') ? buttons([ button( - page_link_to('admin_rooms', ['show' => 'edit', 'id' => $room->id]), + page_link_to('admin/rooms/edit/' . $room->id), icon('pencil') . __('edit'), 'btn' ), - button( - page_link_to('admin_rooms', ['show' => 'delete', 'id' => $room->id]), - icon('trash') . __('delete'), - 'btn' - ), ]) : '', $dect, $description, diff --git a/resources/assets/js/forms.js b/resources/assets/js/forms.js index 3a7e46d7..e6e19196 100644 --- a/resources/assets/js/forms.js +++ b/resources/assets/js/forms.js @@ -254,6 +254,13 @@ ready(() => { document.querySelectorAll('[data-bs-toggle="popover"]').forEach((element) => new bootstrap.Popover(element)); }); +/** + * Init Bootstrap Tooltips + */ +ready(() => { + document.querySelectorAll('[data-bs-toggle="tooltip"]').forEach((element) => new bootstrap.Tooltip(element)); +}); + /** * Show oauth buttons on welcome title click */ diff --git a/resources/assets/themes/base.scss b/resources/assets/themes/base.scss index 2ab2ef87..6893e425 100644 --- a/resources/assets/themes/base.scss +++ b/resources/assets/themes/base.scss @@ -46,6 +46,7 @@ $form-label-font-weight: $font-weight-bold; @import '~bootstrap/scss/list-group'; @import '~bootstrap/scss/close'; @import '~bootstrap/scss/popover'; +@import '~bootstrap/scss/tooltip'; @import '~bootstrap/scss/helpers'; diff --git a/resources/lang/de_DE/additional.po b/resources/lang/de_DE/additional.po index 2ea82b44..aad375fb 100644 --- a/resources/lang/de_DE/additional.po +++ b/resources/lang/de_DE/additional.po @@ -209,3 +209,12 @@ msgstr "Arbeitseinsatz erfolgreich bearbeitet." msgid "worklog.delete.success" msgstr "Arbeitseinsatz erfolgreich gelöscht." + +msgid "room.edit.success" +msgstr "Raum erfolgreich bearbeitet." + +msgid "room.delete.success" +msgstr "Raum erfolgreich gelöscht." + +msgid "validation.name.exists" +msgstr "Der Name wird bereits verwendet." diff --git a/resources/lang/de_DE/default.po b/resources/lang/de_DE/default.po index 86466259..2499bf97 100644 --- a/resources/lang/de_DE/default.po +++ b/resources/lang/de_DE/default.po @@ -2812,6 +2812,12 @@ msgstr "Aktualisiert" msgid "form.cancel" msgstr "Abbrechen" +msgid "form.markdown" +msgstr "Du kannst hier Markdown verwenden" + +msgid "form.required" +msgstr "Pflichtfeld" + msgid "schedule.import" msgstr "Programm importieren" @@ -3189,3 +3195,30 @@ msgstr "E-Mail" msgid "registration.register" msgstr "Registrieren" + +msgid "room.rooms" +msgstr "Räume" + +msgid "room.name" +msgstr "Name" + +msgid "room.dect" +msgstr "DECT" + +msgid "room.map_url" +msgstr "Karte" + +msgid "room.description" +msgstr "Beschreibung" + +msgid "room.required_angels" +msgstr "Benötigte Engel" + +msgid "room.map_url.info" +msgstr "Die Karte wird auf der Raum-Seite als iframe eingebettet." + +msgid "room.create.title" +msgstr "Raum erstellen" + +msgid "room.edit.title" +msgstr "Raum bearbeiten" diff --git a/resources/lang/en_US/additional.po b/resources/lang/en_US/additional.po index 51a6f9c0..57f5f2dc 100644 --- a/resources/lang/en_US/additional.po +++ b/resources/lang/en_US/additional.po @@ -208,3 +208,12 @@ msgstr "Work log successfully updated." msgid "worklog.delete.success" msgstr "Work log successfully deleted." + +msgid "room.edit.success" +msgstr "Room edited successfully." + +msgid "room.delete.success" +msgstr "Room successfully deleted." + +msgid "validation.name.exists" +msgstr "The name is already used." diff --git a/resources/lang/en_US/default.po b/resources/lang/en_US/default.po index aaa353e5..5ab33542 100644 --- a/resources/lang/en_US/default.po +++ b/resources/lang/en_US/default.po @@ -79,6 +79,12 @@ msgstr "Updated" msgid "form.cancel" msgstr "Cancel" +msgid "form.required" +msgstr "Required" + +msgid "form.markdown" +msgstr "You can use Markdown here" + msgid "schedule.import" msgstr "Import schedule" @@ -452,3 +458,30 @@ msgstr "E-Mail" msgid "registration.register" msgstr "Register" + +msgid "room.rooms" +msgstr "Rooms" + +msgid "room.name" +msgstr "Name" + +msgid "room.dect" +msgstr "DECT" + +msgid "room.map_url" +msgstr "Map" + +msgid "room.description" +msgstr "Description" + +msgid "room.required_angels" +msgstr "Required angels" + +msgid "room.map_url.info" +msgstr "The map will be embedded on the room page as an iframe." + +msgid "room.create.title" +msgstr "Create room" + +msgid "room.edit.title" +msgstr "Edit room" diff --git a/resources/views/admin/rooms/edit.twig b/resources/views/admin/rooms/edit.twig new file mode 100644 index 00000000..43df6d23 --- /dev/null +++ b/resources/views/admin/rooms/edit.twig @@ -0,0 +1,66 @@ +{% extends 'admin/rooms/index.twig' %} +{% import 'macros/base.twig' as m %} +{% import 'macros/form.twig' as f %} + +{% block title %}{{ room ? __('room.edit.title') : __('room.create.title') }}{% endblock %} + +{% block row_content %} +
+{% endblock %} diff --git a/resources/views/admin/rooms/index.twig b/resources/views/admin/rooms/index.twig new file mode 100644 index 00000000..ac439c08 --- /dev/null +++ b/resources/views/admin/rooms/index.twig @@ -0,0 +1,71 @@ +{% extends 'layouts/app.twig' %} +{% import 'macros/base.twig' as m %} +{% import 'macros/form.twig' as f %} + +{% block title %}{{ __('room.rooms') }}{% endblock %} + +{% block content %} +{{ __('room.name') }} | +{{ __('room.dect') }} | +{{ __('room.map_url') }} | ++ |
---|---|---|---|
+ {{ m.icon('pin-map-fill') }} + + {{ room.name }} + + | + +{{ m.iconBool(room.dect) }} | + +{{ m.iconBool(room.map_url) }} | + +
+
+
+ {{ m.button(m.icon('pencil'), url('admin/rooms/edit/' ~ room.id), null, 'sm', __('form.edit')) }}
+
+
+
+
+ |
+
icon(icon_name)
{{ m.icon('star') }}
iconBool(true)
{{ m.iconBool(true) }} {{ m.iconBool(false) }}
+alert(message, type)
{{ m.alert('Test content', 'info') }}
diff --git a/src/Controllers/Admin/RoomsController.php b/src/Controllers/Admin/RoomsController.php new file mode 100644 index 00000000..85e6b281 --- /dev/null +++ b/src/Controllers/Admin/RoomsController.php @@ -0,0 +1,174 @@ + */ + protected array $permissions = [ + 'admin_rooms', + ]; + + public function __construct( + protected LoggerInterface $log, + protected Room $room, + protected Redirector $redirect, + protected Response $response + ) { + } + + public function index(): Response + { + $rooms = $this->room + ->orderBy('name') + ->get(); + + return $this->response->withView( + 'admin/rooms/index', + ['rooms' => $rooms, 'is_index' => true] + ); + } + + public function edit(Request $request): Response + { + $roomId = (int) $request->getAttribute('room_id'); + + $room = $this->room->find($roomId); + + return $this->showEdit($room); + } + + public function save(Request $request): Response + { + $roomId = (int) $request->getAttribute('room_id'); + + /** @var Room $room */ + $room = $this->room->findOrNew($roomId); + /** @var Collection|AngelType[] $angelTypes */ + $angelTypes = AngelType::all(); + $validation = []; + foreach ($angelTypes as $angelType) { + $validation['angel_type_' . $angelType->id] = 'optional|int'; + } + + if ($request->request->has('delete')) { + return $this->delete($request); + } + + $data = $this->validate( + $request, + [ + 'name' => 'required', + 'description' => 'required|optional', + 'dect' => 'required|optional', + 'map_url' => 'optional|url', + ] + $validation + ); + + if (Room::whereName($data['name'])->where('id', '!=', $room->id)->exists()) { + throw new ValidationException((new Validator())->addErrors(['name' => ['validation.name.exists']])); + } + + $room->name = $data['name']; + $room->description = $data['description']; + $room->dect = $data['dect']; + $room->map_url = $data['map_url']; + + $room->save(); + $room->neededAngelTypes()->getQuery()->delete(); + $angelsInfo = ''; + + foreach ($angelTypes as $angelType) { + $count = $data['angel_type_' . $angelType->id]; + if (!$count) { + continue; + } + + $neededAngelType = new NeededAngelType(); + + $neededAngelType->room()->associate($room); + $neededAngelType->angelType()->associate($angelType); + + $neededAngelType->count = $data['angel_type_' . $angelType->id]; + + $neededAngelType->save(); + + $angelsInfo .= sprintf(', %s: %s', $angelType->name, $count); + } + + $this->log->info( + 'Updated room "{name}": {description} {dect} {map_url} {angels}', + [ + 'name' => $room->name, + 'description' => $room->description, + 'dect' => $room->dect, + 'map_url' => $room->map_url, + 'angels' => $angelsInfo, + ] + ); + + $this->addNotification('room.edit.success'); + + return $this->redirect->to('/admin/rooms'); + } + + public function delete(Request $request): Response + { + $data = $this->validate($request, [ + 'id' => 'required|int', + 'delete' => 'checked', + ]); + + $room = $this->room->findOrFail($data['id']); + + $shifts = $room->shifts; + foreach ($shifts as $shift) { + foreach ($shift->shiftEntries as $entry) { + event('shift.entry.deleting', [ + 'user' => $entry->user, + 'start' => $shift->start, + 'end' => $shift->end, + 'name' => $shift->shiftType->name, + 'title' => $shift->title, + 'type' => $entry->angelType->name, + 'room' => $room, + 'freeloaded' => $entry->freeloaded, + ]); + } + } + $room->delete(); + + $this->log->info('Deleted room {room}', ['room' => $room->name]); + $this->addNotification('room.delete.success'); + + return $this->redirect->to('/admin/rooms'); + } + + protected function showEdit(?Room $room): Response + { + $angeltypes = AngelType::all() + ->sortBy('name'); + + return $this->response->withView( + 'admin/rooms/edit', + ['room' => $room, 'angel_types' => $angeltypes, 'needed_angel_types' => $room?->neededAngelTypes] + ); + } +} diff --git a/src/Http/Response.php b/src/Http/Response.php index 60200098..0d275f04 100644 --- a/src/Http/Response.php +++ b/src/Http/Response.php @@ -167,7 +167,9 @@ class Response extends SymfonyResponse implements ResponseInterface throw new InvalidArgumentException('Session not defined'); } - $this->session->set('form-data', $input); + foreach ($input as $name => $value) { + $this->session->set('form-data-' . $name, $value); + } return $this; } diff --git a/src/Http/Validation/Validator.php b/src/Http/Validation/Validator.php index 1dd6459a..10655aa9 100644 --- a/src/Http/Validation/Validator.php +++ b/src/Http/Validation/Validator.php @@ -103,4 +103,11 @@ class Validator { return $this->errors; } + + public function addErrors(array $errors): self + { + $this->errors = array_merge($this->errors, $errors); + + return $this; + } } diff --git a/src/Middleware/LegacyMiddleware.php b/src/Middleware/LegacyMiddleware.php index 30be467b..34b861fe 100644 --- a/src/Middleware/LegacyMiddleware.php +++ b/src/Middleware/LegacyMiddleware.php @@ -128,10 +128,6 @@ class LegacyMiddleware implements MiddlewareInterface $title = admin_free_title(); $content = admin_free(); return [$title, $content]; - case 'admin_rooms': - $title = admin_rooms_title(); - $content = admin_rooms(); - return [$title, $content]; case 'admin_groups': $title = admin_groups_title(); $content = admin_groups(); diff --git a/src/Renderer/Twig/Extensions/Session.php b/src/Renderer/Twig/Extensions/Session.php index be74a845..e4551e87 100644 --- a/src/Renderer/Twig/Extensions/Session.php +++ b/src/Renderer/Twig/Extensions/Session.php @@ -22,6 +22,18 @@ class Session extends TwigExtension return [ new TwigFunction('session_get', [$this->session, 'get']), new TwigFunction('session_set', [$this->session, 'set']), + new TwigFunction('session_pop', [$this, 'sessionPop']), ]; } + + /** + * Returns the requested attribute and removes it from the session + */ + public function sessionPop(string $name, mixed $default = null): mixed + { + $value = $this->session->get($name, $default); + $this->session->remove($name); + + return $value; + } } diff --git a/tests/Unit/Controllers/Admin/RoomsControllerTest.php b/tests/Unit/Controllers/Admin/RoomsControllerTest.php new file mode 100644 index 00000000..4e885331 --- /dev/null +++ b/tests/Unit/Controllers/Admin/RoomsControllerTest.php @@ -0,0 +1,220 @@ +app->make(RoomsController::class); + Room::factory(5)->create(); + + $this->response->expects($this->once()) + ->method('withView') + ->willReturnCallback(function (string $view, array $data) { + $this->assertEquals('admin/rooms/index', $view); + $this->assertTrue($data['is_index'] ?? false); + $this->assertCount(5, $data['rooms'] ?? []); + return $this->response; + }); + + $controller->index(); + } + + /** + * @covers \Engelsystem\Controllers\Admin\RoomsController::edit + * @covers \Engelsystem\Controllers\Admin\RoomsController::showEdit + */ + public function testEdit(): void + { + /** @var RoomsController $controller */ + $controller = $this->app->make(RoomsController::class); + /** @var Room $room */ + $room = Room::factory()->create(); + $angelTypes = AngelType::factory(3)->create(); + (new NeededAngelType(['room_id' => $room->id, 'angel_type_id' => $angelTypes[0]->id, 'count' => 3]))->save(); + + $this->response->expects($this->once()) + ->method('withView') + ->willReturnCallback(function (string $view, array $data) use ($room) { + $this->assertEquals('admin/rooms/edit', $view); + $this->assertEquals($room->id, $data['room']?->id); + $this->assertCount(3, $data['angel_types'] ?? []); + $this->assertCount(1, $data['needed_angel_types'] ?? []); + return $this->response; + }); + + $this->request = $this->request->withAttribute('room_id', 1); + + $controller->edit($this->request); + } + + /** + * @covers \Engelsystem\Controllers\Admin\RoomsController::edit + * @covers \Engelsystem\Controllers\Admin\RoomsController::showEdit + */ + public function testEditNew(): void + { + /** @var RoomsController $controller */ + $controller = $this->app->make(RoomsController::class); + AngelType::factory(3)->create(); + + $this->response->expects($this->once()) + ->method('withView') + ->willReturnCallback(function (string $view, array $data) { + $this->assertEquals('admin/rooms/edit', $view); + $this->assertEmpty($data['room'] ?? []); + $this->assertCount(3, $data['angel_types'] ?? []); + $this->assertEmpty($data['needed_angel_types'] ?? []); + return $this->response; + }); + + $controller->edit($this->request); + } + + /** + * @covers \Engelsystem\Controllers\Admin\RoomsController::save + */ + public function testSave(): void + { + /** @var RoomsController $controller */ + $controller = $this->app->make(RoomsController::class); + $controller->setValidator(new Validator()); + AngelType::factory(3)->create(); + + $this->setExpects($this->redirect, 'to', ['/admin/rooms']); + + $this->request = $this->request->withParsedBody([ + 'name' => 'Testroom', + 'description' => 'Something', + 'dect' => 'DECTNR', + 'map_url' => 'https://osm.url/#map=h/x/y', + 'angel_type_1' => '0', + 'angel_type_2' => '3', + ]); + + $controller->save($this->request); + + $this->assertTrue($this->log->hasInfoThatContains('Updated room')); + $this->assertHasNotification('room.edit.success'); + $this->assertCount(1, Room::whereName('Testroom')->get()); + + $neededAngelType = NeededAngelType::whereRoomId(1) + ->where('angel_type_id', 2) + ->where('count', 3) + ->get(); + $this->assertCount(1, $neededAngelType); + } + + /** + * @covers \Engelsystem\Controllers\Admin\RoomsController::save + */ + public function testSaveUniqueName(): void + { + /** @var RoomsController $controller */ + $controller = $this->app->make(RoomsController::class); + $controller->setValidator(new Validator()); + Room::factory()->create(['name' => 'Testroom']); + + $this->request = $this->request->withParsedBody([ + 'name' => 'Testroom', + ]); + + $this->expectException(ValidationException::class); + $controller->save($this->request); + } + + /** + * @covers \Engelsystem\Controllers\Admin\RoomsController::save + * @covers \Engelsystem\Controllers\Admin\RoomsController::delete + */ + public function testSaveDelete(): void + { + /** @var RoomsController $controller */ + $controller = $this->app->make(RoomsController::class); + $controller->setValidator(new Validator()); + /** @var Room $room */ + $room = Room::factory()->create(); + + $this->request = $this->request->withParsedBody([ + 'id' => '1', + 'delete' => '1', + ]); + + $controller->save($this->request); + $this->assertEmpty(Room::find($room->id)); + } + + /** + * @covers \Engelsystem\Controllers\Admin\RoomsController::delete + */ + public function testDelete(): void + { + /** @var EventDispatcher|MockObject $dispatcher */ + $dispatcher = $this->createMock(EventDispatcher::class); + $this->app->instance('events.dispatcher', $dispatcher); + /** @var RoomsController $controller */ + $controller = $this->app->make(RoomsController::class); + $controller->setValidator(new Validator()); + /** @var Room $room */ + $room = Room::factory()->create(); + /** @var Shift $shift */ + $shift = Shift::factory()->create(['room_id' => $room->id, 'start' => Carbon::create()->subHour()]); + /** @var User $user */ + $user = User::factory()->create(['name' => 'foo', 'email' => 'lorem@ipsum']); + /** @var ShiftEntry $shiftEntry */ + ShiftEntry::factory()->create(['shift_id' => $shift->id, 'user_id' => $user->id]); + + $this->setExpects($this->redirect, 'to', ['/admin/rooms'], $this->response); + + $dispatcher->expects($this->once()) + ->method('dispatch') + ->willReturnCallback(function (string $event, array $data) use ($room, $user) { + $this->assertEquals('shift.entry.deleting', $event); + $this->assertEquals($room->id, $data['room']->id); + $this->assertEquals($user->id, $data['user']->id); + + return []; + }); + + $this->request = $this->request->withParsedBody(['id' => 1, 'delete' => '1']); + + $controller->delete($this->request); + + $this->assertNull(Room::find($room->id)); + $this->assertTrue($this->log->hasInfoThatContains('Deleted room')); + $this->assertHasNotification('room.delete.success'); + } + + public function setUp(): void + { + parent::setUp(); + + $this->redirect = $this->createMock(Redirector::class); + $this->app->instance(Redirector::class, $this->redirect); + } +} diff --git a/tests/Unit/Http/ResponseTest.php b/tests/Unit/Http/ResponseTest.php index e7c3c2de..cecb0e10 100644 --- a/tests/Unit/Http/ResponseTest.php +++ b/tests/Unit/Http/ResponseTest.php @@ -159,10 +159,10 @@ class ResponseTest extends TestCase $response = new Response('', 200, [], null, $session); $response->withInput(['some' => 'value']); - $this->assertEquals(['some' => 'value'], $session->get('form-data')); + $this->assertEquals('value', $session->get('form-data-some')); $response->withInput(['lorem' => 'ipsum']); - $this->assertEquals(['lorem' => 'ipsum'], $session->get('form-data')); + $this->assertEquals('ipsum', $session->get('form-data-lorem')); } /** diff --git a/tests/Unit/Http/Validation/ValidatorTest.php b/tests/Unit/Http/Validation/ValidatorTest.php index 0938c8c0..e06037f5 100644 --- a/tests/Unit/Http/Validation/ValidatorTest.php +++ b/tests/Unit/Http/Validation/ValidatorTest.php @@ -171,4 +171,19 @@ class ValidatorTest extends TestCase ['foo' => 'optional|int'] )); } + + /** + * @covers \Engelsystem\Http\Validation\Validator::addErrors + */ + public function testAddErrors(): void + { + $val = new Validator(); + $val->addErrors(['bar' => ['Lorem']]); + $val->addErrors(['foo' => ['Foo value is definitely wrong!']]); + + $this->assertEquals([ + 'bar' => ['Lorem'], + 'foo' => ['Foo value is definitely wrong!'], + ], $val->getErrors()); + } } diff --git a/tests/Unit/Middleware/ErrorHandlerTest.php b/tests/Unit/Middleware/ErrorHandlerTest.php index 52e21dea..70c81b8c 100644 --- a/tests/Unit/Middleware/ErrorHandlerTest.php +++ b/tests/Unit/Middleware/ErrorHandlerTest.php @@ -220,9 +220,7 @@ class ErrorHandlerTest extends TestCase ], ], ], - 'form-data' => [ - 'foo' => 'bar', - ], + 'form-data-foo' => 'bar', ], $session->all()); $request = $request->withAddedHeader('referer', '/foo/batz'); diff --git a/tests/Unit/Renderer/Twig/Extensions/SessionTest.php b/tests/Unit/Renderer/Twig/Extensions/SessionTest.php index 35d37c02..c3e2882f 100644 --- a/tests/Unit/Renderer/Twig/Extensions/SessionTest.php +++ b/tests/Unit/Renderer/Twig/Extensions/SessionTest.php @@ -5,8 +5,8 @@ declare(strict_types=1); namespace Engelsystem\Test\Unit\Renderer\Twig\Extensions; use Engelsystem\Renderer\Twig\Extensions\Session; -use PHPUnit\Framework\MockObject\MockObject; use Symfony\Component\HttpFoundation\Session\Session as SymfonySession; +use Symfony\Component\HttpFoundation\Session\Storage\MockArraySessionStorage; class SessionTest extends ExtensionTest { @@ -16,13 +16,31 @@ class SessionTest extends ExtensionTest */ public function testGetGlobals(): void { - /** @var SymfonySession|MockObject $session */ - $session = $this->createMock(SymfonySession::class); + $session = new SymfonySession(new MockArraySessionStorage()); $extension = new Session($session); $functions = $extension->getFunctions(); $this->assertExtensionExists('session_get', [$session, 'get'], $functions); $this->assertExtensionExists('session_set', [$session, 'set'], $functions); + $this->assertExtensionExists('session_pop', [$extension, 'sessionPop'], $functions); + } + + /** + * @covers \Engelsystem\Renderer\Twig\Extensions\Session::sessionPop + */ + public function testSessionPop(): void + { + $session = new SymfonySession(new MockArraySessionStorage()); + $session->set('test', 'value'); + + $extension = new Session($session); + + $result = $extension->sessionPop('test'); + $this->assertEquals('value', $result); + $this->assertFalse($session->has('test')); + + $result = $extension->sessionPop('foo', 'default value'); + $this->assertEquals('default value', $result); } }