Fenster & Türen page: contact/motion sensor overview
Activates the windows nav tab. Summary banner (how many open) + per-room sensor list with open/closed state and battery, live via Echo. Nav check 5/5 clean. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>feat/phase-1-bootstrap
parent
f4d8f27db4
commit
cc81e309b3
|
|
@ -0,0 +1,33 @@
|
|||
<?php
|
||||
|
||||
namespace App\Livewire;
|
||||
|
||||
use App\Models\Entity;
|
||||
use Livewire\Attributes\Layout;
|
||||
use Livewire\Attributes\On;
|
||||
use Livewire\Component;
|
||||
|
||||
#[Layout('layouts.app')]
|
||||
class Windows extends Component
|
||||
{
|
||||
#[On('echo-private:home,.DeviceStateChanged')]
|
||||
public function onDeviceStateChanged(): void {}
|
||||
|
||||
public function render()
|
||||
{
|
||||
$sensors = Entity::query()
|
||||
->whereIn('type', ['contact', 'motion'])
|
||||
->with(['device.room', 'device.entities.state', 'state'])
|
||||
->get()
|
||||
->sortBy(fn ($e) => ($e->device->room?->name ?? 'zzz').$e->device->name)
|
||||
->values();
|
||||
|
||||
$open = $sensors->filter(fn ($e) => $e->type === 'contact' && data_get($e->state, 'state.open') === true)->count();
|
||||
|
||||
return view('livewire.windows', [
|
||||
'groups' => $sensors->groupBy(fn ($e) => $e->device->room?->name ?? __('devices.no_room')),
|
||||
'open' => $open,
|
||||
'total' => $sensors->count(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
<?php
|
||||
|
||||
return [
|
||||
'subtitle' => 'Kontakte an Fenstern und Türen.',
|
||||
'empty' => 'Keine Kontaktsensoren.',
|
||||
'all_closed' => 'Alles geschlossen',
|
||||
'open_count' => '{1}1 offen|[2,*]:count offen',
|
||||
'sensor_count' => '{0}Keine Sensoren|{1}1 Sensor|[2,*]:count Sensoren',
|
||||
];
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
<?php
|
||||
|
||||
return [
|
||||
'subtitle' => 'Contacts on windows and doors.',
|
||||
'empty' => 'No contact sensors.',
|
||||
'all_closed' => 'All closed',
|
||||
'open_count' => '{1}1 open|[2,*]:count open',
|
||||
'sensor_count' => '{0}No sensors|{1}1 sensor|[2,*]:count sensors',
|
||||
];
|
||||
|
|
@ -9,7 +9,7 @@
|
|||
['key' => 'persons', 'icon' => 'persons', 'route' => null, 'lock' => false],
|
||||
],
|
||||
'nav.section_security' => [
|
||||
['key' => 'windows', 'icon' => 'window', 'route' => null, 'lock' => false],
|
||||
['key' => 'windows', 'icon' => 'window', 'route' => 'windows', 'lock' => false],
|
||||
['key' => 'access', 'icon' => 'shield', 'route' => null, 'lock' => true],
|
||||
],
|
||||
'nav.section_system' => [
|
||||
|
|
|
|||
|
|
@ -0,0 +1,50 @@
|
|||
<div wire:poll.30s>
|
||||
<x-topbar :title="__('nav.windows')" :subtitle="__('windows.subtitle')" />
|
||||
|
||||
<div class="px-5 lg:px-[26px] py-6 flex flex-col gap-5 max-w-[1560px] mx-auto w-full">
|
||||
@if ($total === 0)
|
||||
<x-panel><p class="text-[13px] text-ink-3 text-center py-6">{{ __('windows.empty') }}</p></x-panel>
|
||||
@else
|
||||
{{-- summary --}}
|
||||
<section class="relative overflow-hidden rounded-card border border-line-soft {{ $open > 0 ? 'bg-warning/10' : 'bg-online/10' }}">
|
||||
<span class="absolute inset-y-0 left-0 w-[3px] {{ $open > 0 ? 'bg-warning' : 'bg-online' }}"></span>
|
||||
<div class="flex items-center gap-3.5 p-4 pl-5">
|
||||
<span class="grid place-items-center w-10 h-10 rounded-lg bg-base/40 {{ $open > 0 ? 'text-warning' : 'text-online' }} shrink-0">
|
||||
<x-icon :name="$open > 0 ? 'window' : 'check'" :size="20" />
|
||||
</span>
|
||||
<div>
|
||||
<h2 class="text-[15px] font-bold text-ink">
|
||||
{{ $open > 0 ? trans_choice('windows.open_count', $open, ['count' => $open]) : __('windows.all_closed') }}
|
||||
</h2>
|
||||
<p class="text-[12.5px] text-ink-2">{{ trans_choice('windows.sensor_count', $total, ['count' => $total]) }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@foreach ($groups as $roomName => $sensors)
|
||||
<x-panel :title="$roomName">
|
||||
<div class="flex flex-col divide-y divide-line-soft -my-1">
|
||||
@foreach ($sensors as $sensor)
|
||||
@php
|
||||
$battery = $sensor->device->entities->firstWhere('type', 'battery');
|
||||
$pct = $battery ? (int) data_get($battery->state, 'state.percent', 0) : null;
|
||||
@endphp
|
||||
<div class="flex items-center gap-3 py-3 first:pt-1 last:pb-1">
|
||||
<span class="grid place-items-center w-8 h-8 rounded-lg bg-inset text-ink-2 shrink-0">
|
||||
<x-icon :name="$sensor->type === 'motion' ? 'activity' : 'window'" :size="16" />
|
||||
</span>
|
||||
<a href="{{ route('devices.show', $sensor->device) }}" wire:navigate class="text-[13px] font-semibold text-ink hover:text-accent transition-colors truncate">{{ $sensor->device->name }}</a>
|
||||
<div class="ml-auto flex items-center gap-2 shrink-0">
|
||||
@if (! is_null($pct))
|
||||
<x-status-pill :state="$pct < 20 ? 'offline' : 'neutral'">{{ __('devices.battery') }} · <span class="font-mono">{{ $pct }}%</span></x-status-pill>
|
||||
@endif
|
||||
<x-entity-state :entity="$sensor" />
|
||||
</div>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
</x-panel>
|
||||
@endforeach
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -7,6 +7,7 @@ use App\Livewire\Devices\Show as DeviceShow;
|
|||
use App\Livewire\Host;
|
||||
use App\Livewire\Rooms\Index as RoomIndex;
|
||||
use App\Livewire\Rooms\Show as RoomShow;
|
||||
use App\Livewire\Windows;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Route;
|
||||
|
||||
|
|
@ -22,6 +23,7 @@ Route::middleware('auth')->group(function () {
|
|||
Route::get('/rooms/{room}', RoomShow::class)->name('rooms.show');
|
||||
Route::get('/devices', DeviceIndex::class)->name('devices.index');
|
||||
Route::get('/devices/{device}', DeviceShow::class)->name('devices.show');
|
||||
Route::get('/windows', Windows::class)->name('windows');
|
||||
Route::get('/host', Host::class)->name('host');
|
||||
|
||||
Route::post('/logout', function () {
|
||||
|
|
|
|||
Loading…
Reference in New Issue