390 lines
22 KiB
PHP
390 lines
22 KiB
PHP
|
|
<div class="space-y-6" wire:key="events-alerts-component">
|
|
{{-- Page Header --}}
|
|
<div class="flex items-center justify-between">
|
|
<div>
|
|
<flux:heading size="lg">Events & Alerts</flux:heading>
|
|
<flux:subheading>Monitor and manage GPS tracking events</flux:subheading>
|
|
</div>
|
|
<div class="flex space-x-3">
|
|
<flux:button wire:click="refreshEvents" variant="ghost" size="sm" icon="arrow-path">
|
|
Refresh
|
|
</flux:button>
|
|
<flux:button wire:click="markAllAsRead" variant="outline" size="sm" icon="check-circle">
|
|
Mark All Read
|
|
</flux:button>
|
|
</div>
|
|
</div>
|
|
|
|
{{-- Stats Cards --}}
|
|
<div class="grid grid-cols-1 md:grid-cols-4 gap-6">
|
|
<div class="bg-red-50 border border-red-200 rounded-lg p-4">
|
|
<div class="flex items-center">
|
|
<div class="flex-shrink-0">
|
|
<div class="w-8 h-8 bg-red-500 rounded-full flex items-center justify-center">
|
|
<flux:icon name="exclamation-triangle" class="w-4 h-4 text-white" />
|
|
</div>
|
|
</div>
|
|
<div class="ml-3">
|
|
<p class="text-sm font-medium text-red-600">Critical Alerts</p>
|
|
<p class="text-lg font-semibold text-red-900">{{ $this->criticalCount }}</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="bg-yellow-50 border border-yellow-200 rounded-lg p-4">
|
|
<div class="flex items-center">
|
|
<div class="flex-shrink-0">
|
|
<div class="w-8 h-8 bg-yellow-500 rounded-full flex items-center justify-center">
|
|
<flux:icon name="exclamation-circle" class="w-4 h-4 text-white" />
|
|
</div>
|
|
</div>
|
|
<div class="ml-3">
|
|
<p class="text-sm font-medium text-yellow-600">Warnings</p>
|
|
<p class="text-lg font-semibold text-yellow-900">{{ $this->warningCount }}</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="bg-blue-50 border border-blue-200 rounded-lg p-4">
|
|
<div class="flex items-center">
|
|
<div class="flex-shrink-0">
|
|
<div class="w-8 h-8 bg-blue-500 rounded-full flex items-center justify-center">
|
|
<flux:icon name="information-circle" class="w-4 h-4 text-white" />
|
|
</div>
|
|
</div>
|
|
<div class="ml-3">
|
|
<p class="text-sm font-medium text-blue-600">Info Events</p>
|
|
<p class="text-lg font-semibold text-blue-900">{{ $this->infoCount }}</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="bg-gray-50 border border-gray-200 rounded-lg p-4">
|
|
<div class="flex items-center">
|
|
<div class="flex-shrink-0">
|
|
<div class="w-8 h-8 bg-gray-500 rounded-full flex items-center justify-center">
|
|
<flux:icon name="bell" class="w-4 h-4 text-white" />
|
|
</div>
|
|
</div>
|
|
<div class="ml-3">
|
|
<p class="text-sm font-medium text-gray-600">Total Events</p>
|
|
<p class="text-lg font-semibold text-gray-900">{{ $this->totalCount }}</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{{-- Filters --}}
|
|
<div class="bg-white shadow rounded-lg border border-zinc-200">
|
|
<div class="px-4 py-5 sm:p-6">
|
|
<div class="grid grid-cols-1 md:grid-cols-6 gap-4">
|
|
<div>
|
|
<flux:field>
|
|
<flux:label>Event Type</flux:label>
|
|
<flux:select wire:model.live="filterType" size="sm">
|
|
<option value="">All Types</option>
|
|
<option value="geofenceEnter">Geofence Enter</option>
|
|
<option value="geofenceExit">Geofence Exit</option>
|
|
<option value="alarm">Alarm</option>
|
|
<option value="ignitionOn">Ignition On</option>
|
|
<option value="ignitionOff">Ignition Off</option>
|
|
<option value="maintenance">Maintenance</option>
|
|
<option value="textMessage">Text Message</option>
|
|
<option value="driverChanged">Driver Changed</option>
|
|
</flux:select>
|
|
</flux:field>
|
|
</div>
|
|
|
|
<div>
|
|
<flux:field>
|
|
<flux:label>Status</flux:label>
|
|
<flux:select wire:model.live="filterStatus" size="sm">
|
|
<option value="">All Status</option>
|
|
<option value="unread">Unread</option>
|
|
<option value="read">Acknowledged</option>
|
|
</flux:select>
|
|
</flux:field>
|
|
</div>
|
|
|
|
<div>
|
|
<flux:field>
|
|
<flux:label>Device</flux:label>
|
|
<flux:select wire:model.live="filterDevice" size="sm">
|
|
<option value="">All Devices</option>
|
|
@foreach($devices as $device)
|
|
<option value="{{ $device->id }}">{{ $device->name }}</option>
|
|
@endforeach
|
|
</flux:select>
|
|
</flux:field>
|
|
</div>
|
|
|
|
<div>
|
|
<flux:field>
|
|
<flux:label>Date From</flux:label>
|
|
<flux:input wire:model.live="filterDateFrom" type="date" size="sm" />
|
|
</flux:field>
|
|
</div>
|
|
|
|
<div>
|
|
<flux:field>
|
|
<flux:label>Date To</flux:label>
|
|
<flux:input wire:model.live="filterDateTo" type="date" size="sm" />
|
|
</flux:field>
|
|
</div>
|
|
|
|
<div>
|
|
<flux:field>
|
|
<flux:label>Status</flux:label>
|
|
<flux:select wire:model.live="filterStatus" size="sm">
|
|
<option value="">All Status</option>
|
|
<option value="unread">Unread</option>
|
|
<option value="read">Read</option>
|
|
<option value="acknowledged">Acknowledged</option>
|
|
</flux:select>
|
|
</flux:field>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="mt-4 flex justify-between">
|
|
<flux:button wire:click="clearFilters" variant="ghost" size="sm">
|
|
Clear Filters
|
|
</flux:button>
|
|
<div class="text-sm text-gray-500">
|
|
Showing {{ $events->count() }} of {{ $events->total() }} events
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{{-- Events List --}}
|
|
<div class="bg-white shadow rounded-lg border border-zinc-200">
|
|
<div class="px-4 py-5 sm:p-6">
|
|
<flux:heading size="base" class="mb-4">Recent Events</flux:heading>
|
|
|
|
@if($events->count() > 0)
|
|
<div class="space-y-4">
|
|
@foreach($events as $event)
|
|
<div class="border rounded-lg p-4 {{ $event->acknowledged ? 'bg-gray-50' : 'bg-white' }} hover:shadow-md transition-shadow">
|
|
<div class="flex items-start justify-between">
|
|
<div class="flex-1">
|
|
<div class="flex items-center space-x-3 mb-2">
|
|
{{-- Event Icon --}}
|
|
<div class="flex-shrink-0">
|
|
@php
|
|
$eventSeverity = $this->getEventSeverity($event->type);
|
|
$iconClass = match($eventSeverity) {
|
|
'critical' => 'bg-red-500 text-white',
|
|
'warning' => 'bg-yellow-500 text-white',
|
|
'info' => 'bg-blue-500 text-white',
|
|
default => 'bg-gray-500 text-white'
|
|
};
|
|
$icon = match($event->type) {
|
|
'geofenceEnter', 'geofenceExit' => 'map-pin',
|
|
'alarm' => 'exclamation-triangle',
|
|
'ignitionOn', 'ignitionOff' => 'key',
|
|
'maintenance' => 'wrench-screwdriver',
|
|
'textMessage' => 'chat-bubble-left',
|
|
'driverChanged' => 'user',
|
|
default => 'bell'
|
|
};
|
|
@endphp
|
|
<div class="w-8 h-8 rounded-full flex items-center justify-center {{ $iconClass }}">
|
|
<flux:icon name="{{ $icon }}" class="w-4 h-4" />
|
|
</div>
|
|
</div>
|
|
|
|
{{-- Event Title --}}
|
|
<div class="flex-1">
|
|
<h4 class="font-medium text-gray-900">
|
|
{{ ucfirst(str_replace(['geofence', 'ignition'], ['Geofence ', 'Ignition '], $event->type)) }}
|
|
@if(!$event->acknowledged)
|
|
<span class="inline-flex items-center px-2 py-1 rounded-full text-xs font-medium bg-blue-100 text-blue-800 ml-2">
|
|
New
|
|
</span>
|
|
@endif
|
|
</h4>
|
|
<p class="text-sm text-gray-600 mt-1">{{ $event->device->name }} - {{ $event->event_time->format('M j, Y H:i') }}</p>
|
|
</div>
|
|
|
|
{{-- Device & Time --}}
|
|
<div class="text-right">
|
|
<p class="text-sm font-medium text-gray-900">{{ $event->device->name ?? 'Unknown Device' }}</p>
|
|
<p class="text-xs text-gray-500">{{ $event->event_time->diffForHumans() }}</p>
|
|
<p class="text-xs text-gray-400">{{ $event->event_time->format('M j, Y H:i') }}</p>
|
|
</div>
|
|
</div>
|
|
|
|
{{-- Additional Event Details --}}
|
|
@if($event->geofence || $event->position)
|
|
<div class="ml-11 mt-2 text-sm text-gray-600">
|
|
@if($event->geofence)
|
|
<span class="inline-flex items-center px-2 py-1 rounded-full text-xs bg-gray-100 text-gray-800 mr-2">
|
|
<flux:icon name="map-pin" class="w-3 h-3 mr-1" />
|
|
{{ $event->geofence->name }}
|
|
</span>
|
|
@endif
|
|
@if($event->position)
|
|
<span class="inline-flex items-center px-2 py-1 rounded-full text-xs bg-gray-100 text-gray-800">
|
|
<flux:icon name="globe-alt" class="w-3 h-3 mr-1" />
|
|
{{ number_format($event->position->latitude, 6) }}, {{ number_format($event->position->longitude, 6) }}
|
|
</span>
|
|
@endif
|
|
</div>
|
|
@endif
|
|
</div>
|
|
|
|
{{-- Actions --}}
|
|
<div class="flex space-x-2 ml-4">
|
|
@if(!$event->acknowledged)
|
|
<flux:button wire:click="acknowledgeEvent({{ $event->id }})" variant="ghost" size="xs" icon="check">
|
|
</flux:button>
|
|
@endif
|
|
<flux:button wire:click="showEventDetails({{ $event->id }})" variant="ghost" size="xs" icon="information-circle">
|
|
</flux:button>
|
|
@if($event->position)
|
|
<flux:button wire:click="showOnMap({{ $event->position->latitude }}, {{ $event->position->longitude }})" variant="ghost" size="xs" icon="map">
|
|
</flux:button>
|
|
@endif
|
|
</div>
|
|
</div>
|
|
</div>
|
|
@endforeach
|
|
</div>
|
|
|
|
{{-- Pagination --}}
|
|
<div class="mt-6">
|
|
{{ $events->links() }}
|
|
</div>
|
|
|
|
@else
|
|
<div class="text-center py-12">
|
|
<flux:icon name="bell-slash" class="w-12 h-12 text-gray-400 mx-auto mb-4" />
|
|
<h3 class="text-lg font-medium text-gray-900 mb-2">No events found</h3>
|
|
<p class="text-gray-500">
|
|
@if($this->hasActiveFilters())
|
|
Try adjusting your filters to see more events.
|
|
@else
|
|
Events will appear here when devices generate alerts or notifications.
|
|
@endif
|
|
</p>
|
|
</div>
|
|
@endif
|
|
</div>
|
|
</div>
|
|
|
|
{{-- Event Details Modal --}}
|
|
@if($selectedEvent)
|
|
<div class="fixed inset-0 z-50 overflow-y-auto" wire:key="event-modal-{{ $selectedEvent->id }}">
|
|
<div class="flex items-center justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:block sm:p-0">
|
|
<div class="fixed inset-0 transition-opacity" aria-hidden="true">
|
|
<div class="absolute inset-0 bg-gray-500 opacity-75" wire:click="closeEventDetails"></div>
|
|
</div>
|
|
|
|
<div class="inline-block align-bottom bg-white rounded-lg text-left overflow-hidden shadow-xl transform transition-all sm:my-8 sm:align-middle sm:max-w-lg sm:w-full">
|
|
<div class="bg-white px-4 pt-5 pb-4 sm:p-6 sm:pb-4">
|
|
<div class="flex items-start">
|
|
<div class="flex-shrink-0">
|
|
@php
|
|
$eventSeverity = $this->getEventSeverity($selectedEvent->type);
|
|
$iconClass = match($eventSeverity) {
|
|
'critical' => 'bg-red-500 text-white',
|
|
'warning' => 'bg-yellow-500 text-white',
|
|
'info' => 'bg-blue-500 text-white',
|
|
default => 'bg-gray-500 text-white'
|
|
};
|
|
@endphp
|
|
<div class="w-10 h-10 rounded-full flex items-center justify-center {{ $iconClass }}">
|
|
<flux:icon name="bell" class="w-5 h-5" />
|
|
</div>
|
|
</div>
|
|
<div class="ml-4 flex-1">
|
|
<flux:heading size="lg" class="mb-2">Event Details</flux:heading>
|
|
|
|
<div class="space-y-3">
|
|
<div>
|
|
<span class="text-sm font-medium text-gray-600">Type:</span>
|
|
<span class="ml-2 text-sm text-gray-900">{{ ucfirst($selectedEvent->type) }}</span>
|
|
</div>
|
|
|
|
<div>
|
|
<span class="text-sm font-medium text-gray-600">Severity:</span>
|
|
<span class="ml-2 inline-flex items-center px-2 py-1 rounded-full text-xs font-medium
|
|
{{ $eventSeverity === 'critical' ? 'bg-red-100 text-red-800' :
|
|
($eventSeverity === 'warning' ? 'bg-yellow-100 text-yellow-800' : 'bg-blue-100 text-blue-800') }}">
|
|
{{ ucfirst($eventSeverity) }}
|
|
</span>
|
|
</div>
|
|
|
|
<div>
|
|
<span class="text-sm font-medium text-gray-600">Device:</span>
|
|
<span class="ml-2 text-sm text-gray-900">{{ $selectedEvent->device->name ?? 'Unknown' }}</span>
|
|
</div>
|
|
|
|
<div>
|
|
<span class="text-sm font-medium text-gray-600">Time:</span>
|
|
<span class="ml-2 text-sm text-gray-900">{{ $selectedEvent->event_time->format('M j, Y H:i:s') }}</span>
|
|
</div>
|
|
|
|
@if($selectedEvent->geofence)
|
|
<div>
|
|
<span class="text-sm font-medium text-gray-600">Geofence:</span>
|
|
<span class="ml-2 text-sm text-gray-900">{{ $selectedEvent->geofence->name }}</span>
|
|
</div>
|
|
@endif
|
|
|
|
@if($selectedEvent->position)
|
|
<div>
|
|
<span class="text-sm font-medium text-gray-600">Location:</span>
|
|
<span class="ml-2 text-sm text-gray-900">
|
|
{{ number_format($selectedEvent->position->latitude, 6) }},
|
|
{{ number_format($selectedEvent->position->longitude, 6) }}
|
|
</span>
|
|
</div>
|
|
@endif
|
|
|
|
<div>
|
|
<span class="text-sm font-medium text-gray-600">Message:</span>
|
|
<p class="mt-1 text-sm text-gray-900">{{ $selectedEvent->message }}</p>
|
|
</div>
|
|
|
|
@if($selectedEvent->attributes && count($selectedEvent->attributes) > 0)
|
|
<div>
|
|
<span class="text-sm font-medium text-gray-600">Additional Data:</span>
|
|
<div class="mt-1 text-xs text-gray-700 bg-gray-50 rounded p-2">
|
|
<pre>{{ json_encode($selectedEvent->attributes, JSON_PRETTY_PRINT) }}</pre>
|
|
</div>
|
|
</div>
|
|
@endif
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="bg-gray-50 px-4 py-3 sm:px-6 sm:flex sm:flex-row-reverse">
|
|
@if(!$selectedEvent->acknowledged)
|
|
<flux:button wire:click="acknowledgeEvent({{ $selectedEvent->id }})" variant="primary" class="sm:ml-3">
|
|
Acknowledge
|
|
</flux:button>
|
|
@endif
|
|
@if($selectedEvent->position)
|
|
<flux:button wire:click="showOnMap({{ $selectedEvent->position->latitude }}, {{ $selectedEvent->position->longitude }})" variant="outline" class="sm:ml-3">
|
|
Show on Map
|
|
</flux:button>
|
|
@endif
|
|
<flux:button wire:click="closeEventDetails" variant="ghost">
|
|
Close
|
|
</flux:button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
@endif
|
|
|
|
|
|
<style>
|
|
.transition-shadow {
|
|
transition: box-shadow 0.15s ease-in-out;
|
|
}
|
|
</style>
|
|
</div> |