@fleetbase/fleetops-engine 0.6.43 → 0.6.45
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/addon/components/entity/form.hbs +24 -0
- package/addon/components/entity/form.js +30 -0
- package/addon/components/leaflet-draw-control.js +1 -1
- package/addon/components/map/drawer/geofence-event-listing.hbs +123 -0
- package/addon/components/map/drawer/geofence-event-listing.js +103 -0
- package/addon/components/map/drawer.js +1 -0
- package/addon/components/map/leaflet-live-map.js +83 -0
- package/addon/components/modals/orchestrator-import.js +2 -78
- package/addon/components/order/form/service-rate.hbs +1 -1
- package/addon/components/service-area/form.hbs +59 -0
- package/addon/components/service-rate/details.hbs +6 -6
- package/addon/components/service-rate/form.hbs +28 -15
- package/addon/components/zone/form.hbs +60 -1
- package/addon/controllers/operations/orders/index/details/virtual.js +5 -0
- package/addon/controllers/operations/orders/index/details.js +27 -2
- package/addon/controllers/operations/service-rates/index/edit.js +1 -0
- package/addon/controllers/operations/service-rates/index/new.js +1 -0
- package/addon/helpers/f-to-int.js +6 -0
- package/addon/routes/application.js +3 -3
- package/addon/routes/operations/orders/index/details/virtual.js +1 -0
- package/addon/routes/operations/orders/index/details.js +19 -3
- package/addon/services/equipment-actions.js +4 -6
- package/addon/services/geofence-event-bus.js +223 -0
- package/addon/services/leaflet-map-manager.js +4 -0
- package/addon/services/maintenance-actions.js +4 -6
- package/addon/services/part-actions.js +4 -6
- package/addon/services/service-rate-actions.js +8 -3
- package/addon/templates/operations/orders/index/details/virtual.hbs +7 -1
- package/addon/utils/fleet-ops-options.js +33 -0
- package/addon/utils/serialize-payload.js +1 -1
- package/app/components/map/drawer/geofence-event-listing.js +1 -0
- package/app/controllers/operations/orders/index/details/virtual.js +1 -0
- package/app/helpers/f-to-int.js +1 -0
- package/app/services/geofence-event-bus.js +1 -0
- package/composer.json +1 -1
- package/extension.json +1 -1
- package/package.json +2 -2
- package/server/migrations/2026_04_05_000001_add_geofence_config_to_zones_table.php +48 -0
- package/server/migrations/2026_04_05_000002_add_geofence_config_to_service_areas_table.php +38 -0
- package/server/migrations/2026_04_05_000003_create_driver_geofence_states_table.php +62 -0
- package/server/migrations/2026_04_05_000004_create_geofence_events_log_table.php +66 -0
- package/server/migrations/2026_04_22_000001_add_vehicle_support_to_geofence_events_log.php +31 -0
- package/server/migrations/2026_04_22_000002_create_vehicle_geofence_states_table.php +39 -0
- package/server/src/Console/Commands/SimulateGeofenceEvents.php +278 -0
- package/server/src/Events/GeofenceDwelled.php +208 -0
- package/server/src/Events/GeofenceEntered.php +207 -0
- package/server/src/Events/GeofenceExited.php +213 -0
- package/server/src/Http/Controllers/Api/v1/DriverController.php +100 -0
- package/server/src/Http/Controllers/Api/v1/GeofenceController.php +263 -0
- package/server/src/Http/Controllers/Api/v1/OrderController.php +13 -8
- package/server/src/Http/Controllers/Api/v1/PurchaseRateController.php +11 -3
- package/server/src/Http/Controllers/Api/v1/ServiceAreaController.php +2 -2
- package/server/src/Http/Controllers/Api/v1/ServiceQuoteController.php +4 -0
- package/server/src/Http/Controllers/Api/v1/ServiceRateController.php +6 -1
- package/server/src/Http/Controllers/Api/v1/VehicleController.php +88 -0
- package/server/src/Http/Controllers/Api/v1/ZoneController.php +2 -2
- package/server/src/Http/Controllers/Internal/v1/GeofenceController.php +7 -0
- package/server/src/Http/Requests/CreateServiceAreaRequest.php +8 -4
- package/server/src/Http/Requests/CreateServiceRateRequest.php +2 -2
- package/server/src/Http/Requests/CreateZoneRequest.php +8 -4
- package/server/src/Http/Resources/v1/Order.php +0 -1
- package/server/src/Http/Resources/v1/ServiceQuote.php +2 -2
- package/server/src/Jobs/CheckGeofenceDwell.php +139 -0
- package/server/src/Listeners/HandleGeofenceDwelled.php +56 -0
- package/server/src/Listeners/HandleGeofenceEntered.php +187 -0
- package/server/src/Listeners/HandleGeofenceExited.php +56 -0
- package/server/src/Models/Entity.php +7 -0
- package/server/src/Models/GeofenceEventLog.php +155 -0
- package/server/src/Models/Order.php +39 -7
- package/server/src/Models/ServiceArea.php +21 -2
- package/server/src/Models/ServiceQuote.php +3 -1
- package/server/src/Models/ServiceRate.php +122 -30
- package/server/src/Models/Zone.php +20 -2
- package/server/src/Notifications/DriverArrivedAtGeofence.php +86 -0
- package/server/src/Observers/PurchaseRateObserver.php +3 -5
- package/server/src/Providers/EventServiceProvider.php +19 -0
- package/server/src/Providers/FleetOpsServiceProvider.php +10 -0
- package/server/src/Rules/ComputableAlgo.php +1 -3
- package/server/src/Support/Algo.php +87 -1
- package/server/src/Support/GeofenceIntersectionService.php +206 -0
- package/server/src/Support/Utils.php +35 -0
- package/server/src/routes.php +16 -0
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
<?php
|
|
2
|
+
|
|
3
|
+
namespace Fleetbase\FleetOps\Support;
|
|
4
|
+
|
|
5
|
+
use Fleetbase\FleetOps\Models\Driver;
|
|
6
|
+
use Fleetbase\FleetOps\Models\ServiceArea;
|
|
7
|
+
use Fleetbase\FleetOps\Models\Vehicle;
|
|
8
|
+
use Fleetbase\FleetOps\Models\Zone;
|
|
9
|
+
use Fleetbase\LaravelMysqlSpatial\Types\Point;
|
|
10
|
+
use Illuminate\Support\Facades\DB;
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* GeofenceIntersectionService.
|
|
14
|
+
*
|
|
15
|
+
* The core spatial engine for the active geofencing system.
|
|
16
|
+
*
|
|
17
|
+
* On every driver location update, this service:
|
|
18
|
+
* 1. Queries all zones and service areas containing the new point using
|
|
19
|
+
* a two-pass MySQL spatial strategy (MBRContains for index-assisted
|
|
20
|
+
* bounding-box pre-filter, then ST_Contains for precise polygon check).
|
|
21
|
+
* 2. Compares the result against the driver's current state in the
|
|
22
|
+
* driver_geofence_states table.
|
|
23
|
+
* 3. Returns a list of crossing events (entered / exited) for the caller
|
|
24
|
+
* to dispatch as Laravel events.
|
|
25
|
+
*
|
|
26
|
+
* This class is registered as a singleton in FleetOpsServiceProvider.
|
|
27
|
+
*/
|
|
28
|
+
class GeofenceIntersectionService
|
|
29
|
+
{
|
|
30
|
+
/**
|
|
31
|
+
* Detect geofence boundary crossings for a driver given their new location.
|
|
32
|
+
*
|
|
33
|
+
* Returns an array of crossing events. Each element is an associative array:
|
|
34
|
+
* - 'type': 'entered' | 'exited'
|
|
35
|
+
* - 'geofence': Zone or ServiceArea model instance
|
|
36
|
+
* - 'geofence_type': 'zone' | 'service_area'
|
|
37
|
+
*/
|
|
38
|
+
public function detectCrossings(Driver|Vehicle $subject, Point $newLocation): array
|
|
39
|
+
{
|
|
40
|
+
if ($subject instanceof Driver) {
|
|
41
|
+
return $this->detectDriverCrossings($subject, $newLocation);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return $this->detectVehicleCrossings($subject, $newLocation);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
public function detectDriverCrossings(Driver $driver, Point $newLocation): array
|
|
48
|
+
{
|
|
49
|
+
return $this->detectSubjectCrossings($driver->company_uuid, $newLocation, 'driver_geofence_states', 'driver_uuid', $driver->uuid);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
public function detectVehicleCrossings(Vehicle $vehicle, Point $newLocation): array
|
|
53
|
+
{
|
|
54
|
+
return $this->detectSubjectCrossings($vehicle->company_uuid, $newLocation, 'vehicle_geofence_states', 'vehicle_uuid', $vehicle->uuid);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
protected function detectSubjectCrossings(string $companyUuid, Point $newLocation, string $stateTable, string $subjectColumn, string $subjectUuid): array
|
|
58
|
+
{
|
|
59
|
+
$crossings = [];
|
|
60
|
+
|
|
61
|
+
// Build the WKT point string. MySQL ST_GeomFromText expects (lng lat) order.
|
|
62
|
+
$wkt = sprintf('POINT(%s %s)', $newLocation->getLng(), $newLocation->getLat());
|
|
63
|
+
|
|
64
|
+
// ----------------------------------------------------------------
|
|
65
|
+
// 1. Find all Zones the driver is currently inside.
|
|
66
|
+
//
|
|
67
|
+
// Two-pass strategy for MySQL performance:
|
|
68
|
+
// - MBRContains uses the SPATIAL index to filter by minimum
|
|
69
|
+
// bounding rectangle (fast, may include false positives).
|
|
70
|
+
// - ST_Contains performs the precise polygon containment check
|
|
71
|
+
// on the reduced candidate set (accurate, no index needed).
|
|
72
|
+
// ----------------------------------------------------------------
|
|
73
|
+
$insideZones = Zone::where('company_uuid', $companyUuid)
|
|
74
|
+
->whereNotNull('border')
|
|
75
|
+
->where(function ($q) {
|
|
76
|
+
$q->where('trigger_on_entry', true)
|
|
77
|
+
->orWhere('trigger_on_exit', true)
|
|
78
|
+
->orWhereNotNull('dwell_threshold_minutes');
|
|
79
|
+
})
|
|
80
|
+
->whereRaw('MBRContains(`border`, ST_GeomFromText(?))', [$wkt])
|
|
81
|
+
->whereRaw('ST_Contains(`border`, ST_GeomFromText(?))', [$wkt])
|
|
82
|
+
->get();
|
|
83
|
+
|
|
84
|
+
// ----------------------------------------------------------------
|
|
85
|
+
// 2. Find all Service Areas the driver is currently inside.
|
|
86
|
+
//
|
|
87
|
+
// Service areas use MultiPolygon borders, so we use
|
|
88
|
+
// ST_Contains which handles both Polygon and MultiPolygon.
|
|
89
|
+
// ----------------------------------------------------------------
|
|
90
|
+
$insideServiceAreas = ServiceArea::where('company_uuid', $companyUuid)
|
|
91
|
+
->whereNotNull('border')
|
|
92
|
+
->where(function ($q) {
|
|
93
|
+
$q->where('trigger_on_entry', true)
|
|
94
|
+
->orWhere('trigger_on_exit', true)
|
|
95
|
+
->orWhereNotNull('dwell_threshold_minutes');
|
|
96
|
+
})
|
|
97
|
+
->whereRaw('MBRContains(`border`, ST_GeomFromText(?))', [$wkt])
|
|
98
|
+
->whereRaw('ST_Contains(`border`, ST_GeomFromText(?))', [$wkt])
|
|
99
|
+
->get();
|
|
100
|
+
|
|
101
|
+
// Merge into a unified collection with a type discriminator
|
|
102
|
+
$currentlyInside = collect()
|
|
103
|
+
->merge($insideZones->map(fn ($z) => ['model' => $z, 'geofence_type' => 'zone']))
|
|
104
|
+
->merge($insideServiceAreas->map(fn ($sa) => ['model' => $sa, 'geofence_type' => 'service_area']));
|
|
105
|
+
|
|
106
|
+
$currentlyInsideUuids = $currentlyInside->pluck('model.uuid')->toArray();
|
|
107
|
+
|
|
108
|
+
// ----------------------------------------------------------------
|
|
109
|
+
// 3. Load the driver's current geofence state records.
|
|
110
|
+
// ----------------------------------------------------------------
|
|
111
|
+
$currentStates = DB::table($stateTable)
|
|
112
|
+
->where($subjectColumn, $subjectUuid)
|
|
113
|
+
->get()
|
|
114
|
+
->keyBy('geofence_uuid');
|
|
115
|
+
|
|
116
|
+
// ----------------------------------------------------------------
|
|
117
|
+
// 4. Detect ENTRIES: geofences the driver is now inside but
|
|
118
|
+
// was not inside before (or has no state record yet).
|
|
119
|
+
// ----------------------------------------------------------------
|
|
120
|
+
foreach ($currentlyInside as $item) {
|
|
121
|
+
$geofence = $item['model'];
|
|
122
|
+
$state = $currentStates->get($geofence->uuid);
|
|
123
|
+
|
|
124
|
+
if (!$state || !$state->is_inside) {
|
|
125
|
+
$crossings[] = [
|
|
126
|
+
'type' => 'entered',
|
|
127
|
+
'geofence' => $geofence,
|
|
128
|
+
'geofence_type' => $item['geofence_type'],
|
|
129
|
+
];
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// ----------------------------------------------------------------
|
|
134
|
+
// 5. Detect EXITS: geofences the driver was inside but is no
|
|
135
|
+
// longer inside (not in the current ST_Contains result set).
|
|
136
|
+
// ----------------------------------------------------------------
|
|
137
|
+
foreach ($currentStates as $geofenceUuid => $state) {
|
|
138
|
+
if ($state->is_inside && !in_array($geofenceUuid, $currentlyInsideUuids)) {
|
|
139
|
+
$geofence = $state->geofence_type === 'service_area'
|
|
140
|
+
? ServiceArea::where('uuid', $geofenceUuid)->first()
|
|
141
|
+
: Zone::where('uuid', $geofenceUuid)->first();
|
|
142
|
+
|
|
143
|
+
if ($geofence) {
|
|
144
|
+
$crossings[] = [
|
|
145
|
+
'type' => 'exited',
|
|
146
|
+
'geofence' => $geofence,
|
|
147
|
+
'geofence_type' => $state->geofence_type,
|
|
148
|
+
];
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return $crossings;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Check if a driver is currently recorded as inside a specific geofence.
|
|
158
|
+
*
|
|
159
|
+
* @param mixed $geofence Zone or ServiceArea
|
|
160
|
+
*/
|
|
161
|
+
public function isDriverInsideGeofence(Driver $driver, $geofence): bool
|
|
162
|
+
{
|
|
163
|
+
return DB::table('driver_geofence_states')
|
|
164
|
+
->where('driver_uuid', $driver->uuid)
|
|
165
|
+
->where('geofence_uuid', $geofence->uuid)
|
|
166
|
+
->where('is_inside', true)
|
|
167
|
+
->exists();
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
public function isVehicleInsideGeofence(Vehicle $vehicle, $geofence): bool
|
|
171
|
+
{
|
|
172
|
+
return DB::table('vehicle_geofence_states')
|
|
173
|
+
->where('vehicle_uuid', $vehicle->uuid)
|
|
174
|
+
->where('geofence_uuid', $geofence->uuid)
|
|
175
|
+
->where('is_inside', true)
|
|
176
|
+
->exists();
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Clear all geofence state records for a driver.
|
|
181
|
+
* Called when a driver goes offline or completes a shift.
|
|
182
|
+
*/
|
|
183
|
+
public function clearDriverState(Driver $driver): void
|
|
184
|
+
{
|
|
185
|
+
DB::table('driver_geofence_states')
|
|
186
|
+
->where('driver_uuid', $driver->uuid)
|
|
187
|
+
->update([
|
|
188
|
+
'is_inside' => false,
|
|
189
|
+
'exited_at' => now(),
|
|
190
|
+
'dwell_job_id' => null,
|
|
191
|
+
'updated_at' => now(),
|
|
192
|
+
]);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
public function clearVehicleState(Vehicle $vehicle): void
|
|
196
|
+
{
|
|
197
|
+
DB::table('vehicle_geofence_states')
|
|
198
|
+
->where('vehicle_uuid', $vehicle->uuid)
|
|
199
|
+
->update([
|
|
200
|
+
'is_inside' => false,
|
|
201
|
+
'exited_at' => now(),
|
|
202
|
+
'dwell_job_id' => null,
|
|
203
|
+
'updated_at' => now(),
|
|
204
|
+
]);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
namespace Fleetbase\FleetOps\Support;
|
|
4
4
|
|
|
5
5
|
use Fleetbase\FleetOps\Flow\Activity;
|
|
6
|
+
use Fleetbase\Models\Company;
|
|
7
|
+
use Fleetbase\Models\Setting;
|
|
6
8
|
use Fleetbase\LaravelMysqlSpatial\Types\Point;
|
|
7
9
|
use Fleetbase\Support\Utils as FleetbaseUtils;
|
|
8
10
|
use Illuminate\Support\Arr;
|
|
@@ -20,6 +22,39 @@ class Utils extends FleetbaseUtils
|
|
|
20
22
|
*/
|
|
21
23
|
public const DRIVING_TIME_MULTIPLIER = 7.2;
|
|
22
24
|
|
|
25
|
+
/**
|
|
26
|
+
* Resolve the preferred transaction currency for a company.
|
|
27
|
+
*
|
|
28
|
+
* Resolution order:
|
|
29
|
+
* 1. Organization/company configured currency
|
|
30
|
+
* 2. Ledger accounting base currency
|
|
31
|
+
* 3. USD
|
|
32
|
+
*/
|
|
33
|
+
public static function getCompanyTransactionCurrency(string|Company|null $company = null): string
|
|
34
|
+
{
|
|
35
|
+
$companyModel = $company instanceof Company
|
|
36
|
+
? $company
|
|
37
|
+
: (filled($company) ? Company::where('uuid', $company)->first() : null);
|
|
38
|
+
|
|
39
|
+
$companyUuid = $companyModel?->uuid ?? (is_string($company) ? $company : null);
|
|
40
|
+
|
|
41
|
+
$companyCurrency = data_get($companyModel, 'currency');
|
|
42
|
+
if (filled($companyCurrency)) {
|
|
43
|
+
return strtoupper($companyCurrency);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (filled($companyUuid)) {
|
|
47
|
+
$accountingSettings = Setting::lookup('company.' . $companyUuid . '.ledger.accounting-settings', []);
|
|
48
|
+
$baseCurrency = data_get($accountingSettings, 'base_currency');
|
|
49
|
+
|
|
50
|
+
if (filled($baseCurrency)) {
|
|
51
|
+
return strtoupper($baseCurrency);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return 'USD';
|
|
56
|
+
}
|
|
57
|
+
|
|
23
58
|
/**
|
|
24
59
|
* Get a formatted string representation of a place's address.
|
|
25
60
|
*
|
package/server/src/routes.php
CHANGED
|
@@ -145,6 +145,13 @@ Route::prefix(config('fleetops.api.routing.prefix', null))->namespace('Fleetbase
|
|
|
145
145
|
$router->put('{id}', 'ServiceAreaController@update');
|
|
146
146
|
$router->delete('{id}', 'ServiceAreaController@delete');
|
|
147
147
|
});
|
|
148
|
+
// geofences routes
|
|
149
|
+
$router->group(['prefix' => 'geofences'], function () use ($router) {
|
|
150
|
+
$router->get('events', 'GeofenceController@events');
|
|
151
|
+
$router->get('inventory', 'GeofenceController@inventory');
|
|
152
|
+
$router->get('dwell-report', 'GeofenceController@dwellReport');
|
|
153
|
+
$router->get('driver/{driverUuid}/history', 'GeofenceController@driverHistory');
|
|
154
|
+
});
|
|
148
155
|
// service-rates routes
|
|
149
156
|
$router->group(['prefix' => 'service-rates'], function () use ($router) {
|
|
150
157
|
$router->post('/', 'ServiceRateController@create');
|
|
@@ -375,6 +382,15 @@ Route::prefix(config('fleetops.api.routing.prefix', null))->namespace('Fleetbase
|
|
|
375
382
|
$router->delete('bulk-delete', $controller('bulkDelete'));
|
|
376
383
|
}
|
|
377
384
|
);
|
|
385
|
+
$router->group(
|
|
386
|
+
['prefix' => 'geofences'],
|
|
387
|
+
function () use ($router) {
|
|
388
|
+
$router->get('events', 'GeofenceController@events');
|
|
389
|
+
$router->get('inventory', 'GeofenceController@inventory');
|
|
390
|
+
$router->get('dwell-report', 'GeofenceController@dwellReport');
|
|
391
|
+
$router->get('driver/{driverUuid}/history', 'GeofenceController@driverHistory');
|
|
392
|
+
}
|
|
393
|
+
);
|
|
378
394
|
$router->fleetbaseRoutes('zones');
|
|
379
395
|
$router->fleetbaseRoutes(
|
|
380
396
|
'service-quotes',
|