gps_system/resources/views/livewire/geofence-management.blade.php
sackey 6b878bb0a0
Some checks failed
linter / quality (push) Has been cancelled
tests / ci (push) Has been cancelled
Initial commit
2025-09-12 16:19:56 +00:00

500 lines
23 KiB
PHP

<div class="space-y-6" wire:key="geofence-component">
{{-- Page Header --}}
<div class="flex items-center justify-between">
<div>
<flux:heading size="lg">Geofence Management</flux:heading>
<flux:subheading>Create and manage geographical boundaries</flux:subheading>
</div>
<div class="flex space-x-3">
<flux:button wire:click="showGeofenceModal" variant="primary" size="sm" icon="plus">
Create Geofence
</flux:button>
<flux:button wire:click="refreshGeofences" variant="ghost" size="sm" icon="arrow-path">
Refresh
</flux:button>
</div>
</div>
<div class="grid grid-cols-1 lg:grid-cols-4 gap-6">
{{-- Geofence List Panel --}}
<div class="lg:col-span-1">
<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">Geofences</flux:heading>
<div class="space-y-3 max-h-96 overflow-y-auto">
@foreach($geofences as $geofence)
<div class="p-3 rounded-lg border border-gray-200 hover:border-blue-300 transition-colors">
<div class="flex items-center justify-between mb-2">
<span class="font-medium text-sm">{{ $geofence->name }}</span>
<div class="flex space-x-1">
<flux:button wire:click="selectGeofence({{ $geofence->id }})"
variant="{{ $selectedGeofence === $geofence->id ? 'primary' : 'ghost' }}"
size="xs" icon="eye">
</flux:button>
<flux:button wire:click="editGeofence({{ $geofence->id }})"
variant="ghost" size="xs" icon="pencil">
</flux:button>
<flux:button wire:click="deleteGeofence({{ $geofence->id }})"
variant="ghost" size="xs" icon="trash"
wire:confirm="Are you sure you want to delete this geofence?">
</flux:button>
</div>
</div>
<div class="text-xs text-gray-500 space-y-1">
<div>Type: {{ ucfirst($geofence->type) }}</div>
@if($geofence->description)
<div>{{ Str::limit($geofence->description, 50) }}</div>
@endif
<div class="flex items-center space-x-2">
<span class="inline-flex items-center px-2 py-1 rounded-full text-xs font-medium
{{ $geofence->is_active ? 'bg-green-100 text-green-800' : 'bg-red-100 text-red-800' }}">
{{ $geofence->is_active ? 'Active' : 'Inactive' }}
</span>
</div>
</div>
{{-- Assigned Devices --}}
@if($geofence->devices->count() > 0)
<div class="mt-2 pt-2 border-t border-gray-100">
<div class="text-xs text-gray-500">
{{ $geofence->devices->count() }} device(s) assigned
</div>
</div>
@endif
</div>
@endforeach
@if($geofences->isEmpty())
<div class="text-center py-6 text-gray-500">
<div class="text-sm">No geofences created yet</div>
<flux:button wire:click="showGeofenceModal" variant="ghost" size="sm" class="mt-2">
Create your first geofence
</flux:button>
</div>
@endif
</div>
</div>
</div>
{{-- Drawing Tools --}}
<div class="bg-white shadow rounded-lg border border-zinc-200 mt-4">
<div class="px-4 py-5 sm:p-6">
<flux:heading size="base" class="mb-4">Drawing Tools</flux:heading>
<div class="space-y-3">
<flux:button id="drawPolygonBtn" variant="outline" size="sm" class="w-full" icon="square-3-stack-3d">
Draw Polygon
</flux:button>
<flux:button id="drawCircleBtn" variant="outline" size="sm" class="w-full" icon="stop">
Draw Circle
</flux:button>
<flux:button id="drawRectangleBtn" variant="outline" size="sm" class="w-full" icon="stop">
Draw Rectangle
</flux:button>
<flux:button id="editGeofenceBtn" variant="outline" size="sm" class="w-full" icon="pencil" disabled>
Edit Selected
</flux:button>
<flux:button id="deleteDrawnBtn" variant="outline" size="sm" class="w-full" icon="trash" disabled>
Delete Drawn
</flux:button>
<flux:button id="clearAllBtn" variant="ghost" size="sm" class="w-full" icon="x-mark">
Clear All
</flux:button>
</div>
</div>
</div>
</div>
{{-- Map Area --}}
<div class="lg:col-span-3">
<div class="bg-white shadow rounded-lg border border-zinc-200 h-[600px] relative">
<div id="geofenceMap" class="w-full h-full rounded-lg"></div>
{{-- Map Instructions --}}
<div class="absolute top-4 left-4 bg-white shadow-lg rounded-lg p-3 border border-zinc-200 max-w-xs">
<flux:heading size="sm" class="mb-2">Instructions</flux:heading>
<div class="text-xs text-gray-600 space-y-1">
<div> Use drawing tools to create geofences</div>
<div> Click on existing geofences to select them</div>
<div> Drag corners to edit shapes</div>
<div> Right-click to finish drawing</div>
</div>
</div>
</div>
</div>
</div>
{{-- Create/Edit Geofence Modal --}}
@if($showModal)
<div class="fixed inset-0 z-50 overflow-y-auto" wire:key="geofence-modal">
<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="closeModal"></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">
<form wire:submit="saveGeofence">
<div class="bg-white px-4 pt-5 pb-4 sm:p-6 sm:pb-4">
<div class="space-y-4">
<flux:heading size="lg">{{ $editingGeofence ? 'Edit' : 'Create' }} Geofence</flux:heading>
<div>
<flux:field>
<flux:label>Name</flux:label>
<flux:input wire:model="geofenceName" placeholder="Enter geofence name" required />
<flux:error name="geofenceName" />
</flux:field>
</div>
<div>
<flux:field>
<flux:label>Description</flux:label>
<flux:textarea wire:model="geofenceDescription" placeholder="Optional description" rows="3" />
<flux:error name="geofenceDescription" />
</flux:field>
</div>
<div>
<flux:field>
<flux:label>Type</flux:label>
<flux:select wire:model.live="geofenceType" required>
<option value="">Select type</option>
<option value="entry">Entry Alert</option>
<option value="exit">Exit Alert</option>
<option value="both">Entry & Exit Alert</option>
<option value="zone">Safe Zone</option>
</flux:select>
<flux:error name="geofenceType" />
</flux:field>
</div>
<div>
<flux:field>
<flux:label>Assign Devices</flux:label>
<div class="space-y-2 max-h-32 overflow-y-auto border border-gray-200 rounded-md p-2">
@foreach($devices as $device)
<label class="flex items-center space-x-2">
<input type="checkbox"
wire:model="selectedDevicesForGeofence"
value="{{ $device->id }}"
class="rounded border-gray-300 text-blue-600 focus:ring-blue-500">
<span class="text-sm">{{ $device->name }}</span>
</label>
@endforeach
</div>
<flux:error name="selectedDevicesForGeofence" />
</flux:field>
</div>
<div class="flex items-center space-x-2">
<input type="checkbox"
wire:model="geofenceActive"
class="rounded border-gray-300 text-blue-600 focus:ring-blue-500">
<flux:label>Active</flux:label>
</div>
</div>
</div>
<div class="bg-gray-50 px-4 py-3 sm:px-6 sm:flex sm:flex-row-reverse">
<flux:button type="submit" variant="primary" class="sm:ml-3">
{{ $editingGeofence ? 'Update' : 'Create' }}
</flux:button>
<flux:button type="button" wire:click="closeModal" variant="ghost">
Cancel
</flux:button>
</div>
</form>
</div>
</div>
</div>
@endif
{{-- Load Leaflet and Leaflet.draw Assets --}}
@assets
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css"
integrity="sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY="
crossorigin=""/>
<link rel="stylesheet" href="https://unpkg.com/leaflet-draw@1.0.4/dist/leaflet.draw.css"/>
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"
integrity="sha256-20nQCchB9co0qIjJZRGuk2/Z9VM+kNiyxNV1lvTlZBo="
crossorigin=""></script>
<script src="https://unpkg.com/leaflet-draw@1.0.4/dist/leaflet.draw.js"></script>
@endassets
@script
<script>
let geofenceMap = null;
let drawnItems = null;
let drawControl = null;
let geofencePolygons = {};
let currentDrawnShape = null;
// Initialize the map
function initGeofenceMap() {
if (geofenceMap) {
geofenceMap.remove();
}
const mapCenter = @json($mapCenter);
const zoomLevel = @json($zoomLevel);
geofenceMap = L.map('geofenceMap').setView([mapCenter.lat, mapCenter.lng], zoomLevel);
// Add tile layer
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors',
maxZoom: 19
}).addTo(geofenceMap);
// Initialize draw control
drawnItems = new L.FeatureGroup();
geofenceMap.addLayer(drawnItems);
// Setup custom drawing controls
setupDrawingControls();
// Load existing geofences
loadGeofences();
// Setup event listeners
setupMapEvents();
}
function setupDrawingControls() {
// Polygon drawing
document.getElementById('drawPolygonBtn').addEventListener('click', function() {
if (currentDrawnShape) {
drawnItems.removeLayer(currentDrawnShape);
}
startPolygonDraw();
});
// Circle drawing
document.getElementById('drawCircleBtn').addEventListener('click', function() {
if (currentDrawnShape) {
drawnItems.removeLayer(currentDrawnShape);
}
startCircleDraw();
});
// Rectangle drawing
document.getElementById('drawRectangleBtn').addEventListener('click', function() {
if (currentDrawnShape) {
drawnItems.removeLayer(currentDrawnShape);
}
startRectangleDraw();
});
// Clear all
document.getElementById('clearAllBtn').addEventListener('click', function() {
drawnItems.clearLayers();
currentDrawnShape = null;
updateDrawButtons();
});
// Delete drawn shape
document.getElementById('deleteDrawnBtn').addEventListener('click', function() {
if (currentDrawnShape) {
drawnItems.removeLayer(currentDrawnShape);
currentDrawnShape = null;
updateDrawButtons();
}
});
}
function startPolygonDraw() {
const polygon = new L.Draw.Polygon(geofenceMap, {
shapeOptions: {
color: '#3b82f6',
fillColor: '#3b82f6',
fillOpacity: 0.2,
weight: 2
}
});
polygon.enable();
}
function startCircleDraw() {
const circle = new L.Draw.Circle(geofenceMap, {
shapeOptions: {
color: '#10b981',
fillColor: '#10b981',
fillOpacity: 0.2,
weight: 2
}
});
circle.enable();
}
function startRectangleDraw() {
const rectangle = new L.Draw.Rectangle(geofenceMap, {
shapeOptions: {
color: '#f59e0b',
fillColor: '#f59e0b',
fillOpacity: 0.2,
weight: 2
}
});
rectangle.enable();
}
function setupMapEvents() {
// Handle shape creation
geofenceMap.on(L.Draw.Event.CREATED, function(e) {
const layer = e.layer;
if (currentDrawnShape) {
drawnItems.removeLayer(currentDrawnShape);
}
drawnItems.addLayer(layer);
currentDrawnShape = layer;
updateDrawButtons();
// Auto-open modal with shape data
const coordinates = getShapeCoordinates(layer);
$wire.set('tempCoordinates', coordinates);
$wire.showGeofenceModal();
});
// Handle shape editing
geofenceMap.on(L.Draw.Event.EDITED, function(e) {
e.layers.eachLayer(function(layer) {
const coordinates = getShapeCoordinates(layer);
if (layer === currentDrawnShape) {
$wire.set('tempCoordinates', coordinates);
}
});
});
}
function getShapeCoordinates(layer) {
if (layer instanceof L.Circle) {
const center = layer.getLatLng();
return {
type: 'circle',
center: [center.lat, center.lng],
radius: layer.getRadius()
};
} else if (layer instanceof L.Polygon || layer instanceof L.Rectangle) {
const latLngs = layer.getLatLngs()[0];
return {
type: 'polygon',
coordinates: latLngs.map(latlng => [latlng.lat, latlng.lng])
};
}
return null;
}
function loadGeofences() {
const geofences = @json($geofences);
Object.values(geofencePolygons).forEach(polygon => {
geofenceMap.removeLayer(polygon);
});
geofencePolygons = {};
geofences.forEach(geofence => {
if (geofence.coordinates) {
let layer;
const coords = JSON.parse(geofence.coordinates);
if (coords.type === 'circle') {
layer = L.circle([coords.center[0], coords.center[1]], {
radius: coords.radius,
color: geofence.is_active ? '#10b981' : '#6b7280',
fillColor: geofence.is_active ? '#10b981' : '#6b7280',
fillOpacity: 0.1,
weight: 2
});
} else if (coords.type === 'polygon') {
layer = L.polygon(coords.coordinates, {
color: geofence.is_active ? '#3b82f6' : '#6b7280',
fillColor: geofence.is_active ? '#3b82f6' : '#6b7280',
fillOpacity: 0.1,
weight: 2
});
}
if (layer) {
layer.bindPopup(`
<div class="p-2">
<h4 class="font-semibold">${geofence.name}</h4>
<p class="text-sm text-gray-600">${geofence.description || ''}</p>
<p class="text-xs text-gray-500">Type: ${geofence.type}</p>
<p class="text-xs text-gray-500">Status: ${geofence.is_active ? 'Active' : 'Inactive'}</p>
</div>
`);
layer.on('click', function() {
$wire.selectGeofence(geofence.id);
});
layer.addTo(geofenceMap);
geofencePolygons[geofence.id] = layer;
}
}
});
}
function updateDrawButtons() {
const editBtn = document.getElementById('editGeofenceBtn');
const deleteBtn = document.getElementById('deleteDrawnBtn');
if (currentDrawnShape) {
editBtn.disabled = false;
editBtn.classList.remove('opacity-50');
deleteBtn.disabled = false;
deleteBtn.classList.remove('opacity-50');
} else {
editBtn.disabled = true;
editBtn.classList.add('opacity-50');
deleteBtn.disabled = true;
deleteBtn.classList.add('opacity-50');
}
}
// Initialize map when component loads
initGeofenceMap();
// Listen for Livewire events
$wire.on('geofencesUpdated', () => {
loadGeofences();
});
$wire.on('clearDrawnShape', () => {
if (currentDrawnShape) {
drawnItems.removeLayer(currentDrawnShape);
currentDrawnShape = null;
updateDrawButtons();
}
});
// Cleanup when component is destroyed
document.addEventListener('livewire:navigate', function() {
if (geofenceMap) {
geofenceMap.remove();
geofenceMap = null;
}
});
</script>
@endscript
<style>
.leaflet-draw-toolbar a {
background-image: url('https://unpkg.com/leaflet-draw@1.0.4/dist/images/spritesheet.png');
}
.leaflet-retina .leaflet-draw-toolbar a {
background-image: url('https://unpkg.com/leaflet-draw@1.0.4/dist/images/spritesheet-2x.png');
}
#geofenceMap {
z-index: 1;
}
</style>
</div>