diff --git a/laravel-app/app/Http/Controllers/DeviceTypeController.php b/laravel-app/app/Http/Controllers/DeviceTypeController.php new file mode 100644 index 0000000..3d9af8e --- /dev/null +++ b/laravel-app/app/Http/Controllers/DeviceTypeController.php @@ -0,0 +1,36 @@ +authorize('viewAny', DeviceType::class); + + $deviceTypes = DeviceType::withCount('devices')->orderBy('code')->get(); + + return Inertia::render('DeviceTypes/Index', [ + 'deviceTypes' => DeviceTypeResource::collection($deviceTypes)->resolve(), + ]); + } + + public function create() + { + $this->authorize('create', DeviceType::class); + + return Inertia::render('DeviceTypes/Create'); + } + + public function store(DeviceTypeRequest $request) + { + DeviceType::create($request->validated()); + + return redirect()->route('device-types.index')->with('status', 'Тип устройства создан.'); + } +} diff --git a/laravel-app/app/Http/Requests/DeviceTypeRequest.php b/laravel-app/app/Http/Requests/DeviceTypeRequest.php new file mode 100644 index 0000000..95406ba --- /dev/null +++ b/laravel-app/app/Http/Requests/DeviceTypeRequest.php @@ -0,0 +1,29 @@ +user()->can('create', DeviceType::class); + } + + /** + * @return array + */ + public function rules(): array + { + return [ + 'code' => ['required', 'string', 'max:255', 'alpha_dash', Rule::unique('device_types', 'code')], + 'category' => ['required', Rule::enum(DeviceCategory::class)], + 'capabilities' => ['array'], + 'capabilities.*' => ['string', 'max:255'], + ]; + } +} diff --git a/laravel-app/app/Http/Resources/DeviceTypeResource.php b/laravel-app/app/Http/Resources/DeviceTypeResource.php index 477ce7e..79f1051 100644 --- a/laravel-app/app/Http/Resources/DeviceTypeResource.php +++ b/laravel-app/app/Http/Resources/DeviceTypeResource.php @@ -17,6 +17,7 @@ class DeviceTypeResource extends JsonResource 'code' => $this->code, 'category' => $this->category->value, 'capabilities' => $this->capabilities, + 'devices_count' => $this->whenCounted('devices'), ]; } } diff --git a/laravel-app/app/Policies/DeviceTypePolicy.php b/laravel-app/app/Policies/DeviceTypePolicy.php new file mode 100644 index 0000000..8a0ba53 --- /dev/null +++ b/laravel-app/app/Policies/DeviceTypePolicy.php @@ -0,0 +1,25 @@ +isOwner(); + } +} diff --git a/laravel-app/resources/js/Pages/DeviceTypes/Create.vue b/laravel-app/resources/js/Pages/DeviceTypes/Create.vue new file mode 100644 index 0000000..c066a51 --- /dev/null +++ b/laravel-app/resources/js/Pages/DeviceTypes/Create.vue @@ -0,0 +1,124 @@ + + + diff --git a/laravel-app/resources/js/Pages/DeviceTypes/Index.vue b/laravel-app/resources/js/Pages/DeviceTypes/Index.vue new file mode 100644 index 0000000..54271a0 --- /dev/null +++ b/laravel-app/resources/js/Pages/DeviceTypes/Index.vue @@ -0,0 +1,74 @@ + + + diff --git a/laravel-app/resources/js/Pages/Devices/Index.vue b/laravel-app/resources/js/Pages/Devices/Index.vue index 43adda8..a9d5638 100644 --- a/laravel-app/resources/js/Pages/Devices/Index.vue +++ b/laravel-app/resources/js/Pages/Devices/Index.vue @@ -23,13 +23,16 @@ function destroy(device) { diff --git a/laravel-app/resources/js/routes.js b/laravel-app/resources/js/routes.js index 70cbfbf..de76eec 100644 --- a/laravel-app/resources/js/routes.js +++ b/laravel-app/resources/js/routes.js @@ -25,6 +25,11 @@ export const routes = { turnOff: (id) => `/devices/${id}/turn-off`, setLevel: (id) => `/devices/${id}/set-level`, }, + deviceTypes: { + index: '/device-types', + create: '/device-types/create', + store: '/device-types', + }, automationRules: { index: '/automation-rules', create: '/automation-rules/create', diff --git a/laravel-app/routes/web.php b/laravel-app/routes/web.php index 919a69d..d89f533 100644 --- a/laravel-app/routes/web.php +++ b/laravel-app/routes/web.php @@ -2,6 +2,7 @@ use App\Http\Controllers\AutomationRuleController; use App\Http\Controllers\DeviceController; +use App\Http\Controllers\DeviceTypeController; use App\Http\Controllers\ProfileController; use App\Http\Controllers\ZoneController; use App\Models\AutomationRule; @@ -32,6 +33,7 @@ Route::get('/dashboard', function (DeviceShadow $shadow) { Route::middleware(['auth', 'verified'])->group(function () { Route::resource('zones', ZoneController::class); Route::resource('devices', DeviceController::class); + Route::resource('device-types', DeviceTypeController::class)->only(['index', 'create', 'store']); Route::resource('automation-rules', AutomationRuleController::class) ->except('show') ->parameters(['automation-rules' => 'automation_rule']); diff --git a/laravel-app/tests/Feature/DeviceTypeControllerTest.php b/laravel-app/tests/Feature/DeviceTypeControllerTest.php new file mode 100644 index 0000000..fb4d0f5 --- /dev/null +++ b/laravel-app/tests/Feature/DeviceTypeControllerTest.php @@ -0,0 +1,76 @@ +create(['role' => UserRole::Owner]); + + $response = $this->actingAs($owner)->post(route('device-types.store'), [ + 'code' => 'smart_socket', + 'category' => 'actuator', + 'capabilities' => ['turn_on', 'turn_off'], + ]); + + $response->assertRedirect(route('device-types.index')); + $this->assertDatabaseHas('device_types', [ + 'code' => 'smart_socket', + 'category' => 'actuator', + ]); + $this->assertSame( + ['turn_on', 'turn_off'], + DeviceType::where('code', 'smart_socket')->first()->capabilities, + ); + } + + public function test_viewer_cannot_create_device_type(): void + { + $viewer = User::factory()->create(['role' => UserRole::Viewer]); + + $response = $this->actingAs($viewer)->post(route('device-types.store'), [ + 'code' => 'smart_socket', + 'category' => 'actuator', + ]); + + $response->assertForbidden(); + $this->assertDatabaseMissing('device_types', ['code' => 'smart_socket']); + } + + public function test_code_must_be_unique(): void + { + $owner = User::factory()->create(['role' => UserRole::Owner]); + DeviceType::create(['code' => 'fan', 'category' => 'actuator', 'capabilities' => []]); + + $response = $this->actingAs($owner)->post(route('device-types.store'), [ + 'code' => 'fan', + 'category' => 'actuator', + ]); + + $response->assertSessionHasErrors('code'); + } + + public function test_viewer_can_view_device_types_index(): void + { + $viewer = User::factory()->create(['role' => UserRole::Viewer]); + DeviceType::create(['code' => 'fan', 'category' => 'actuator', 'capabilities' => ['turn_on']]); + + $response = $this->actingAs($viewer)->get(route('device-types.index')); + + $response->assertOk(); + $response->assertInertia(fn (Assert $page) => $page + ->component('DeviceTypes/Index') + ->where('deviceTypes.0.code', 'fan') + ); + } +}