Policies (ZonePolicy/DevicePolicy/AutomationRulePolicy): не multi-tenant изоляция по user_id, а household-wide RBAC — все авторизованные видят одни и те же данные, только owner может создавать/менять/удалять. user_id на записи — это created_by, а не граница видимости. Form Requests с authorize() через политики; DeviceRequest учитывает unique external_id с ignore на update. AutomationRuleRequest: action_type остаётся строкой (не enum), т.к. это открытый список из device_types.capabilities; для действия set_level в форме есть отдельное поле "level" (Alpine.js переключает его видимость), которое контроллер мапит в action_params — без сырого JSON-редактора для MVP. Контроллеры без show (index+create/edit достаточно), Blade-формы на базе Breeze-компонентов. Навигация и дашборд (счётчики зон/устройств/активных правил) обновлены. Добавлен DemoGrowboxSeeder (демо-зона с датчиком, вентилятором и правилом) и viewer-пользователь в сидер. Восстановлен трейт AuthorizesRequests в базовом Controller (Laravel 11+ убрал его по умолчанию) — нужен для $this->authorize(). Проверено: 33/33 теста (включая новые ZoneControllerTest, AutomationRuleControllerTest — маппинг level→action_params, 403 для viewer), полный CRUD-цикл вживую через браузер под owner (создание зоны, правила с set_level, Alpine-переключение поля "Уровень"), доступ viewer подтверждён как read-only (нет кнопок изменения, прямой заход на /zones/create отдаёт 403).
61 lines
3.0 KiB
PHP
61 lines
3.0 KiB
PHP
<x-app-layout>
|
|
<x-slot name="header">
|
|
<div class="flex justify-between items-center">
|
|
<h2 class="font-semibold text-xl text-gray-800 leading-tight">
|
|
{{ __('Зоны') }}
|
|
</h2>
|
|
@can('create', \App\Models\Zone::class)
|
|
<x-primary-button onclick="window.location='{{ route('zones.create') }}'">
|
|
{{ __('Добавить зону') }}
|
|
</x-primary-button>
|
|
@endcan
|
|
</div>
|
|
</x-slot>
|
|
|
|
<div class="py-12">
|
|
<div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
|
|
<div class="bg-white overflow-hidden shadow-sm sm:rounded-lg p-6">
|
|
@if (session('status'))
|
|
<div class="mb-4 text-sm text-green-600">{{ session('status') }}</div>
|
|
@endif
|
|
|
|
<table class="w-full text-left text-sm">
|
|
<thead>
|
|
<tr class="border-b">
|
|
<th class="py-2">{{ __('Название') }}</th>
|
|
<th class="py-2">{{ __('Описание') }}</th>
|
|
<th class="py-2">{{ __('Устройств') }}</th>
|
|
<th class="py-2"></th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
@forelse ($zones as $zone)
|
|
<tr class="border-b">
|
|
<td class="py-2">{{ $zone->name }}</td>
|
|
<td class="py-2 text-gray-500">{{ $zone->description }}</td>
|
|
<td class="py-2">{{ $zone->devices_count }}</td>
|
|
<td class="py-2 text-right space-x-2">
|
|
@can('update', $zone)
|
|
<a href="{{ route('zones.edit', $zone) }}" class="text-indigo-600 hover:underline">{{ __('Изменить') }}</a>
|
|
@endcan
|
|
@can('delete', $zone)
|
|
<form method="POST" action="{{ route('zones.destroy', $zone) }}" class="inline" onsubmit="return confirm('{{ __('Удалить зону?') }}')">
|
|
@csrf
|
|
@method('DELETE')
|
|
<button type="submit" class="text-red-600 hover:underline">{{ __('Удалить') }}</button>
|
|
</form>
|
|
@endcan
|
|
</td>
|
|
</tr>
|
|
@empty
|
|
<tr>
|
|
<td colspan="4" class="py-4 text-gray-500">{{ __('Зон пока нет.') }}</td>
|
|
</tr>
|
|
@endforelse
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</x-app-layout>
|