@fleetbase/fleetops-engine 0.6.55 → 0.6.57

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.
Files changed (161) hide show
  1. package/addon/components/admin/map-settings.hbs +1 -1
  2. package/addon/components/ai/create-order-preview.hbs +297 -0
  3. package/addon/components/ai/create-order-preview.js +362 -0
  4. package/addon/components/ai/route-preview-map.hbs +79 -0
  5. package/addon/components/ai/route-preview-map.js +527 -0
  6. package/addon/components/device/details.js +1 -1
  7. package/addon/components/device-event/details.hbs +192 -0
  8. package/addon/components/device-event/details.js +151 -0
  9. package/addon/components/driver/schedule.js +1 -1
  10. package/addon/components/fleet/details.hbs +21 -8
  11. package/addon/components/fleet/details.js +55 -1
  12. package/addon/components/fleet/hierarchy-tree.hbs +137 -0
  13. package/addon/components/fleet/hierarchy-tree.js +528 -0
  14. package/addon/components/layout/fleet-ops-sidebar.js +15 -9
  15. package/addon/components/map/drawer/device-event-listing.js +1 -0
  16. package/addon/components/map/drawer/driver-listing.js +1 -0
  17. package/addon/components/map/drawer/place-listing.js +1 -0
  18. package/addon/components/map/drawer/vehicle-listing.js +1 -0
  19. package/addon/components/order/details.hbs +34 -1
  20. package/addon/components/order/form/details.hbs +4 -0
  21. package/addon/components/order/form.hbs +9 -1
  22. package/addon/components/telematic/details.hbs +1 -1
  23. package/addon/components/telematic/form.hbs +2 -2
  24. package/addon/components/telematic/settings.hbs +1 -1
  25. package/addon/components/vehicle/card.hbs +14 -2
  26. package/addon/components/vehicle/form.hbs +13 -1
  27. package/addon/controllers/connectivity/devices/index/details/events.js +1 -1
  28. package/addon/controllers/connectivity/devices/index/details/vehicle.js +2 -2
  29. package/addon/controllers/connectivity/events/details.js +68 -1
  30. package/addon/controllers/connectivity/sensors/index.js +4 -0
  31. package/addon/controllers/connectivity/telematics/details/events.js +1 -1
  32. package/addon/controllers/connectivity/telematics/details/sensors.js +4 -0
  33. package/addon/controllers/settings/map.js +19 -0
  34. package/addon/extension.js +14 -0
  35. package/addon/instance-initializers/register-vroom-allocation.js +1 -1
  36. package/addon/routes/connectivity/events/details.js +26 -1
  37. package/addon/routes/management/fleets/index/details.js +1 -1
  38. package/addon/routes/management/vehicles/index/details.js +3 -1
  39. package/addon/services/map-adapter/google.js +66 -1
  40. package/addon/services/map-adapter/leaflet.js +1 -1
  41. package/addon/services/map-adapter-interface.js +1 -1
  42. package/addon/services/map-manager.js +16 -1
  43. package/addon/services/map-settings.js +15 -0
  44. package/addon/styles/fleetops-engine.css +487 -1
  45. package/addon/templates/connectivity/devices/index/details/vehicle.hbs +2 -1
  46. package/addon/templates/connectivity/events/details.hbs +40 -1
  47. package/addon/templates/settings/map.hbs +38 -9
  48. package/addon/utils/leaflet-plugin-loader.js +5 -5
  49. package/addon/utils/map-drawer-dropdown-position.js +11 -13
  50. package/addon/utils/route-colors.js +1 -1
  51. package/app/components/ai/create-order-preview.js +1 -0
  52. package/app/components/ai/route-preview-map.js +1 -0
  53. package/app/components/device-event/details.hbs +192 -0
  54. package/app/components/device-event/details.js +1 -0
  55. package/app/components/fleet/hierarchy-tree.js +1 -0
  56. package/app/services/route-engine.js +1 -0
  57. package/composer.json +1 -1
  58. package/extension.json +1 -1
  59. package/package.json +6 -4
  60. package/scripts/normalize-rgba-alpha.js +90 -0
  61. package/server/migrations/2026_06_23_000001_delete_bad_afaqy_telematics_sensors.php +34 -0
  62. package/server/src/Exports/ContactExport.php +12 -5
  63. package/server/src/Exports/DeviceExport.php +92 -0
  64. package/server/src/Exports/DriverExport.php +28 -3
  65. package/server/src/Exports/EquipmentExport.php +94 -0
  66. package/server/src/Exports/FleetExport.php +22 -7
  67. package/server/src/Exports/FuelReportExport.php +16 -1
  68. package/server/src/Exports/IssueExport.php +25 -3
  69. package/server/src/Exports/MaintenanceExport.php +106 -0
  70. package/server/src/Exports/MaintenanceScheduleExport.php +99 -0
  71. package/server/src/Exports/OrderExport.php +24 -5
  72. package/server/src/Exports/PartExport.php +99 -0
  73. package/server/src/Exports/PlaceExport.php +33 -7
  74. package/server/src/Exports/SensorExport.php +94 -0
  75. package/server/src/Exports/ServiceAreaExport.php +31 -7
  76. package/server/src/Exports/ServiceRateExport.php +41 -1
  77. package/server/src/Exports/TelematicExport.php +90 -0
  78. package/server/src/Exports/VehicleExport.php +274 -8
  79. package/server/src/Exports/VendorExport.php +15 -2
  80. package/server/src/Exports/WorkOrderExport.php +92 -0
  81. package/server/src/Http/Controllers/Api/v1/Concerns/ResolvesFleetOpsApiResources.php +176 -0
  82. package/server/src/Http/Controllers/Api/v1/DeviceController.php +225 -0
  83. package/server/src/Http/Controllers/Api/v1/EquipmentController.php +114 -0
  84. package/server/src/Http/Controllers/Api/v1/FuelTransactionController.php +185 -0
  85. package/server/src/Http/Controllers/Api/v1/PartController.php +120 -0
  86. package/server/src/Http/Controllers/Api/v1/SensorController.php +132 -0
  87. package/server/src/Http/Controllers/Api/v1/WorkOrderController.php +161 -0
  88. package/server/src/Http/Controllers/Internal/v1/DeviceController.php +18 -0
  89. package/server/src/Http/Controllers/Internal/v1/EquipmentController.php +17 -0
  90. package/server/src/Http/Controllers/Internal/v1/MaintenanceController.php +17 -0
  91. package/server/src/Http/Controllers/Internal/v1/MaintenanceScheduleController.php +17 -0
  92. package/server/src/Http/Controllers/Internal/v1/PartController.php +17 -0
  93. package/server/src/Http/Controllers/Internal/v1/SensorController.php +18 -0
  94. package/server/src/Http/Controllers/Internal/v1/SettingController.php +29 -5
  95. package/server/src/Http/Controllers/Internal/v1/TelematicController.php +18 -0
  96. package/server/src/Http/Controllers/Internal/v1/WorkOrderController.php +17 -0
  97. package/server/src/Http/Filter/DeviceFilter.php +33 -10
  98. package/server/src/Http/Filter/EquipmentFilter.php +23 -0
  99. package/server/src/Http/Filter/FuelProviderTransactionFilter.php +57 -2
  100. package/server/src/Http/Filter/PartFilter.php +47 -0
  101. package/server/src/Http/Filter/SensorFilter.php +34 -6
  102. package/server/src/Http/Filter/WorkOrderFilter.php +23 -0
  103. package/server/src/Http/Requests/CreateDeviceRequest.php +51 -0
  104. package/server/src/Http/Requests/CreateEquipmentRequest.php +35 -0
  105. package/server/src/Http/Requests/CreateFuelTransactionRequest.php +50 -0
  106. package/server/src/Http/Requests/CreatePartRequest.php +40 -0
  107. package/server/src/Http/Requests/CreateSensorRequest.php +47 -0
  108. package/server/src/Http/Requests/CreateWorkOrderRequest.php +42 -0
  109. package/server/src/Http/Requests/UpdateDeviceRequest.php +7 -0
  110. package/server/src/Http/Requests/UpdateEquipmentRequest.php +7 -0
  111. package/server/src/Http/Requests/UpdateFuelTransactionRequest.php +7 -0
  112. package/server/src/Http/Requests/UpdatePartRequest.php +7 -0
  113. package/server/src/Http/Requests/UpdateSensorRequest.php +7 -0
  114. package/server/src/Http/Requests/UpdateWorkOrderRequest.php +7 -0
  115. package/server/src/Http/Resources/v1/Device.php +72 -0
  116. package/server/src/Http/Resources/v1/Equipment.php +57 -0
  117. package/server/src/Http/Resources/v1/Fleet.php +8 -2
  118. package/server/src/Http/Resources/v1/FuelTransaction.php +69 -0
  119. package/server/src/Http/Resources/v1/Part.php +64 -0
  120. package/server/src/Http/Resources/v1/Sensor.php +68 -0
  121. package/server/src/Jobs/SyncTelematicDevicesJob.php +194 -38
  122. package/server/src/Models/Driver.php +6 -0
  123. package/server/src/Models/Order.php +20 -7
  124. package/server/src/Models/Place.php +11 -3
  125. package/server/src/Observers/PurchaseRateObserver.php +2 -1
  126. package/server/src/Providers/FleetOpsServiceProvider.php +26 -0
  127. package/server/src/Support/Ai/Capabilities/AbstractFleetOpsAICapability.php +88 -0
  128. package/server/src/Support/Ai/Capabilities/AssetStatusCapability.php +108 -0
  129. package/server/src/Support/Ai/Capabilities/ConsoleNavigationCapability.php +55 -0
  130. package/server/src/Support/Ai/Capabilities/CreateOrderPreviewCapability.php +508 -0
  131. package/server/src/Support/Ai/Capabilities/DocsHelpCapability.php +49 -0
  132. package/server/src/Support/Ai/Capabilities/ImportOrdersPreviewCapability.php +66 -0
  133. package/server/src/Support/Ai/Capabilities/OperationalQueryCapability.php +319 -0
  134. package/server/src/Support/Ai/Capabilities/OptimizeOrderRouteCapability.php +240 -0
  135. package/server/src/Support/Ai/Capabilities/OrderInsightsCapability.php +95 -0
  136. package/server/src/Support/Ai/Capabilities/SearchResourcesCapability.php +195 -0
  137. package/server/src/Support/Ai/FleetOpsAiQueryResources.php +150 -0
  138. package/server/src/Support/Analytics/RevenueTrend.php +3 -8
  139. package/server/src/Support/Metrics/ActiveRevenueQuery.php +121 -0
  140. package/server/src/Support/Metrics/AvgOrderValueMetric.php +1 -7
  141. package/server/src/Support/Metrics/EarningsMetric.php +1 -10
  142. package/server/src/Support/Telematics/Providers/AfaqyProvider.php +104 -5
  143. package/server/src/Support/Telematics/Providers/SafeeProvider.php +550 -54
  144. package/server/src/Support/Telematics/TelematicService.php +28 -9
  145. package/server/src/Support/Utils.php +52 -0
  146. package/server/src/routes.php +67 -14
  147. package/server/tests/AiCreateOrderPreviewCapabilityTest.php +131 -0
  148. package/server/tests/AiOperationalQueryCapabilityTest.php +75 -0
  149. package/server/tests/ExportCoverageTest.php +79 -0
  150. package/server/tests/FleetHierarchyResourceTest.php +12 -0
  151. package/server/tests/MetricsRegistryTest.php +45 -5
  152. package/server/tests/OrderPayloadPlaceResolutionTest.php +53 -0
  153. package/server/tests/PointResolutionTest.php +63 -0
  154. package/server/tests/PublicFleetOpsRestEndpointsTest.php +191 -0
  155. package/server/tests/TelematicsHardeningTest.php +636 -1
  156. package/addon/components/table/cell/dropdown/action-item.hbs +0 -31
  157. package/addon/components/table/cell/dropdown/action-item.js +0 -62
  158. package/addon/components/table/cell/dropdown.hbs +0 -28
  159. package/addon/components/table/cell/dropdown.js +0 -91
  160. package/app/components/table/cell/dropdown/action-item.js +0 -1
  161. package/app/components/table/cell/dropdown.js +0 -1
@@ -194,19 +194,30 @@ class TelematicService
194
194
  {
195
195
  $device = $this->linkDevice($telematic, $provider->normalizeDevice($payload));
196
196
 
197
- $event = null;
197
+ $event = null;
198
+ $events = [];
198
199
  try {
199
- $eventData = $provider->normalizeEvent($payload);
200
- if ($this->hasEventSignal($eventData)) {
201
- $event = $this->storeDeviceEvent($telematic, $eventData, $device);
200
+ $eventPayloads = method_exists($provider, 'normalizeEvents') ? $provider->normalizeEvents($payload) : [];
201
+ if (empty($eventPayloads)) {
202
+ $eventPayloads = [$provider->normalizeEvent($payload)];
203
+ }
204
+
205
+ foreach ($eventPayloads as $eventData) {
206
+ if ($this->hasEventSignal($eventData)) {
207
+ $storedEvent = $this->storeDeviceEvent($telematic, $eventData, $device);
208
+ $events[] = $storedEvent;
209
+ $event ??= $storedEvent;
210
+ }
202
211
  }
203
212
  } catch (\Throwable) {
204
- $event = null;
213
+ $event = null;
214
+ $events = [];
205
215
  }
206
216
 
207
217
  return [
208
218
  'device' => $device,
209
219
  'event' => $event,
220
+ 'events' => $events,
210
221
  'sensors' => $this->storeSnapshotSensors($telematic, $provider, $payload, $device),
211
222
  ];
212
223
  }
@@ -663,6 +674,10 @@ class TelematicService
663
674
  $vehicle->altitude = $eventData['altitude'];
664
675
  }
665
676
 
677
+ if (array_key_exists('odometer', $eventData) && $eventData['odometer'] !== null) {
678
+ $vehicle->odometer = $eventData['odometer'];
679
+ }
680
+
666
681
  $vehicle->telematics = array_merge($vehicle->telematics ?? [], [
667
682
  'last_event_uuid' => $event->uuid,
668
683
  'last_event_id' => $event->public_id,
@@ -757,13 +772,17 @@ class TelematicService
757
772
  return collect($rawSensors)
758
773
  ->map(function ($sensor, $name) {
759
774
  if (is_array($sensor)) {
760
- return array_merge(['name' => $name], $sensor);
775
+ return array_merge([
776
+ 'sensor_key' => $name,
777
+ 'name' => $name,
778
+ ], $sensor);
761
779
  }
762
780
 
763
781
  return [
764
- 'name' => $name,
765
- 'type' => $name,
766
- 'value' => $sensor,
782
+ 'sensor_key' => $name,
783
+ 'name' => $name,
784
+ 'type' => $name,
785
+ 'value' => $sensor,
767
786
  ];
768
787
  })
769
788
  ->values()
@@ -248,6 +248,10 @@ class Utils extends FleetbaseUtils
248
248
  $coordinates = static::extractPointWktFromQueryExpression($coordinates) ?? $coordinates;
249
249
  }
250
250
 
251
+ if ($geoJsonPoint = static::pointFromGeoJson($coordinates)) {
252
+ return $geoJsonPoint;
253
+ }
254
+
251
255
  if ($coordinates instanceof Point) {
252
256
  $latitude = $coordinates->getLat();
253
257
  $longitude = $coordinates->getLng();
@@ -452,6 +456,10 @@ class Utils extends FleetbaseUtils
452
456
  return $coordinates;
453
457
  }
454
458
 
459
+ if ($geoJsonPoint = static::pointFromGeoJson($coordinates)) {
460
+ return $geoJsonPoint;
461
+ }
462
+
455
463
  if (static::isGeoJson($coordinates)) {
456
464
  $coordinatesJson = null;
457
465
 
@@ -541,6 +549,10 @@ class Utils extends FleetbaseUtils
541
549
  $coordinates = $coordinates->location;
542
550
  }
543
551
 
552
+ if ($geoJsonPoint = static::pointFromGeoJson($coordinates)) {
553
+ $coordinates = $geoJsonPoint;
554
+ }
555
+
544
556
  if ($coordinates instanceof Point) {
545
557
  $latitude = $coordinates->getLat();
546
558
  $longitude = $coordinates->getLng();
@@ -639,6 +651,10 @@ class Utils extends FleetbaseUtils
639
651
  return $coordinates;
640
652
  }
641
653
 
654
+ if ($geoJsonPoint = static::pointFromGeoJson($coordinates)) {
655
+ return $geoJsonPoint;
656
+ }
657
+
642
658
  if (is_null($coordinates) || !static::isCoordinates($coordinates)) {
643
659
  return new Point(0, 0);
644
660
  }
@@ -649,6 +665,42 @@ class Utils extends FleetbaseUtils
649
665
  return new Point($latitude, $longitude);
650
666
  }
651
667
 
668
+ /**
669
+ * Resolve direct GeoJSON Point values and Feature-wrapped Point values.
670
+ */
671
+ protected static function pointFromGeoJson($coordinates): ?Point
672
+ {
673
+ if (is_string($coordinates) && static::isJson($coordinates)) {
674
+ $coordinates = json_decode($coordinates, true);
675
+ }
676
+
677
+ if (!is_array($coordinates) && !is_object($coordinates)) {
678
+ return null;
679
+ }
680
+
681
+ if (data_get($coordinates, 'type') === 'Feature') {
682
+ $coordinates = data_get($coordinates, 'geometry');
683
+ }
684
+
685
+ if (data_get($coordinates, 'type') !== 'Point') {
686
+ return null;
687
+ }
688
+
689
+ $pointCoordinates = data_get($coordinates, 'coordinates');
690
+ if (!is_array($pointCoordinates) || count($pointCoordinates) < 2) {
691
+ return null;
692
+ }
693
+
694
+ $longitude = $pointCoordinates[0];
695
+ $latitude = $pointCoordinates[1];
696
+
697
+ if (!is_numeric($latitude) || !is_numeric($longitude)) {
698
+ return null;
699
+ }
700
+
701
+ return new Point((float) $latitude, (float) $longitude);
702
+ }
703
+
652
704
  /**
653
705
  * Converts a point to a WKT (Well-Known Text) representation for SQL insert.
654
706
  *
@@ -105,6 +105,61 @@ Route::prefix(config('fleetops.api.routing.prefix'))->namespace('Fleetbase\Fleet
105
105
  $router->put('{id}', 'FuelReportController@update');
106
106
  $router->delete('{id}', 'FuelReportController@delete');
107
107
  });
108
+ // fuel-transactions routes
109
+ $router->group(['prefix' => 'fuel-transactions'], function () use ($router) {
110
+ $router->post('/', 'FuelTransactionController@create');
111
+ $router->get('/', 'FuelTransactionController@query');
112
+ $router->get('{id}', 'FuelTransactionController@find');
113
+ $router->put('{id}', 'FuelTransactionController@update');
114
+ $router->delete('{id}', 'FuelTransactionController@delete');
115
+ $router->post('{id}/match-vehicle', 'FuelTransactionController@matchVehicle');
116
+ $router->post('{id}/match-order', 'FuelTransactionController@matchOrder');
117
+ $router->post('{id}/reprocess', 'FuelTransactionController@reprocess');
118
+ $router->post('{id}/review', 'FuelTransactionController@review');
119
+ });
120
+ // equipment routes
121
+ $router->group(['prefix' => 'equipment'], function () use ($router) {
122
+ $router->post('/', 'EquipmentController@create');
123
+ $router->get('/', 'EquipmentController@query');
124
+ $router->get('{id}', 'EquipmentController@find');
125
+ $router->put('{id}', 'EquipmentController@update');
126
+ $router->delete('{id}', 'EquipmentController@delete');
127
+ });
128
+ // parts routes
129
+ $router->group(['prefix' => 'parts'], function () use ($router) {
130
+ $router->post('/', 'PartController@create');
131
+ $router->get('/', 'PartController@query');
132
+ $router->get('{id}', 'PartController@find');
133
+ $router->put('{id}', 'PartController@update');
134
+ $router->delete('{id}', 'PartController@delete');
135
+ });
136
+ // work-orders routes
137
+ $router->group(['prefix' => 'work-orders'], function () use ($router) {
138
+ $router->post('/', 'WorkOrderController@create');
139
+ $router->get('/', 'WorkOrderController@query');
140
+ $router->get('{id}', 'WorkOrderController@find');
141
+ $router->put('{id}', 'WorkOrderController@update');
142
+ $router->delete('{id}', 'WorkOrderController@delete');
143
+ $router->post('{id}/send', 'WorkOrderController@send');
144
+ });
145
+ // devices routes
146
+ $router->group(['prefix' => 'devices'], function () use ($router) {
147
+ $router->post('/', 'DeviceController@create');
148
+ $router->get('/', 'DeviceController@query');
149
+ $router->get('{id}', 'DeviceController@find');
150
+ $router->put('{id}', 'DeviceController@update');
151
+ $router->delete('{id}', 'DeviceController@delete');
152
+ $router->post('{id}/attach', 'DeviceController@attach');
153
+ $router->post('{id}/detach', 'DeviceController@detach');
154
+ });
155
+ // sensors routes
156
+ $router->group(['prefix' => 'sensors'], function () use ($router) {
157
+ $router->post('/', 'SensorController@create');
158
+ $router->get('/', 'SensorController@query');
159
+ $router->get('{id}', 'SensorController@find');
160
+ $router->put('{id}', 'SensorController@update');
161
+ $router->delete('{id}', 'SensorController@delete');
162
+ });
108
163
  // orders routes
109
164
  $router->group(['prefix' => 'orders', 'middleware' => []], function () use ($router) {
110
165
  $router->post('/', 'OrderController@create');
@@ -315,7 +370,6 @@ Route::prefix(config('fleetops.api.routing.prefix'))->namespace('Fleetbase\Fleet
315
370
  $router->get('facilitators/{id}', $controller('getAsFacilitator'));
316
371
  $router->get('customers/{id}', $controller('getAsCustomer'));
317
372
  $router->post('{id}/convert-to-vendor', $controller('convertToVendor'));
318
- $router->delete('bulk-delete', $controller('bulkDelete'));
319
373
  }
320
374
  );
321
375
  $router->fleetbaseRoutes(
@@ -330,7 +384,6 @@ Route::prefix(config('fleetops.api.routing.prefix'))->namespace('Fleetbase\Fleet
330
384
  $router->post('{id}/assign-vehicle', $controller('assignVehicle'));
331
385
  $router->post('{id}/unassign-vehicle', $controller('unassignVehicle'));
332
386
  $router->match(['get', 'post'], 'export', $controller('export'));
333
- $router->delete('bulk-delete', $controller('bulkDelete'));
334
387
  $router->post('import', $controller('import'));
335
388
  // Driver scheduling endpoints
336
389
  $router->get('{id}/schedule-items', $controller('scheduleItems'));
@@ -349,7 +402,6 @@ Route::prefix(config('fleetops.api.routing.prefix'))->namespace('Fleetbase\Fleet
349
402
  $router->post('remove-vehicle', $controller('removeVehicle'));
350
403
  $router->match(['get', 'post'], 'export', $controller('export'));
351
404
  $router->post('import', $controller('import'));
352
- $router->delete('bulk-delete', $controller('bulkDelete'));
353
405
  }
354
406
  );
355
407
  $router->fleetbaseRoutes(
@@ -357,7 +409,6 @@ Route::prefix(config('fleetops.api.routing.prefix'))->namespace('Fleetbase\Fleet
357
409
  function ($router, $controller) {
358
410
  $router->match(['get', 'post'], 'export', $controller('export'));
359
411
  $router->post('import', $controller('import'));
360
- $router->delete('bulk-delete', $controller('bulkDelete'));
361
412
  }
362
413
  );
363
414
  $router->fleetbaseRoutes(
@@ -385,14 +436,12 @@ Route::prefix(config('fleetops.api.routing.prefix'))->namespace('Fleetbase\Fleet
385
436
  function ($router, $controller) {
386
437
  $router->match(['get', 'post'], 'export', $controller('export'));
387
438
  $router->post('import', $controller('import'));
388
- $router->delete('bulk-delete', $controller('bulkDelete'));
389
439
  }
390
440
  );
391
441
  $router->fleetbaseRoutes(
392
442
  'integrated-vendors',
393
443
  function ($router, $controller) {
394
444
  $router->get('supported', $controller('getSupported'));
395
- $router->delete('bulk-delete', $controller('bulkDelete'));
396
445
  }
397
446
  );
398
447
  $router->fleetbaseRoutes(
@@ -421,7 +470,6 @@ Route::prefix(config('fleetops.api.routing.prefix'))->namespace('Fleetbase\Fleet
421
470
  $router->patch('start', $controller('start'));
422
471
  // Scheduler: set scheduled_at + driver without triggering dispatch
423
472
  $router->patch('schedule', $controller('scheduleOrder'));
424
- $router->delete('bulk-delete', $controller('bulkDelete'));
425
473
  $router->match(['get', 'post'], 'export', $controller('export'));
426
474
  }
427
475
  );
@@ -434,7 +482,6 @@ Route::prefix(config('fleetops.api.routing.prefix'))->namespace('Fleetbase\Fleet
434
482
  $router->get('lookup', $controller('geocode'))->middleware(['cache.headers:private;max_age=3600']);
435
483
  $router->get('avatars', $controller('avatars'));
436
484
  $router->match(['get', 'post'], 'export', $controller('export'));
437
- $router->delete('bulk-delete', $controller('bulkDelete'));
438
485
  $router->post('import', $controller('import'));
439
486
  }
440
487
  );
@@ -449,7 +496,6 @@ Route::prefix(config('fleetops.api.routing.prefix'))->namespace('Fleetbase\Fleet
449
496
  'service-areas',
450
497
  function ($router, $controller) {
451
498
  $router->match(['get', 'post'], 'export', $controller('export'));
452
- $router->delete('bulk-delete', $controller('bulkDelete'));
453
499
  }
454
500
  );
455
501
  $router->group(
@@ -473,7 +519,6 @@ Route::prefix(config('fleetops.api.routing.prefix'))->namespace('Fleetbase\Fleet
473
519
  $router->fleetbaseRoutes(
474
520
  'service-rates',
475
521
  function ($router, $controller) {
476
- $router->delete('bulk-delete', $controller('bulkDelete'));
477
522
  $router->get('for-route', $controller('getServicesForRoute'));
478
523
  $router->match(['get', 'post'], 'export', $controller('export'));
479
524
  }
@@ -493,7 +538,6 @@ Route::prefix(config('fleetops.api.routing.prefix'))->namespace('Fleetbase\Fleet
493
538
  $router->post('{id}/detach-device', $controller('detachDevice'));
494
539
  $router->match(['get', 'post'], 'export', $controller('export'));
495
540
  $router->post('import', $controller('import'));
496
- $router->delete('bulk-delete', $controller('bulkDelete'));
497
541
  }
498
542
  );
499
543
  $router->fleetbaseRoutes('vehicle-devices');
@@ -509,18 +553,22 @@ Route::prefix(config('fleetops.api.routing.prefix'))->namespace('Fleetbase\Fleet
509
553
  $router->delete('{id}/personnels/{contact}', $controller('removeVendorPersonnel'));
510
554
  $router->post('{id}/assign-driver', $controller('assignDriver'));
511
555
  $router->post('{id}/remove-driver', $controller('removeDriver'));
512
- $router->delete('bulk-delete', $controller('bulkDelete'));
513
556
  $router->post('import', $controller('import'));
514
557
  }
515
558
  );
516
- $router->fleetbaseRoutes('devices');
559
+ $router->fleetbaseRoutes('devices', function ($router, $controller) {
560
+ $router->match(['get', 'post'], 'export', $controller('export'));
561
+ });
517
562
  $router->post('devices/{id}/attach', 'DeviceController@attach');
518
563
  $router->post('devices/{id}/detach', 'DeviceController@detach');
519
564
  $router->fleetbaseRoutes('device-events', function ($router, $controller) {
520
565
  $router->post('{id}/mark-processed', $controller('markProcessed'));
521
566
  });
522
- $router->fleetbaseRoutes('sensors');
567
+ $router->fleetbaseRoutes('sensors', function ($router, $controller) {
568
+ $router->match(['get', 'post'], 'export', $controller('export'));
569
+ });
523
570
  $router->fleetbaseRoutes('telematics', function ($router, $controller) {
571
+ $router->match(['get', 'post'], 'export', $controller('export'));
524
572
  $router->get('providers', $controller('providers'));
525
573
  $router->get('{id}/logs', $controller('logs'));
526
574
  $router->get('{id}/devices', $controller('devices'));
@@ -530,6 +578,7 @@ Route::prefix(config('fleetops.api.routing.prefix'))->namespace('Fleetbase\Fleet
530
578
  $router->post('{key}/test-credentials', $controller('testCredentials'));
531
579
  });
532
580
  $router->fleetbaseRoutes('maintenance-schedules', function ($router, $controller) {
581
+ $router->match(['get', 'post'], 'export', $controller('export'));
533
582
  $router->post('import', $controller('import'));
534
583
  $router->post('{id}/pause', $controller('pause'));
535
584
  $router->post('{id}/resume', $controller('resume'));
@@ -538,19 +587,23 @@ Route::prefix(config('fleetops.api.routing.prefix'))->namespace('Fleetbase\Fleet
538
587
  $router->get('{id}/ical', $controller('ical'));
539
588
  });
540
589
  $router->fleetbaseRoutes('work-orders', function ($router, $controller) {
590
+ $router->match(['get', 'post'], 'export', $controller('export'));
541
591
  $router->post('import', $controller('import'));
542
592
  $router->post('{id}/send', $controller('sendEmail'));
543
593
  });
544
594
  $router->fleetbaseRoutes('maintenances', function ($router, $controller) {
595
+ $router->match(['get', 'post'], 'export', $controller('export'));
545
596
  $router->post('import', $controller('import'));
546
597
  $router->post('{id}/line-items', $controller('addLineItem'));
547
598
  $router->put('{id}/line-items/{index}', $controller('updateLineItem'));
548
599
  $router->delete('{id}/line-items/{index}', $controller('removeLineItem'));
549
600
  });
550
601
  $router->fleetbaseRoutes('equipment', function ($router, $controller) {
602
+ $router->match(['get', 'post'], 'export', $controller('export'));
551
603
  $router->post('import', $controller('import'));
552
604
  });
553
605
  $router->fleetbaseRoutes('parts', function ($router, $controller) {
606
+ $router->match(['get', 'post'], 'export', $controller('export'));
554
607
  $router->post('import', $controller('import'));
555
608
  });
556
609
  $router->fleetbaseRoutes('warranties');
@@ -0,0 +1,131 @@
1
+ <?php
2
+
3
+ use Fleetbase\FleetOps\Models\Place;
4
+ use Fleetbase\FleetOps\Support\Ai\Capabilities\CreateOrderPreviewCapability;
5
+ use Fleetbase\LaravelMysqlSpatial\Types\Point as SpatialPoint;
6
+ use Illuminate\Support\Carbon;
7
+
8
+ function fleetopsAiCreateOrderCapability()
9
+ {
10
+ return (new ReflectionClass(CreateOrderPreviewCapability::class))->newInstanceWithoutConstructor();
11
+ }
12
+
13
+ function fleetopsAiCreateOrderProtectedMethod(string $method): ReflectionMethod
14
+ {
15
+ $reflection = new ReflectionClass(CreateOrderPreviewCapability::class);
16
+ $method = $reflection->getMethod($method);
17
+ $method->setAccessible(true);
18
+
19
+ return $method;
20
+ }
21
+
22
+ test('create order preview parser extracts pickup and dropoff labels', function (string $prompt, array $expected) {
23
+ $capability = fleetopsAiCreateOrderCapability();
24
+ $method = fleetopsAiCreateOrderProtectedMethod('addressPairFromPrompt');
25
+
26
+ expect($method->invoke($capability, $prompt))->toBe($expected);
27
+ })->with([
28
+ 'pickup comma and dropoff' => [
29
+ 'Create an order with the pickup 16 simon walk singapore, and the dropoff 18 hougang ave',
30
+ ['16 simon walk singapore', '18 hougang ave'],
31
+ ],
32
+ 'pickup at and drop off to' => [
33
+ 'Create an order pickup at 16 simon walk singapore and drop off to 18 hougang ave',
34
+ ['16 simon walk singapore', '18 hougang ave'],
35
+ ],
36
+ 'from to' => [
37
+ 'Create an order from 16 simon walk singapore to 18 hougang ave',
38
+ ['16 simon walk singapore', '18 hougang ave'],
39
+ ],
40
+ ]);
41
+
42
+ test('create order preview keeps unresolved addresses as provisional places', function () {
43
+ $capability = fleetopsAiCreateOrderCapability();
44
+ $method = fleetopsAiCreateOrderProtectedMethod('provisionalPlace');
45
+
46
+ expect($method->invoke($capability, '16 simon walk singapore'))->toMatchArray([
47
+ 'uuid' => null,
48
+ 'name' => '16 simon walk singapore',
49
+ 'address' => '16 simon walk singapore',
50
+ 'latitude' => null,
51
+ 'longitude' => null,
52
+ 'source' => 'unresolved',
53
+ ]);
54
+ });
55
+
56
+ test('create order preview serializes coordinates from place location', function () {
57
+ $capability = fleetopsAiCreateOrderCapability();
58
+ $method = fleetopsAiCreateOrderProtectedMethod('serializePlace');
59
+ $place = new Place([
60
+ 'name' => '16 Simon Walk',
61
+ 'street1' => '16 Simon Walk',
62
+ 'city' => 'Singapore',
63
+ 'postal_code' => '545870',
64
+ 'location' => new SpatialPoint(1.3621663, 103.8845049),
65
+ ]);
66
+
67
+ expect($method->invoke($capability, $place, '16 simon walk singapore'))->toMatchArray([
68
+ 'latitude' => 1.3621663,
69
+ 'longitude' => 103.8845049,
70
+ ]);
71
+ });
72
+
73
+ test('create order preview route preview excludes return stops', function () {
74
+ $capability = fleetopsAiCreateOrderCapability();
75
+ $method = fleetopsAiCreateOrderProtectedMethod('routePreview');
76
+
77
+ $preview = $method->invoke($capability, [
78
+ 'payload' => [
79
+ 'pickup' => ['address' => '16 Simon Walk', 'latitude' => 1.3621663, 'longitude' => 103.8845049],
80
+ 'dropoff' => ['address' => '18 Hougang Avenue 8', 'latitude' => 1.3701, 'longitude' => 103.8912],
81
+ 'return' => ['address' => 'Ignored return', 'latitude' => 1.3, 'longitude' => 103.8],
82
+ ],
83
+ ]);
84
+
85
+ expect(collect($preview['stops'])->pluck('role')->all())
86
+ ->toBe(['pickup', 'dropoff'])
87
+ ->and($preview['coordinates'])->toHaveCount(2);
88
+ });
89
+
90
+ test('create order apply draft removes embedded places when endpoint uuids exist', function () {
91
+ $capability = new class extends CreateOrderPreviewCapability {
92
+ public function sanitizeForTest(array $draft): array
93
+ {
94
+ return $this->sanitizeDraftForApply($draft);
95
+ }
96
+
97
+ protected function hasExistingPlaceUuid($uuid): bool
98
+ {
99
+ return in_array($uuid, ['pickup_uuid_test', 'dropoff_uuid_test'], true);
100
+ }
101
+ };
102
+
103
+ $draft = $capability->sanitizeForTest([
104
+ 'payload' => [
105
+ 'pickup_uuid' => 'pickup_uuid_test',
106
+ 'dropoff_uuid' => 'dropoff_uuid_test',
107
+ 'pickup' => ['uuid' => 'pickup_uuid_test', 'location' => ['type' => 'Point', 'coordinates' => [103.851, 1.2816]]],
108
+ 'dropoff' => ['uuid' => 'dropoff_uuid_test', 'location' => ['type' => 'Point', 'coordinates' => [103.8318, 1.3048]]],
109
+ 'return' => ['location' => ['type' => 'Point', 'coordinates' => [103.8, 1.3]]],
110
+ ],
111
+ ]);
112
+
113
+ expect($draft['payload'])->toHaveKeys(['pickup_uuid', 'dropoff_uuid', 'return'])
114
+ ->and($draft['payload'])->not->toHaveKeys(['pickup', 'dropoff']);
115
+ });
116
+
117
+ test('create order preview resolves relative schedule phrases', function () {
118
+ $timezone = date_default_timezone_get();
119
+ date_default_timezone_set('Asia/Singapore');
120
+ Carbon::setTestNow(Carbon::parse('2026-06-30 15:00:00', 'Asia/Singapore'));
121
+
122
+ try {
123
+ $capability = fleetopsAiCreateOrderCapability();
124
+ $method = fleetopsAiCreateOrderProtectedMethod('scheduledAtFromPrompt');
125
+
126
+ expect($method->invoke($capability, 'Create an order scheduled for 3 days from now'))->toBe('2026-07-03T15:00:00+08:00');
127
+ } finally {
128
+ Carbon::setTestNow();
129
+ date_default_timezone_set($timezone);
130
+ }
131
+ });
@@ -0,0 +1,75 @@
1
+ <?php
2
+
3
+ use Fleetbase\Ai\Support\AiQueryRegistry;
4
+ use Fleetbase\FleetOps\Support\Ai\Capabilities\AssetStatusCapability;
5
+ use Fleetbase\FleetOps\Support\Ai\Capabilities\OperationalQueryCapability;
6
+ use Fleetbase\FleetOps\Support\Ai\FleetOpsAiQueryResources;
7
+ use Illuminate\Support\Carbon;
8
+
9
+ function fleetopsAiOperationalCapability()
10
+ {
11
+ return (new ReflectionClass(OperationalQueryCapability::class))->newInstanceWithoutConstructor();
12
+ }
13
+
14
+ function fleetopsAiOperationalProtectedMethod(string $method): ReflectionMethod
15
+ {
16
+ $reflection = new ReflectionClass(OperationalQueryCapability::class);
17
+ $method = $reflection->getMethod($method);
18
+ $method->setAccessible(true);
19
+
20
+ return $method;
21
+ }
22
+
23
+ test('fleet-ops registers safe ai query resources', function () {
24
+ $registry = new AiQueryRegistry();
25
+
26
+ FleetOpsAiQueryResources::register($registry);
27
+
28
+ expect($registry->find('drivers')->key)->toBe('fleet-ops.drivers')
29
+ ->and($registry->find('orders')->hasField('driver_assigned_uuid'))->toBeTrue()
30
+ ->and($registry->find('devices')->hasField('online'))->toBeTrue()
31
+ ->and($registry->find('service area')->key)->toBe('fleet-ops.service_areas');
32
+ });
33
+
34
+ test('operational query capability matches common fleet-ops data questions', function (string $prompt) {
35
+ $capability = fleetopsAiOperationalCapability();
36
+ $method = fleetopsAiOperationalProtectedMethod('matchesPrompt');
37
+
38
+ expect($method->invoke($capability, strtolower($prompt)))->toBeTrue();
39
+ })->with([
40
+ 'How many drivers are currently online?',
41
+ 'Where are most of my drivers located?',
42
+ 'How many vehicles were online yesterday?',
43
+ 'Which service area has the most online drivers?',
44
+ 'Show me drivers without vehicles.',
45
+ 'How many active orders have no assigned driver?',
46
+ 'Break down orders by status for this month.',
47
+ ]);
48
+
49
+ test('asset status capability includes driver online prompts', function () {
50
+ $capability = (new ReflectionClass(AssetStatusCapability::class))->newInstanceWithoutConstructor();
51
+ $method = (new ReflectionClass(AssetStatusCapability::class))->getMethod('matchesPrompt');
52
+ $method->setAccessible(true);
53
+
54
+ expect($method->invoke($capability, 'how many drivers are online'))->toBeTrue();
55
+ });
56
+
57
+ test('operational query date filters use resolved local windows', function () {
58
+ $timezone = date_default_timezone_get();
59
+ date_default_timezone_set('Asia/Singapore');
60
+ Carbon::setTestNow(Carbon::parse('2026-06-30 15:00:00', 'Asia/Singapore'));
61
+
62
+ try {
63
+ $capability = fleetopsAiOperationalCapability();
64
+ $method = fleetopsAiOperationalProtectedMethod('orderDateFilters');
65
+ $filters = $method->invoke($capability, 'How many orders were created last week?');
66
+
67
+ expect($filters)->toHaveCount(2)
68
+ ->and($filters[0])->toMatchArray(['field' => 'created_at', 'operator' => '>='])
69
+ ->and($filters[0]['value']->toIso8601String())->toBe('2026-06-22T00:00:00+08:00')
70
+ ->and($filters[1]['value']->toIso8601String())->toBe('2026-06-28T23:59:59+08:00');
71
+ } finally {
72
+ Carbon::setTestNow();
73
+ date_default_timezone_set($timezone);
74
+ }
75
+ });
@@ -0,0 +1,79 @@
1
+ <?php
2
+
3
+ use Fleetbase\FleetOps\Exports\DeviceExport;
4
+ use Fleetbase\FleetOps\Exports\MaintenanceExport;
5
+ use Fleetbase\FleetOps\Exports\MaintenanceScheduleExport;
6
+ use Fleetbase\FleetOps\Exports\VehicleExport;
7
+ use Fleetbase\FleetOps\Exports\WorkOrderExport;
8
+
9
+ test('expanded export headings include important operational fields', function () {
10
+ expect((new VehicleExport())->headings())
11
+ ->toContain('Plate Number')
12
+ ->toContain('VIN')
13
+ ->toContain('Fuel Card Number')
14
+ ->toContain('Status')
15
+ ->toContain('Body Type')
16
+ ->toContain('Usage Type')
17
+ ->toContain('Transmission')
18
+ ->toContain('Measurement System')
19
+ ->toContain('Engine Number')
20
+ ->toContain('Engine Size (L)')
21
+ ->toContain('Payload Capacity (kg)')
22
+ ->toContain('Payload Volume (m3)')
23
+ ->toContain('DPF Equipped')
24
+ ->toContain('Insurance Value')
25
+ ->toContain('Loan Amount')
26
+ ->toContain('Vehicle Skills')
27
+ ->toContain('Updated At');
28
+
29
+ expect((new WorkOrderExport())->headings())
30
+ ->toContain('Assignee')
31
+ ->toContain('Target')
32
+ ->toContain('Due At')
33
+ ->toContain('Completion Percentage');
34
+
35
+ expect((new DeviceExport())->headings())
36
+ ->toContain('Connection Status')
37
+ ->toContain('Attached To')
38
+ ->toContain('Telematic Provider')
39
+ ->toContain('Last Seen');
40
+ });
41
+
42
+ test('relationship exports use readable name columns', function () {
43
+ expect((new VehicleExport())->headings())
44
+ ->toContain('Driver')
45
+ ->toContain('Vendor');
46
+
47
+ expect((new WorkOrderExport())->headings())
48
+ ->toContain('Assignee')
49
+ ->toContain('Target');
50
+
51
+ expect((new MaintenanceExport())->headings())
52
+ ->toContain('Asset')
53
+ ->toContain('Performed By');
54
+
55
+ expect((new MaintenanceScheduleExport())->headings())
56
+ ->toContain('Subject')
57
+ ->toContain('Default Assignee');
58
+ });
59
+
60
+ test('vehicle export helpers format spreadsheet friendly values', function () {
61
+ $export = new VehicleExport();
62
+ $helper = fn (string $method, ...$arguments) => (new ReflectionMethod($export, $method))->invoke($export, ...$arguments);
63
+
64
+ expect($helper('yesNo', true))->toBe('Yes')
65
+ ->and($helper('yesNo', false))->toBe('No')
66
+ ->and($helper('yesNo', null))->toBeNull()
67
+ ->and($helper('joinSkills', ['hazmat', 'refrigerated']))->toBe('hazmat, refrigerated')
68
+ ->and($helper('joinSkills', []))->toBeNull();
69
+ });
70
+
71
+ test('resources with visible export actions have backend export routes', function () {
72
+ $routes = file_get_contents(dirname(__DIR__) . '/src/routes.php');
73
+
74
+ foreach (['devices', 'sensors', 'telematics', 'maintenance-schedules', 'work-orders', 'maintenances', 'equipment', 'parts'] as $resource) {
75
+ expect($routes)->toContain("\$router->fleetbaseRoutes('{$resource}'");
76
+ }
77
+
78
+ expect(substr_count($routes, "\$router->match(['get', 'post'], 'export', \$controller('export'));"))->toBeGreaterThanOrEqual(19);
79
+ });
@@ -0,0 +1,12 @@
1
+ <?php
2
+
3
+ test('fleet resource expands subfleet drivers and vehicles for hierarchy payloads', function () {
4
+ $resource = file_get_contents(__DIR__ . '/../src/Http/Resources/v1/Fleet.php');
5
+
6
+ expect($resource)
7
+ ->toContain("if (in_array('subfleets', \$with, true))")
8
+ ->toContain("if (in_array('drivers', \$with, true))")
9
+ ->toContain("\$this->loadMissing('subFleets.drivers')")
10
+ ->toContain("if (in_array('vehicles', \$with, true))")
11
+ ->toContain("\$this->loadMissing('subFleets.vehicles')");
12
+ });