diff --git a/app/Http/Controllers/Api/V1/Admin/AdminDropOffPointController.php b/app/Http/Controllers/Api/V1/Admin/AdminDropOffPointController.php index c3dfd88..b24d61a 100644 --- a/app/Http/Controllers/Api/V1/Admin/AdminDropOffPointController.php +++ b/app/Http/Controllers/Api/V1/Admin/AdminDropOffPointController.php @@ -14,7 +14,9 @@ use App\Services\DropOff\DropOffPointFinder; use Illuminate\Support\Facades\DB; use Illuminate\Http\JsonResponse; use Illuminate\Http\Request; +use MatanYadaev\EloquentSpatial\Objects\LineString; use MatanYadaev\EloquentSpatial\Objects\Point; +use MatanYadaev\EloquentSpatial\Objects\Polygon; class AdminDropOffPointController extends ApiController { @@ -73,6 +75,7 @@ class AdminDropOffPointController extends ApiController 'photo_path' => $data['photo_path'] ?? null, 'contact_person' => $data['contact_person'] ?? null, 'contact_phone' => $data['contact_phone'] ?? null, + 'geofence' => !empty($data['geofence_wkt']) ? Polygon::fromJson($data['geofence_wkt'], 4326) : null, ]); return $this->created(new DropOffPointResource($dop->load('barangay')), 'Drop-off point created'); @@ -100,6 +103,11 @@ class AdminDropOffPointController extends ApiController } unset($data['lat'], $data['lng']); + if (array_key_exists('geofence_wkt', $data)) { + $data['geofence'] = !empty($data['geofence_wkt']) ? Polygon::fromJson($data['geofence_wkt'], 4326) : null; + unset($data['geofence_wkt']); + } + DB::transaction(function () use ($dropOffPoint, $data, $oldStatus, $newStatus) { $dropOffPoint->update($data); diff --git a/app/Http/Controllers/Api/V1/Admin/AdminDumpsiteController.php b/app/Http/Controllers/Api/V1/Admin/AdminDumpsiteController.php index 842efda..0d7c10c 100644 --- a/app/Http/Controllers/Api/V1/Admin/AdminDumpsiteController.php +++ b/app/Http/Controllers/Api/V1/Admin/AdminDumpsiteController.php @@ -148,10 +148,8 @@ class AdminDumpsiteController extends ApiController $payload['coordinates'] = new Point((float) $data['lat'], (float) $data['lng'], 4326); } - if (array_key_exists('boundary_polygon', $data)) { - $payload['boundary_polygon'] = $data['boundary_polygon'] - ? $this->buildPolygon($data['boundary_polygon']) - : null; + if (array_key_exists('geofence_wkt', $data)) { + $payload['boundary_polygon'] = !empty($data['geofence_wkt']) ? \MatanYadaev\EloquentSpatial\Objects\Polygon::fromJson($data['geofence_wkt'], 4326) : null; } return $payload; diff --git a/app/Http/Requests/DropOff/StoreDropOffPointRequest.php b/app/Http/Requests/DropOff/StoreDropOffPointRequest.php index 04c9a02..5e8715c 100644 --- a/app/Http/Requests/DropOff/StoreDropOffPointRequest.php +++ b/app/Http/Requests/DropOff/StoreDropOffPointRequest.php @@ -21,6 +21,7 @@ class StoreDropOffPointRequest extends FormRequest 'barangay_id' => ['nullable', 'integer', 'exists:barangays,id'], 'lat' => ['required', 'numeric', 'between:-90,90'], 'lng' => ['required', 'numeric', 'between:-180,180'], + 'geofence_wkt' => ['nullable', 'string'], 'address_line' => ['required', 'string', 'max:255'], 'capacity_kg' => ['nullable', 'integer', 'min:0'], 'operating_hours' => ['nullable', 'array'], diff --git a/app/Http/Requests/DropOff/UpdateDropOffPointRequest.php b/app/Http/Requests/DropOff/UpdateDropOffPointRequest.php index 93d00c9..d5e6201 100644 --- a/app/Http/Requests/DropOff/UpdateDropOffPointRequest.php +++ b/app/Http/Requests/DropOff/UpdateDropOffPointRequest.php @@ -26,6 +26,7 @@ class UpdateDropOffPointRequest extends FormRequest 'barangay_id' => ['sometimes', 'nullable', 'integer', 'exists:barangays,id'], 'lat' => ['sometimes', 'required_with:lng', 'numeric', 'between:-90,90'], 'lng' => ['sometimes', 'required_with:lat', 'numeric', 'between:-180,180'], + 'geofence_wkt' => ['sometimes', 'nullable', 'string'], 'address_line' => ['sometimes', 'required', 'string', 'max:255'], 'capacity_kg' => ['sometimes', 'nullable', 'integer', 'min:0'], 'operating_hours' => ['sometimes', 'nullable', 'array'], diff --git a/app/Http/Requests/Dumpsite/StoreDumpsiteRequest.php b/app/Http/Requests/Dumpsite/StoreDumpsiteRequest.php index 1363567..1047fe9 100644 --- a/app/Http/Requests/Dumpsite/StoreDumpsiteRequest.php +++ b/app/Http/Requests/Dumpsite/StoreDumpsiteRequest.php @@ -22,9 +22,7 @@ class StoreDumpsiteRequest extends FormRequest 'city_municipality_id' => ['nullable', 'integer', 'exists:cities_municipalities,id'], 'lat' => ['required', 'numeric', 'between:-90,90'], 'lng' => ['required', 'numeric', 'between:-180,180'], - 'boundary_polygon' => ['nullable', 'array', 'min:3'], - 'boundary_polygon.*.lat' => ['required_with:boundary_polygon', 'numeric', 'between:-90,90'], - 'boundary_polygon.*.lng' => ['required_with:boundary_polygon', 'numeric', 'between:-180,180'], + 'geofence_wkt' => ['nullable', 'string'], 'accepted_waste_types' => ['nullable', 'array'], 'accepted_waste_types.*' => ['string', 'max:50'], 'operating_hours' => ['nullable', 'array'], diff --git a/app/Http/Requests/Dumpsite/UpdateDumpsiteRequest.php b/app/Http/Requests/Dumpsite/UpdateDumpsiteRequest.php index 6907989..40daf9a 100644 --- a/app/Http/Requests/Dumpsite/UpdateDumpsiteRequest.php +++ b/app/Http/Requests/Dumpsite/UpdateDumpsiteRequest.php @@ -27,9 +27,7 @@ class UpdateDumpsiteRequest extends FormRequest 'city_municipality_id' => ['sometimes', 'nullable', 'integer', 'exists:cities_municipalities,id'], 'lat' => ['sometimes', 'required_with:lng', 'numeric', 'between:-90,90'], 'lng' => ['sometimes', 'required_with:lat', 'numeric', 'between:-180,180'], - 'boundary_polygon' => ['sometimes', 'nullable', 'array', 'min:3'], - 'boundary_polygon.*.lat' => ['required_with:boundary_polygon', 'numeric', 'between:-90,90'], - 'boundary_polygon.*.lng' => ['required_with:boundary_polygon', 'numeric', 'between:-180,180'], + 'geofence_wkt' => ['sometimes', 'nullable', 'string'], 'accepted_waste_types' => ['sometimes', 'nullable', 'array'], 'operating_hours' => ['sometimes', 'nullable', 'array'], 'capacity_tons' => ['sometimes', 'nullable', 'integer', 'min:0'], diff --git a/app/Http/Resources/DropOffPointResource.php b/app/Http/Resources/DropOffPointResource.php index 3404b9e..64c52b5 100644 --- a/app/Http/Resources/DropOffPointResource.php +++ b/app/Http/Resources/DropOffPointResource.php @@ -19,6 +19,7 @@ class DropOffPointResource extends JsonResource 'lat' => $this->coordinates->latitude, 'lng' => $this->coordinates->longitude, ] : null, + 'geofence' => $this->geofence, 'capacity_kg' => $this->capacity_kg, 'operating_hours' => $this->operating_hours, 'accepted_waste_types' => $this->accepted_waste_types, diff --git a/app/Models/DropOffPoint.php b/app/Models/DropOffPoint.php index f3b643a..5eab5e0 100644 --- a/app/Models/DropOffPoint.php +++ b/app/Models/DropOffPoint.php @@ -29,6 +29,7 @@ class DropOffPoint extends Model 'code', 'barangay_id', 'coordinates', + 'geofence', 'address_line', 'capacity_kg', 'operating_hours', @@ -43,6 +44,7 @@ class DropOffPoint extends Model { return [ 'coordinates' => Point::class, + 'geofence' => \MatanYadaev\EloquentSpatial\Objects\Polygon::class, 'operating_hours' => 'array', 'accepted_waste_types' => 'array', 'capacity_kg' => 'integer', diff --git a/database/migrations/2026_06_30_052044_add_geofence_to_facilities_tables.php b/database/migrations/2026_06_30_052044_add_geofence_to_facilities_tables.php new file mode 100644 index 0000000..f953449 --- /dev/null +++ b/database/migrations/2026_06_30_052044_add_geofence_to_facilities_tables.php @@ -0,0 +1,36 @@ +geometry('geofence')->nullable()->after('coordinates'); + }); + + Schema::table('dumpsites', function (Blueprint $table) { + $table->geometry('geofence')->nullable()->after('coordinates'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('drop_off_points', function (Blueprint $table) { + $table->dropColumn('geofence'); + }); + + Schema::table('dumpsites', function (Blueprint $table) { + $table->dropColumn('geofence'); + }); + } +}; diff --git a/resources/views/admin/drop-off-points.blade.php b/resources/views/admin/drop-off-points.blade.php index c61394f..3613836 100644 --- a/resources/views/admin/drop-off-points.blade.php +++ b/resources/views/admin/drop-off-points.blade.php @@ -2,7 +2,9 @@ @section('page') + +
@@ -53,6 +55,7 @@
+
@@ -167,7 +170,7 @@ lngInput.value = lng.toFixed(6); modal.classList.remove('hidden'); - await initMap(lat, lng); + await initMap(lat, lng, d.geofence); } async function load() { @@ -216,15 +219,34 @@ let map = null; let marker = null; + let geofenceLayer = null; const latInput = document.getElementById('dop-lat'); const lngInput = document.getElementById('dop-lng'); + const geofenceInput = document.getElementById('geofence_wkt'); - async function initMap(initialLat = 14.6539, initialLng = 121.0685) { + function updateGeofenceInput(layer) { + if (!layer) { + geofenceInput.value = ''; + return; + } + geofenceInput.value = JSON.stringify(layer.toGeoJSON().geometry); + } + + async function initMap(initialLat = 14.6539, initialLng = 121.0685, geofenceObj = null) { if (map) { if (marker) { marker.setLatLng([initialLat, initialLng]); } map.setView([initialLat, initialLng], 13); + + if (geofenceLayer) { + map.removeLayer(geofenceLayer); + geofenceLayer = null; + } + if (geofenceObj) { + geofenceLayer = L.geoJSON(geofenceObj).addTo(map); + } + setTimeout(() => map.invalidateSize(), 50); return; } @@ -250,6 +272,43 @@ lngInput.value = e.latlng.lng.toFixed(6); }); + map.pm.addControls({ + position: 'topleft', + drawCircle: false, + drawMarker: false, + drawCircleMarker: false, + drawPolyline: false, + drawRectangle: true, + drawText: false, + cutPolygon: false, + editMode: true, + dragMode: true, + removalMode: true + }); + + map.on('pm:create', (e) => { + if (geofenceLayer) { + map.removeLayer(geofenceLayer); + } + geofenceLayer = e.layer; + + e.layer.on('pm:edit', (e) => updateGeofenceInput(e.layer)); + e.layer.on('pm:remove', () => updateGeofenceInput(null)); + + updateGeofenceInput(geofenceLayer); + }); + + map.on('pm:remove', (e) => { + if (e.layer === geofenceLayer) { + geofenceLayer = null; + updateGeofenceInput(null); + } + }); + + if (geofenceObj) { + geofenceLayer = L.geoJSON(geofenceObj).addTo(map); + } + // Setup geocoding search const searchInput = document.getElementById('dop-search'); const searchBtn = document.getElementById('dop-search-btn'); @@ -348,8 +407,9 @@ latInput.value = '14.653900'; lngInput.value = '121.068500'; + geofenceInput.value = ''; modal.classList.remove('hidden'); - initMap(14.6539, 121.0685); + initMap(14.6539, 121.0685, null); }); modal.querySelectorAll('[data-close]').forEach(el => el.addEventListener('click', () => modal.classList.add('hidden'))); diff --git a/resources/views/admin/dumpsites.blade.php b/resources/views/admin/dumpsites.blade.php index ba03234..bd7f8f1 100644 --- a/resources/views/admin/dumpsites.blade.php +++ b/resources/views/admin/dumpsites.blade.php @@ -2,7 +2,9 @@ @section('page') + +
@@ -43,6 +45,7 @@
+
@@ -65,14 +68,7 @@

Click anywhere on the map or drag the marker to auto-fill coordinates.

-
-
- - -
-

At least 3 points. Ring auto-closes.

-
-
+
@@ -102,7 +98,6 @@