367 lines
19 KiB
PHP
367 lines
19 KiB
PHP
<div class="space-y-6" wire:key="device-management">
|
|
{{-- Header --}}
|
|
<div class="flex items-center justify-between">
|
|
<div>
|
|
<flux:heading size="lg">Device Management</flux:heading>
|
|
<flux:subheading>Manage your GPS tracking devices and Traccar integration</flux:subheading>
|
|
</div>
|
|
<div class="flex space-x-3">
|
|
<flux:button wire:click="syncWithTraccar" variant="outline" size="sm" icon="arrow-path">
|
|
Sync with Traccar
|
|
</flux:button>
|
|
<flux:button wire:click="syncUnsyncedDevicesToTraccar" variant="outline" size="sm" icon="cloud-arrow-up">
|
|
Sync Unsynced to Traccar
|
|
</flux:button>
|
|
<flux:button wire:click="createDevice" variant="primary" size="sm" icon="plus">
|
|
Add Device
|
|
</flux:button>
|
|
</div>
|
|
</div>
|
|
|
|
{{-- Flash Messages --}}
|
|
@if (session()->has('message'))
|
|
<div class="bg-green-100 dark:bg-green-900 border border-green-400 dark:border-green-600 text-green-700 dark:text-green-200 px-4 py-3 rounded relative" role="alert">
|
|
<span class="block sm:inline">{{ session('message') }}</span>
|
|
</div>
|
|
@endif
|
|
|
|
@if (session()->has('error'))
|
|
<div class="bg-red-100 dark:bg-red-900 border border-red-400 dark:border-red-600 text-red-700 dark:text-red-200 px-4 py-3 rounded relative" role="alert">
|
|
<span class="block sm:inline">{{ session('error') }}</span>
|
|
</div>
|
|
@endif
|
|
|
|
{{-- Stats Cards --}}
|
|
<div class="grid grid-cols-1 md:grid-cols-4 gap-4">
|
|
<flux:card>
|
|
<div class="text-center">
|
|
<div class="text-2xl font-bold text-blue-600">{{ $this->stats['total_devices'] }}</div>
|
|
<div class="text-sm text-gray-600 dark:text-gray-400">Total Devices</div>
|
|
</div>
|
|
</flux:card>
|
|
<flux:card>
|
|
<div class="text-center">
|
|
<div class="text-2xl font-bold text-green-600">{{ $this->stats['active_devices'] }}</div>
|
|
<div class="text-sm text-gray-600 dark:text-gray-400">Active Devices</div>
|
|
</div>
|
|
</flux:card>
|
|
<flux:card>
|
|
<div class="text-center">
|
|
<div class="text-2xl font-bold text-orange-600">{{ $this->stats['online_devices'] }}</div>
|
|
<div class="text-sm text-gray-600 dark:text-gray-400">Online Now</div>
|
|
</div>
|
|
</flux:card>
|
|
<flux:card>
|
|
<div class="text-center">
|
|
<div class="text-2xl font-bold text-purple-600">{{ $this->stats['with_traccar'] }}</div>
|
|
<div class="text-sm text-gray-600 dark:text-gray-400">Traccar Synced</div>
|
|
</div>
|
|
</flux:card>
|
|
</div>
|
|
|
|
{{-- Filters --}}
|
|
<flux:card>
|
|
<div class="grid grid-cols-1 md:grid-cols-4 gap-4">
|
|
<flux:field>
|
|
<flux:label>Search</flux:label>
|
|
<flux:input
|
|
wire:model.live="search"
|
|
placeholder="Search devices..."
|
|
icon="magnifying-glass"
|
|
/>
|
|
</flux:field>
|
|
|
|
<flux:field>
|
|
<flux:label>Status</flux:label>
|
|
<flux:select wire:model.live="statusFilter">
|
|
<option value="all">All Status</option>
|
|
<option value="online">Online</option>
|
|
<option value="offline">Offline</option>
|
|
<option value="unknown">Unknown</option>
|
|
</flux:select>
|
|
</flux:field>
|
|
|
|
<flux:field>
|
|
<flux:label>Group</flux:label>
|
|
<flux:select wire:model.live="groupFilter">
|
|
<option value="all">All Groups</option>
|
|
@foreach($deviceGroups as $group)
|
|
<option value="{{ $group->id }}">{{ $group->name }}</option>
|
|
@endforeach
|
|
</flux:select>
|
|
</flux:field>
|
|
</div>
|
|
</flux:card>
|
|
|
|
{{-- Device Form Modal --}}
|
|
<flux:modal wire:model.live="showForm" class="md:w-2xl">
|
|
<div class="space-y-6">
|
|
<div>
|
|
<flux:heading size="lg">
|
|
{{ $editingDevice ? 'Edit Device' : 'Add New Device' }}
|
|
</flux:heading>
|
|
<flux:subheading>
|
|
{{ $editingDevice ? 'Update device information and Traccar integration' : 'Add a new GPS tracking device with Traccar integration' }}
|
|
</flux:subheading>
|
|
</div>
|
|
|
|
<form wire:submit="saveDevice" class="space-y-6">
|
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
<flux:field>
|
|
<flux:label>Device Name *</flux:label>
|
|
<flux:input wire:model="name" required />
|
|
<flux:error name="name" />
|
|
</flux:field>
|
|
|
|
<flux:field>
|
|
<flux:label>Unique ID *</flux:label>
|
|
<flux:input wire:model="unique_id" required />
|
|
<flux:error name="unique_id" />
|
|
</flux:field>
|
|
|
|
<flux:field>
|
|
<flux:label>IMEI</flux:label>
|
|
<flux:input wire:model="imei" />
|
|
<flux:error name="imei" />
|
|
</flux:field>
|
|
|
|
<flux:field>
|
|
<flux:label>Phone Number</flux:label>
|
|
<flux:input wire:model="phone" />
|
|
<flux:error name="phone" />
|
|
</flux:field>
|
|
|
|
<flux:field>
|
|
<flux:label>Model</flux:label>
|
|
<flux:input wire:model="model" />
|
|
<flux:error name="model" />
|
|
</flux:field>
|
|
|
|
<flux:field>
|
|
<flux:label>Contact</flux:label>
|
|
<flux:input wire:model="contact" />
|
|
<flux:error name="contact" />
|
|
</flux:field>
|
|
|
|
<flux:field>
|
|
<flux:label>Protocol</flux:label>
|
|
<flux:input wire:model="protocol" placeholder="e.g., osmand, gt06, h02" />
|
|
<flux:error name="protocol" />
|
|
</flux:field>
|
|
|
|
<flux:field>
|
|
<flux:label>Category</flux:label>
|
|
<flux:select wire:model="category">
|
|
<option value="default">Default</option>
|
|
<option value="car">Car</option>
|
|
<option value="truck">Truck</option>
|
|
<option value="motorcycle">Motorcycle</option>
|
|
<option value="person">Person</option>
|
|
<option value="animal">Animal</option>
|
|
</flux:select>
|
|
<flux:error name="category" />
|
|
</flux:field>
|
|
|
|
<flux:field>
|
|
<flux:label>Group</flux:label>
|
|
<flux:select wire:model="group_id">
|
|
<option value="">No Group</option>
|
|
@foreach($deviceGroups as $group)
|
|
<option value="{{ $group->id }}">{{ $group->name }}</option>
|
|
@endforeach
|
|
</flux:select>
|
|
<flux:error name="group_id" />
|
|
</flux:field>
|
|
|
|
<flux:field>
|
|
<flux:label>Driver</flux:label>
|
|
<flux:select wire:model="driver_id">
|
|
<option value="">No Driver</option>
|
|
@foreach($drivers as $driver)
|
|
<option value="{{ $driver->id }}">{{ $driver->name }}</option>
|
|
@endforeach
|
|
</flux:select>
|
|
<flux:error name="driver_id" />
|
|
</flux:field>
|
|
|
|
<flux:field class="md:col-span-2">
|
|
<flux:checkbox wire:model="is_active">
|
|
Device is active
|
|
</flux:checkbox>
|
|
<flux:error name="is_active" />
|
|
</flux:field>
|
|
</div>
|
|
|
|
<flux:field>
|
|
<flux:label>Attributes (JSON)</flux:label>
|
|
<flux:textarea wire:model="deviceAttributes" rows="3" placeholder='{"key": "value"}' />
|
|
<flux:error name="deviceAttributes" />
|
|
</flux:field>
|
|
|
|
<div class="flex gap-2">
|
|
<flux:spacer />
|
|
<flux:modal.close>
|
|
<flux:button variant="ghost">Cancel</flux:button>
|
|
</flux:modal.close>
|
|
<flux:button type="submit" variant="primary">
|
|
{{ $editingDevice ? 'Update Device' : 'Create Device' }}
|
|
</flux:button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</flux:modal> {{-- Devices Table --}}
|
|
<flux:card>
|
|
<div class="overflow-x-auto">
|
|
<table class="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
|
|
<thead class="bg-gray-50 dark:bg-gray-700">
|
|
<tr>
|
|
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">
|
|
Device
|
|
</th>
|
|
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">
|
|
Status
|
|
</th>
|
|
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">
|
|
Location
|
|
</th>
|
|
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">
|
|
Last Update
|
|
</th>
|
|
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">
|
|
Group/Driver
|
|
</th>
|
|
<th class="px-6 py-3 text-right text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">
|
|
Actions
|
|
</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody class="bg-white dark:bg-gray-800 divide-y divide-gray-200 dark:divide-gray-700">
|
|
@forelse($devices as $device)
|
|
<tr>
|
|
<td class="px-6 py-4 whitespace-nowrap">
|
|
<div class="flex items-center space-x-3">
|
|
<div class="flex-shrink-0">
|
|
<div class="h-10 w-10 rounded-full bg-gray-100 dark:bg-gray-700 flex items-center justify-center">
|
|
<flux:icon.device-phone-mobile class="h-5 w-5 text-gray-600 dark:text-gray-400" />
|
|
</div>
|
|
</div>
|
|
<div>
|
|
<div class="text-sm font-medium text-gray-900 dark:text-gray-100">{{ $device->name }}</div>
|
|
<div class="text-sm text-gray-500 dark:text-gray-400">{{ $device->unique_id }}</div>
|
|
@if($device->imei)
|
|
<div class="text-xs text-gray-400 dark:text-gray-500">IMEI: {{ $device->imei }}</div>
|
|
@endif
|
|
</div>
|
|
</div>
|
|
</td>
|
|
<td class="px-6 py-4 whitespace-nowrap">
|
|
<span class="inline-flex items-center px-2 py-1 text-xs font-semibold rounded-full
|
|
@if($device->status === 'online') bg-green-100 dark:bg-green-900 text-green-800 dark:text-green-200
|
|
@elseif($device->status === 'offline') bg-red-100 dark:bg-red-900 text-red-800 dark:text-red-200
|
|
@else bg-gray-100 dark:bg-gray-900 text-gray-800 dark:text-gray-200 @endif">
|
|
{{ ucfirst($device->status) }}
|
|
</span>
|
|
@if($device->is_active)
|
|
<div class="mt-1">
|
|
<span class="inline-flex px-2 py-1 text-xs font-semibold rounded-full bg-green-100 dark:bg-green-900 text-green-800 dark:text-green-200">
|
|
Active
|
|
</span>
|
|
</div>
|
|
@else
|
|
<div class="mt-1">
|
|
<span class="inline-flex px-2 py-1 text-xs font-semibold rounded-full bg-gray-100 dark:bg-gray-900 text-gray-800 dark:text-gray-200">
|
|
Inactive
|
|
</span>
|
|
</div>
|
|
@endif
|
|
</td>
|
|
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-gray-100">
|
|
@if($device->currentPosition)
|
|
<div>{{ $device->currentPosition->address ?? 'Unknown' }}</div>
|
|
<div class="text-xs text-gray-500 dark:text-gray-400">{{ $device->currentPosition->getCoordinates() }}</div>
|
|
@else
|
|
<span class="text-gray-400 dark:text-gray-500">No position</span>
|
|
@endif
|
|
</td>
|
|
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-gray-400">
|
|
@if($device->last_update)
|
|
{{ $device->last_update->diffForHumans() }}
|
|
@else
|
|
Never
|
|
@endif
|
|
</td>
|
|
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-gray-100">
|
|
<div class="space-y-1">
|
|
@if($device->group)
|
|
<div class="text-sm text-blue-600 dark:text-blue-400">
|
|
<flux:icon.folder class="h-3 w-3 inline mr-1" />
|
|
{{ $device->group->name }}
|
|
</div>
|
|
@endif
|
|
@if($device->driver)
|
|
<div class="text-sm text-green-600 dark:text-green-400">
|
|
<flux:icon.user class="h-3 w-3 inline mr-1" />
|
|
{{ $device->driver->name }}
|
|
</div>
|
|
@endif
|
|
@if($device->protocol)
|
|
<div class="text-xs text-gray-500 dark:text-gray-400">{{ $device->protocol }}</div>
|
|
@endif
|
|
@if($device->traccar_device_id)
|
|
<div class="text-xs text-purple-600 dark:text-purple-400">
|
|
<flux:icon.link class="h-3 w-3 inline mr-1" />
|
|
Traccar Synced
|
|
</div>
|
|
@else
|
|
<div class="text-xs text-gray-400 dark:text-gray-500">
|
|
<flux:icon.exclamation-triangle class="h-3 w-3 inline mr-1" />
|
|
Local Only
|
|
</div>
|
|
@endif
|
|
</div>
|
|
</td>
|
|
<td class="px-6 py-4 whitespace-nowrap text-right text-sm font-medium space-x-2">
|
|
<flux:button
|
|
wire:click="editDevice({{ $device->id }})"
|
|
variant="outline"
|
|
size="sm">
|
|
<flux:icon.pencil class="size-4" />
|
|
</flux:button>
|
|
<flux:button
|
|
wire:click="deleteDevice({{ $device->id }})"
|
|
wire:confirm="Are you sure you want to delete this device? This will also remove it from Traccar."
|
|
variant="danger"
|
|
size="sm">
|
|
<flux:icon.trash class="size-4" />
|
|
</flux:button>
|
|
</td>
|
|
</tr>
|
|
@empty
|
|
<tr>
|
|
<td colspan="6" class="px-6 py-12 text-center">
|
|
<flux:icon.device-phone-mobile class="mx-auto h-12 w-12 text-gray-400 dark:text-gray-500" />
|
|
<h3 class="mt-2 text-sm font-medium text-gray-900 dark:text-gray-100">No devices found</h3>
|
|
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">Get started by adding your first device or syncing with Traccar.</p>
|
|
<div class="mt-6 flex justify-center space-x-3">
|
|
<flux:button wire:click="createDevice" icon="plus">
|
|
Add Device
|
|
</flux:button>
|
|
<flux:button wire:click="syncWithTraccar" variant="outline" icon="arrow-path">
|
|
Sync with Traccar
|
|
</flux:button>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
@endforelse
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
|
|
@if($devices->hasPages())
|
|
<div class="px-6 py-4 border-t border-gray-200 dark:border-gray-700">
|
|
{{ $devices->links() }}
|
|
</div>
|
|
@endif
|
|
</flux:card>
|
|
</div>
|