@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.
- package/addon/components/admin/map-settings.hbs +1 -1
- package/addon/components/ai/create-order-preview.hbs +297 -0
- package/addon/components/ai/create-order-preview.js +362 -0
- package/addon/components/ai/route-preview-map.hbs +79 -0
- package/addon/components/ai/route-preview-map.js +527 -0
- package/addon/components/device/details.js +1 -1
- package/addon/components/device-event/details.hbs +192 -0
- package/addon/components/device-event/details.js +151 -0
- package/addon/components/driver/schedule.js +1 -1
- package/addon/components/fleet/details.hbs +21 -8
- package/addon/components/fleet/details.js +55 -1
- package/addon/components/fleet/hierarchy-tree.hbs +137 -0
- package/addon/components/fleet/hierarchy-tree.js +528 -0
- package/addon/components/layout/fleet-ops-sidebar.js +15 -9
- package/addon/components/map/drawer/device-event-listing.js +1 -0
- package/addon/components/map/drawer/driver-listing.js +1 -0
- package/addon/components/map/drawer/place-listing.js +1 -0
- package/addon/components/map/drawer/vehicle-listing.js +1 -0
- package/addon/components/order/details.hbs +34 -1
- package/addon/components/order/form/details.hbs +4 -0
- package/addon/components/order/form.hbs +9 -1
- package/addon/components/telematic/details.hbs +1 -1
- package/addon/components/telematic/form.hbs +2 -2
- package/addon/components/telematic/settings.hbs +1 -1
- package/addon/components/vehicle/card.hbs +14 -2
- package/addon/components/vehicle/form.hbs +13 -1
- package/addon/controllers/connectivity/devices/index/details/events.js +1 -1
- package/addon/controllers/connectivity/devices/index/details/vehicle.js +2 -2
- package/addon/controllers/connectivity/events/details.js +68 -1
- package/addon/controllers/connectivity/sensors/index.js +4 -0
- package/addon/controllers/connectivity/telematics/details/events.js +1 -1
- package/addon/controllers/connectivity/telematics/details/sensors.js +4 -0
- package/addon/controllers/settings/map.js +19 -0
- package/addon/extension.js +14 -0
- package/addon/instance-initializers/register-vroom-allocation.js +1 -1
- package/addon/routes/connectivity/events/details.js +26 -1
- package/addon/routes/management/fleets/index/details.js +1 -1
- package/addon/routes/management/vehicles/index/details.js +3 -1
- package/addon/services/map-adapter/google.js +66 -1
- package/addon/services/map-adapter/leaflet.js +1 -1
- package/addon/services/map-adapter-interface.js +1 -1
- package/addon/services/map-manager.js +16 -1
- package/addon/services/map-settings.js +15 -0
- package/addon/styles/fleetops-engine.css +487 -1
- package/addon/templates/connectivity/devices/index/details/vehicle.hbs +2 -1
- package/addon/templates/connectivity/events/details.hbs +40 -1
- package/addon/templates/settings/map.hbs +38 -9
- package/addon/utils/leaflet-plugin-loader.js +5 -5
- package/addon/utils/map-drawer-dropdown-position.js +11 -13
- package/addon/utils/route-colors.js +1 -1
- package/app/components/ai/create-order-preview.js +1 -0
- package/app/components/ai/route-preview-map.js +1 -0
- package/app/components/device-event/details.hbs +192 -0
- package/app/components/device-event/details.js +1 -0
- package/app/components/fleet/hierarchy-tree.js +1 -0
- package/app/services/route-engine.js +1 -0
- package/composer.json +1 -1
- package/extension.json +1 -1
- package/package.json +6 -4
- package/scripts/normalize-rgba-alpha.js +90 -0
- package/server/migrations/2026_06_23_000001_delete_bad_afaqy_telematics_sensors.php +34 -0
- package/server/src/Exports/ContactExport.php +12 -5
- package/server/src/Exports/DeviceExport.php +92 -0
- package/server/src/Exports/DriverExport.php +28 -3
- package/server/src/Exports/EquipmentExport.php +94 -0
- package/server/src/Exports/FleetExport.php +22 -7
- package/server/src/Exports/FuelReportExport.php +16 -1
- package/server/src/Exports/IssueExport.php +25 -3
- package/server/src/Exports/MaintenanceExport.php +106 -0
- package/server/src/Exports/MaintenanceScheduleExport.php +99 -0
- package/server/src/Exports/OrderExport.php +24 -5
- package/server/src/Exports/PartExport.php +99 -0
- package/server/src/Exports/PlaceExport.php +33 -7
- package/server/src/Exports/SensorExport.php +94 -0
- package/server/src/Exports/ServiceAreaExport.php +31 -7
- package/server/src/Exports/ServiceRateExport.php +41 -1
- package/server/src/Exports/TelematicExport.php +90 -0
- package/server/src/Exports/VehicleExport.php +274 -8
- package/server/src/Exports/VendorExport.php +15 -2
- package/server/src/Exports/WorkOrderExport.php +92 -0
- package/server/src/Http/Controllers/Api/v1/Concerns/ResolvesFleetOpsApiResources.php +176 -0
- package/server/src/Http/Controllers/Api/v1/DeviceController.php +225 -0
- package/server/src/Http/Controllers/Api/v1/EquipmentController.php +114 -0
- package/server/src/Http/Controllers/Api/v1/FuelTransactionController.php +185 -0
- package/server/src/Http/Controllers/Api/v1/PartController.php +120 -0
- package/server/src/Http/Controllers/Api/v1/SensorController.php +132 -0
- package/server/src/Http/Controllers/Api/v1/WorkOrderController.php +161 -0
- package/server/src/Http/Controllers/Internal/v1/DeviceController.php +18 -0
- package/server/src/Http/Controllers/Internal/v1/EquipmentController.php +17 -0
- package/server/src/Http/Controllers/Internal/v1/MaintenanceController.php +17 -0
- package/server/src/Http/Controllers/Internal/v1/MaintenanceScheduleController.php +17 -0
- package/server/src/Http/Controllers/Internal/v1/PartController.php +17 -0
- package/server/src/Http/Controllers/Internal/v1/SensorController.php +18 -0
- package/server/src/Http/Controllers/Internal/v1/SettingController.php +29 -5
- package/server/src/Http/Controllers/Internal/v1/TelematicController.php +18 -0
- package/server/src/Http/Controllers/Internal/v1/WorkOrderController.php +17 -0
- package/server/src/Http/Filter/DeviceFilter.php +33 -10
- package/server/src/Http/Filter/EquipmentFilter.php +23 -0
- package/server/src/Http/Filter/FuelProviderTransactionFilter.php +57 -2
- package/server/src/Http/Filter/PartFilter.php +47 -0
- package/server/src/Http/Filter/SensorFilter.php +34 -6
- package/server/src/Http/Filter/WorkOrderFilter.php +23 -0
- package/server/src/Http/Requests/CreateDeviceRequest.php +51 -0
- package/server/src/Http/Requests/CreateEquipmentRequest.php +35 -0
- package/server/src/Http/Requests/CreateFuelTransactionRequest.php +50 -0
- package/server/src/Http/Requests/CreatePartRequest.php +40 -0
- package/server/src/Http/Requests/CreateSensorRequest.php +47 -0
- package/server/src/Http/Requests/CreateWorkOrderRequest.php +42 -0
- package/server/src/Http/Requests/UpdateDeviceRequest.php +7 -0
- package/server/src/Http/Requests/UpdateEquipmentRequest.php +7 -0
- package/server/src/Http/Requests/UpdateFuelTransactionRequest.php +7 -0
- package/server/src/Http/Requests/UpdatePartRequest.php +7 -0
- package/server/src/Http/Requests/UpdateSensorRequest.php +7 -0
- package/server/src/Http/Requests/UpdateWorkOrderRequest.php +7 -0
- package/server/src/Http/Resources/v1/Device.php +72 -0
- package/server/src/Http/Resources/v1/Equipment.php +57 -0
- package/server/src/Http/Resources/v1/Fleet.php +8 -2
- package/server/src/Http/Resources/v1/FuelTransaction.php +69 -0
- package/server/src/Http/Resources/v1/Part.php +64 -0
- package/server/src/Http/Resources/v1/Sensor.php +68 -0
- package/server/src/Jobs/SyncTelematicDevicesJob.php +194 -38
- package/server/src/Models/Driver.php +6 -0
- package/server/src/Models/Order.php +20 -7
- package/server/src/Models/Place.php +11 -3
- package/server/src/Observers/PurchaseRateObserver.php +2 -1
- package/server/src/Providers/FleetOpsServiceProvider.php +26 -0
- package/server/src/Support/Ai/Capabilities/AbstractFleetOpsAICapability.php +88 -0
- package/server/src/Support/Ai/Capabilities/AssetStatusCapability.php +108 -0
- package/server/src/Support/Ai/Capabilities/ConsoleNavigationCapability.php +55 -0
- package/server/src/Support/Ai/Capabilities/CreateOrderPreviewCapability.php +508 -0
- package/server/src/Support/Ai/Capabilities/DocsHelpCapability.php +49 -0
- package/server/src/Support/Ai/Capabilities/ImportOrdersPreviewCapability.php +66 -0
- package/server/src/Support/Ai/Capabilities/OperationalQueryCapability.php +319 -0
- package/server/src/Support/Ai/Capabilities/OptimizeOrderRouteCapability.php +240 -0
- package/server/src/Support/Ai/Capabilities/OrderInsightsCapability.php +95 -0
- package/server/src/Support/Ai/Capabilities/SearchResourcesCapability.php +195 -0
- package/server/src/Support/Ai/FleetOpsAiQueryResources.php +150 -0
- package/server/src/Support/Analytics/RevenueTrend.php +3 -8
- package/server/src/Support/Metrics/ActiveRevenueQuery.php +121 -0
- package/server/src/Support/Metrics/AvgOrderValueMetric.php +1 -7
- package/server/src/Support/Metrics/EarningsMetric.php +1 -10
- package/server/src/Support/Telematics/Providers/AfaqyProvider.php +104 -5
- package/server/src/Support/Telematics/Providers/SafeeProvider.php +550 -54
- package/server/src/Support/Telematics/TelematicService.php +28 -9
- package/server/src/Support/Utils.php +52 -0
- package/server/src/routes.php +67 -14
- package/server/tests/AiCreateOrderPreviewCapabilityTest.php +131 -0
- package/server/tests/AiOperationalQueryCapabilityTest.php +75 -0
- package/server/tests/ExportCoverageTest.php +79 -0
- package/server/tests/FleetHierarchyResourceTest.php +12 -0
- package/server/tests/MetricsRegistryTest.php +45 -5
- package/server/tests/OrderPayloadPlaceResolutionTest.php +53 -0
- package/server/tests/PointResolutionTest.php +63 -0
- package/server/tests/PublicFleetOpsRestEndpointsTest.php +191 -0
- package/server/tests/TelematicsHardeningTest.php +636 -1
- package/addon/components/table/cell/dropdown/action-item.hbs +0 -31
- package/addon/components/table/cell/dropdown/action-item.js +0 -62
- package/addon/components/table/cell/dropdown.hbs +0 -28
- package/addon/components/table/cell/dropdown.js +0 -91
- package/app/components/table/cell/dropdown/action-item.js +0 -1
- 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
|
|
197
|
+
$event = null;
|
|
198
|
+
$events = [];
|
|
198
199
|
try {
|
|
199
|
-
$
|
|
200
|
-
if (
|
|
201
|
-
$
|
|
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
|
|
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([
|
|
775
|
+
return array_merge([
|
|
776
|
+
'sensor_key' => $name,
|
|
777
|
+
'name' => $name,
|
|
778
|
+
], $sensor);
|
|
761
779
|
}
|
|
762
780
|
|
|
763
781
|
return [
|
|
764
|
-
'
|
|
765
|
-
'
|
|
766
|
-
'
|
|
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
|
*
|
package/server/src/routes.php
CHANGED
|
@@ -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
|
+
});
|