From f3ae29d65aaccee024d6fedcf2e54c3e50775ed6 Mon Sep 17 00:00:00 2001 From: HomeOS Bootstrap Date: Sat, 18 Jul 2026 10:12:49 +0200 Subject: [PATCH] feat(rooms): create + delete rooms (delete unassigns devices) Issue 5: rooms couldn't be created or deleted. - "Raum anlegen" button + CreateRoom modal (name + icon picker) on the rooms index. - Delete on the room page behind a confirm that states how many devices are in it; on delete the devices are KEPT but moved to "no room" (room_id null). 3 tests. Suite green, 12/12 tabs clean. Co-Authored-By: Claude Opus 4.8 --- app/Livewire/Modals/CreateRoom.php | 36 ++++++++++++ app/Livewire/Rooms/Show.php | 10 ++++ app/Models/Room.php | 3 + lang/de/rooms.php | 8 +++ lang/en/rooms.php | 8 +++ .../livewire/modals/create-room.blade.php | 33 +++++++++++ .../views/livewire/rooms/index.blade.php | 9 ++- resources/views/livewire/rooms/show.blade.php | 6 ++ tests/Feature/RoomsTest.php | 56 +++++++++++++++++++ 9 files changed, 168 insertions(+), 1 deletion(-) create mode 100644 app/Livewire/Modals/CreateRoom.php create mode 100644 resources/views/livewire/modals/create-room.blade.php create mode 100644 tests/Feature/RoomsTest.php diff --git a/app/Livewire/Modals/CreateRoom.php b/app/Livewire/Modals/CreateRoom.php new file mode 100644 index 0000000..94f98a4 --- /dev/null +++ b/app/Livewire/Modals/CreateRoom.php @@ -0,0 +1,36 @@ +validate([ + 'name' => 'required|string|max:100', + 'icon' => 'required|string|in:'.implode(',', Room::ICON_CHOICES), + ]); + + Room::create([ + 'name' => $this->name, + 'icon' => $this->icon, + 'sort' => (int) (Room::max('sort') ?? 0) + 1, + ]); + + $this->closeModal(); + + return redirect()->route('rooms.index'); + } + + public function render() + { + return view('livewire.modals.create-room'); + } +} diff --git a/app/Livewire/Rooms/Show.php b/app/Livewire/Rooms/Show.php index b7a3da5..f32ab96 100644 --- a/app/Livewire/Rooms/Show.php +++ b/app/Livewire/Rooms/Show.php @@ -36,6 +36,16 @@ class Show extends Component app(DeviceCommandService::class)->toggle($entity); } + /** Delete the room; its devices are kept but moved to "no room". */ + #[On('deleteRoom')] + public function deleteRoom() + { + $this->room->devices()->update(['room_id' => null]); + $this->room->delete(); + + return redirect()->route('rooms.index'); + } + public function render() { $this->room->load(['devices' => fn ($q) => $q->orderBy('name'), 'devices.entities.state']); diff --git a/app/Models/Room.php b/app/Models/Room.php index 9553e64..26b547c 100644 --- a/app/Models/Room.php +++ b/app/Models/Room.php @@ -10,6 +10,9 @@ class Room extends Model { use HasUuid; + /** Icons offered when creating a room (keys map to the x-icon set). */ + public const ICON_CHOICES = ['rooms', 'bolt', 'lamp', 'plug', 'window', 'activity', 'temp', 'devices']; + protected $fillable = ['uuid', 'name', 'icon', 'sort']; public function devices(): HasMany diff --git a/lang/de/rooms.php b/lang/de/rooms.php index 3203c92..19badf1 100644 --- a/lang/de/rooms.php +++ b/lang/de/rooms.php @@ -7,4 +7,12 @@ return [ 'online' => 'online', 'lights' => 'Licht an', 'open' => 'offen', + 'add' => 'Raum anlegen', + 'add_title' => 'Neuer Raum', + 'name' => 'Name', + 'icon' => 'Symbol', + 'delete' => 'Raum löschen', + 'delete_title' => 'Raum löschen?', + 'delete_body' => 'Der Raum wird gelöscht. :count Gerät(e) darin werden keinem Raum mehr zugewiesen (bleiben aber erhalten).', + 'delete_body_empty' => 'Der Raum wird gelöscht. Es sind keine Geräte zugewiesen.', ]; diff --git a/lang/en/rooms.php b/lang/en/rooms.php index 92b0f98..8faf86d 100644 --- a/lang/en/rooms.php +++ b/lang/en/rooms.php @@ -7,4 +7,12 @@ return [ 'online' => 'online', 'lights' => 'lights on', 'open' => 'open', + 'add' => 'Add room', + 'add_title' => 'New room', + 'name' => 'Name', + 'icon' => 'Icon', + 'delete' => 'Delete room', + 'delete_title' => 'Delete room?', + 'delete_body' => 'The room is deleted. :count device(s) in it become unassigned (but are kept).', + 'delete_body_empty' => 'The room is deleted. No devices are assigned.', ]; diff --git a/resources/views/livewire/modals/create-room.blade.php b/resources/views/livewire/modals/create-room.blade.php new file mode 100644 index 0000000..5bf9df1 --- /dev/null +++ b/resources/views/livewire/modals/create-room.blade.php @@ -0,0 +1,33 @@ +
+ +
+
+ + + @error('name')

{{ $message }}

@enderror +
+ +
+ +
+ @foreach (\App\Models\Room::ICON_CHOICES as $ic) + + @endforeach +
+
+
+ + + + + +
+
diff --git a/resources/views/livewire/rooms/index.blade.php b/resources/views/livewire/rooms/index.blade.php index 1c313de..86e8e2e 100644 --- a/resources/views/livewire/rooms/index.blade.php +++ b/resources/views/livewire/rooms/index.blade.php @@ -1,5 +1,12 @@
- + + + + +
@if ($rooms->isEmpty()) diff --git a/resources/views/livewire/rooms/show.blade.php b/resources/views/livewire/rooms/show.blade.php index 06af1cc..81538ce 100644 --- a/resources/views/livewire/rooms/show.blade.php +++ b/resources/views/livewire/rooms/show.blade.php @@ -1,6 +1,12 @@
+ @php $deviceCount = $room->devices->count(); @endphp + diff --git a/tests/Feature/RoomsTest.php b/tests/Feature/RoomsTest.php new file mode 100644 index 0000000..9b3bc77 --- /dev/null +++ b/tests/Feature/RoomsTest.php @@ -0,0 +1,56 @@ +actingAs(User::factory()->create()); + + Livewire::test(CreateRoom::class) + ->set('name', 'Wohnzimmer') + ->set('icon', 'lamp') + ->call('save') + ->assertRedirect(route('rooms.index')); + + $this->assertDatabaseHas('rooms', ['name' => 'Wohnzimmer', 'icon' => 'lamp']); + } + + public function test_create_room_rejects_an_unknown_icon(): void + { + $this->actingAs(User::factory()->create()); + + Livewire::test(CreateRoom::class) + ->set('name', 'X') + ->set('icon', 'not-an-icon') + ->call('save') + ->assertHasErrors('icon'); + } + + public function test_deleting_a_room_keeps_devices_but_unassigns_them(): void + { + $this->actingAs(User::factory()->create()); + + $room = Room::create(['name' => 'Küche', 'icon' => 'rooms', 'sort' => 1]); + $device = Device::create(['name' => 'Lampe', 'vendor' => 'Shelly', 'protocol' => 'http', 'room_id' => $room->id, 'config' => ['ip' => '1.2.3.4']]); + + Livewire::test(Show::class, ['room' => $room]) + ->call('deleteRoom') + ->assertRedirect(route('rooms.index')); + + $this->assertModelMissing($room); + $this->assertNull($device->fresh()->room_id); // device kept, just unassigned + } +}