@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
@@ -0,0 +1,49 @@
1
+ <?php
2
+
3
+ namespace Fleetbase\FleetOps\Support\Ai\Capabilities;
4
+
5
+ use Fleetbase\Ai\Models\AiTask;
6
+
7
+ class DocsHelpCapability extends AbstractFleetOpsAICapability
8
+ {
9
+ public function key(): string
10
+ {
11
+ return 'fleet-ops.docs_help';
12
+ }
13
+
14
+ public function label(): string
15
+ {
16
+ return 'Fleet-Ops docs help';
17
+ }
18
+
19
+ public function description(): string
20
+ {
21
+ return 'Provides curated official Fleetbase documentation references for Fleet-Ops how-to questions.';
22
+ }
23
+
24
+ public function resolve(AiTask $task): array
25
+ {
26
+ $prompt = $this->prompt($task);
27
+
28
+ return [
29
+ 'references' => array_values(array_filter([
30
+ $this->reference('Fleet-Ops overview', 'https://fleetbase.io/docs/fleet-ops', 'Fleet-Ops capabilities, concepts, orders, resources, maintenance, connectivity, analytics, and reports.'),
31
+ $this->containsAny($prompt, ['order', 'dispatch', 'create']) ? $this->reference('Fleet-Ops quickstart', 'https://fleetbase.io/docs/fleet-ops/getting-started/quickstart', 'Create your first order, assign a driver, and dispatch.') : null,
32
+ $this->containsAny($prompt, ['driver app', 'navigator', 'mobile']) ? $this->reference('Navigator app setup', 'https://fleetbase.io/docs/fleet-ops/getting-started/navigator-app-setup', 'Set up Navigator for drivers.') : null,
33
+ $this->containsAny($prompt, ['maintenance', 'work order', 'parts']) ? $this->reference('Maintenance overview', 'https://fleetbase.io/docs/fleet-ops/maintenance', 'Maintenance schedules, work orders, equipment, and parts.') : null,
34
+ $this->containsAny($prompt, ['telematic', 'device', 'sensor', 'connectivity']) ? $this->reference('Connectivity overview', 'https://fleetbase.io/docs/fleet-ops/connectivity', 'Telematics, devices, sensors, and connectivity events.') : null,
35
+ $this->containsAny($prompt, ['analytics', 'report', 'reports']) ? $this->reference('Analytics overview', 'https://fleetbase.io/docs/fleet-ops/analytics', 'Fleet-Ops analytics and report workflows.') : null,
36
+ ])),
37
+ ];
38
+ }
39
+
40
+ protected function matchesPrompt(string $prompt): bool
41
+ {
42
+ return $this->containsAny($prompt, ['how do i', 'how to', 'docs', 'documentation', 'help me', 'where do i', 'guide']);
43
+ }
44
+
45
+ protected function reference(string $title, string $url, string $description): array
46
+ {
47
+ return compact('title', 'url', 'description');
48
+ }
49
+ }
@@ -0,0 +1,66 @@
1
+ <?php
2
+
3
+ namespace Fleetbase\FleetOps\Support\Ai\Capabilities;
4
+
5
+ use Fleetbase\Ai\Models\AiTask;
6
+
7
+ class ImportOrdersPreviewCapability extends AbstractFleetOpsAICapability
8
+ {
9
+ public function key(): string
10
+ {
11
+ return 'fleet-ops.import_orders_preview';
12
+ }
13
+
14
+ public function label(): string
15
+ {
16
+ return 'Import Fleet-Ops orders preview';
17
+ }
18
+
19
+ public function description(): string
20
+ {
21
+ return 'Explains and drafts preview-only Fleet-Ops import requirements for order/resource spreadsheets.';
22
+ }
23
+
24
+ public function type(): string
25
+ {
26
+ return 'action';
27
+ }
28
+
29
+ public function mode(): string
30
+ {
31
+ return 'preview';
32
+ }
33
+
34
+ public function permissions(): array
35
+ {
36
+ return ['fleet-ops import order'];
37
+ }
38
+
39
+ public function resolve(AiTask $task): array
40
+ {
41
+ return [
42
+ 'preview_only' => true,
43
+ 'action' => 'import_orders',
44
+ 'authorized' => $this->can('fleet-ops import order'),
45
+ 'message' => 'This pilot can inspect intent and explain import requirements, but it will not process uploaded spreadsheets.',
46
+ 'accepted_sources' => ['xlsx', 'csv'],
47
+ 'minimum_columns' => [
48
+ 'pickup address or pickup place',
49
+ 'dropoff address or dropoff place',
50
+ 'customer/contact reference',
51
+ 'order type/configuration when required',
52
+ 'item/entity details when required',
53
+ ],
54
+ 'draft_hints' => [
55
+ 'Ask the user to upload a spreadsheet through the Fleet-Ops import flow.',
56
+ 'Do not claim rows were imported.',
57
+ 'Offer to summarize expected columns or validation issues once file inspection support exists.',
58
+ ],
59
+ ];
60
+ }
61
+
62
+ protected function matchesPrompt(string $prompt): bool
63
+ {
64
+ return $this->containsAny($prompt, ['import', 'spreadsheet', 'excel', 'xlsx', 'csv']) && $this->containsAny($prompt, ['order', 'orders', 'resources', 'vehicles', 'drivers']);
65
+ }
66
+ }
@@ -0,0 +1,319 @@
1
+ <?php
2
+
3
+ namespace Fleetbase\FleetOps\Support\Ai\Capabilities;
4
+
5
+ use Fleetbase\Ai\Models\AiTask;
6
+ use Fleetbase\Ai\Services\AiQueryExecutor;
7
+ use Fleetbase\Ai\Support\AiRelativeDateResolver;
8
+ use Fleetbase\FleetOps\Models\Driver;
9
+ use Fleetbase\FleetOps\Models\ServiceArea;
10
+ use Fleetbase\FleetOps\Models\Zone;
11
+ use Illuminate\Database\Eloquent\Builder;
12
+
13
+ class OperationalQueryCapability extends AbstractFleetOpsAICapability
14
+ {
15
+ public function key(): string
16
+ {
17
+ return 'fleet-ops.operational_query';
18
+ }
19
+
20
+ public function label(): string
21
+ {
22
+ return 'Fleet-Ops operational query';
23
+ }
24
+
25
+ public function description(): string
26
+ {
27
+ return 'Executes safe allowlisted Fleet-Ops operational queries and aggregate summaries.';
28
+ }
29
+
30
+ public function permissions(): array
31
+ {
32
+ return [
33
+ 'fleet-ops list driver',
34
+ 'fleet-ops list vehicle',
35
+ 'fleet-ops list device',
36
+ 'fleet-ops list order',
37
+ 'fleet-ops list fleet',
38
+ 'fleet-ops list service-area',
39
+ 'fleet-ops list zone',
40
+ ];
41
+ }
42
+
43
+ public function resolve(AiTask $task): array
44
+ {
45
+ $prompt = $this->prompt($task);
46
+ $executor = app(AiQueryExecutor::class);
47
+ $queries = [];
48
+
49
+ if ($this->mentions($prompt, ['driver', 'drivers'])) {
50
+ $queries['drivers'] = $this->driverQueries($prompt, $executor);
51
+ }
52
+
53
+ if ($this->mentions($prompt, ['vehicle', 'vehicles'])) {
54
+ $queries['vehicles'] = $this->onlineResourceQueries('fleet-ops.vehicles', $prompt, $executor);
55
+ }
56
+
57
+ if ($this->mentions($prompt, ['device', 'devices'])) {
58
+ $queries['devices'] = $this->onlineResourceQueries('fleet-ops.devices', $prompt, $executor);
59
+ }
60
+
61
+ if ($this->mentions($prompt, ['order', 'orders'])) {
62
+ $queries['orders'] = $this->orderQueries($prompt, $executor);
63
+ }
64
+
65
+ if ($this->mentions($prompt, ['fleet', 'fleets'])) {
66
+ $queries['fleets'] = [
67
+ 'total' => $executor->count('fleet-ops.fleets'),
68
+ ];
69
+ }
70
+
71
+ return [
72
+ 'authorized' => true,
73
+ 'query_engine' => 'fleetbase_ai_allowlisted_operational_query',
74
+ 'instruction' => 'Answer only from these executed Fleetbase query summaries. If a requested metric is missing or empty, say that Fleetbase did not return enough data for that specific metric.',
75
+ 'queries' => array_filter($queries),
76
+ ];
77
+ }
78
+
79
+ protected function matchesPrompt(string $prompt): bool
80
+ {
81
+ return $this->mentions($prompt, ['driver', 'drivers', 'vehicle', 'vehicles', 'device', 'devices', 'order', 'orders', 'fleet', 'fleets'])
82
+ && $this->mentions($prompt, ['how many', 'count', 'where', 'located', 'location', 'majority', 'online', 'offline', 'without', 'unassigned', 'assigned', 'break down', 'status', 'service area', 'zone', 'today', 'yesterday', 'tomorrow', 'this week', 'last week', 'next week', 'this month', 'last month', 'last 30 days']);
83
+ }
84
+
85
+ protected function driverQueries(string $prompt, AiQueryExecutor $executor): array
86
+ {
87
+ $dateWindow = $this->dateWindow($prompt);
88
+ $dateFilters = $dateWindow ? $this->dateFilters($dateWindow, 'updated_at') : [];
89
+
90
+ $queries = [
91
+ 'total' => $executor->count('fleet-ops.drivers'),
92
+ 'online' => $executor->count('fleet-ops.drivers', [['field' => 'online', 'operator' => '=', 'value' => true]]),
93
+ 'offline' => $executor->count('fleet-ops.drivers', [['field' => 'online', 'operator' => 'false_or_null']]),
94
+ 'counts_by_status' => $executor->countsBy('fleet-ops.drivers', 'status'),
95
+ ];
96
+
97
+ if ($dateWindow) {
98
+ $queries['date_window'] = $this->dateWindowPayload($dateWindow, 'updated_at');
99
+ $queries['total_in_window'] = $executor->count('fleet-ops.drivers', $dateFilters);
100
+ $queries['online_in_window'] = $executor->count('fleet-ops.drivers', array_merge($dateFilters, [['field' => 'online', 'operator' => '=', 'value' => true]]));
101
+ $queries['offline_in_window'] = $executor->count('fleet-ops.drivers', array_merge($dateFilters, [['field' => 'online', 'operator' => 'false_or_null']]));
102
+ }
103
+
104
+ if ($this->mentions($prompt, ['without vehicle', 'without vehicles', 'unassigned vehicle', 'no vehicle'])) {
105
+ $queries['without_vehicle'] = [
106
+ 'count' => $executor->count('fleet-ops.drivers', [['field' => 'vehicle_uuid', 'operator' => 'null']]),
107
+ 'samples' => $executor->samples('fleet-ops.drivers', [['field' => 'vehicle_uuid', 'operator' => 'null']], 10),
108
+ ];
109
+ }
110
+
111
+ if ($this->mentions($prompt, ['where', 'located', 'location', 'majority', 'service area', 'zone'])) {
112
+ $filters = array_merge($dateFilters, $this->mentions($prompt, ['online']) ? [['field' => 'online', 'operator' => '=', 'value' => true]] : []);
113
+
114
+ $queries['location_summary'] = $executor->locationSummary('fleet-ops.drivers', $filters, 250);
115
+ $queries['service_area_distribution'] = $this->driverGeofenceDistribution($filters);
116
+ }
117
+
118
+ return $queries;
119
+ }
120
+
121
+ protected function onlineResourceQueries(string $resource, string $prompt, AiQueryExecutor $executor): array
122
+ {
123
+ $dateWindow = $this->dateWindow($prompt);
124
+ $dateField = $resource === 'fleet-ops.devices' ? 'last_online_at' : 'updated_at';
125
+ $dateFilters = $dateWindow ? $this->dateFilters($dateWindow, $dateField) : [];
126
+
127
+ $queries = [
128
+ 'total' => $executor->count($resource),
129
+ 'online' => $executor->count($resource, [['field' => 'online', 'operator' => '=', 'value' => true]]),
130
+ 'offline' => $executor->count($resource, [['field' => 'online', 'operator' => 'false_or_null']]),
131
+ 'counts_by_status' => $executor->countsBy($resource, 'status'),
132
+ ];
133
+
134
+ if ($dateWindow) {
135
+ $queries['date_window'] = $this->dateWindowPayload($dateWindow, $dateField);
136
+ $queries['total_in_window'] = $executor->count($resource, $dateFilters);
137
+ $queries['online_in_window'] = $executor->count($resource, array_merge($dateFilters, [['field' => 'online', 'operator' => '=', 'value' => true]]));
138
+ $queries['offline_in_window'] = $executor->count($resource, array_merge($dateFilters, [['field' => 'online', 'operator' => 'false_or_null']]));
139
+ }
140
+
141
+ if ($this->mentions($prompt, ['where', 'located', 'location', 'majority']) && $resource === 'fleet-ops.vehicles') {
142
+ $queries['location_summary'] = $executor->locationSummary($resource, array_merge($dateFilters, $this->mentions($prompt, ['online']) ? [['field' => 'online', 'operator' => '=', 'value' => true]] : []), 250);
143
+ }
144
+
145
+ return $queries;
146
+ }
147
+
148
+ protected function orderQueries(string $prompt, AiQueryExecutor $executor): array
149
+ {
150
+ $filters = $this->orderDateFilters($prompt);
151
+
152
+ if ($this->mentions($prompt, ['active'])) {
153
+ $filters[] = ['field' => 'status', 'operator' => 'not_in', 'value' => ['canceled', 'completed', 'expired']];
154
+ }
155
+
156
+ $queries = [
157
+ 'total' => $executor->count('fleet-ops.orders', $filters),
158
+ 'counts_by_status' => $executor->countsBy('fleet-ops.orders', 'status', $filters),
159
+ ];
160
+
161
+ if ($dateWindow = $this->dateWindow($prompt)) {
162
+ $queries['date_window'] = $this->dateWindowPayload($dateWindow, 'created_at');
163
+ }
164
+
165
+ if ($this->mentions($prompt, ['without driver', 'unassigned driver', 'no driver'])) {
166
+ $queries['without_driver'] = $executor->count('fleet-ops.orders', array_merge($filters, [['field' => 'driver_assigned_uuid', 'operator' => 'null']]));
167
+ }
168
+
169
+ if ($this->mentions($prompt, ['assigned driver', 'with driver'])) {
170
+ $queries['with_driver'] = $executor->count('fleet-ops.orders', array_merge($filters, [['field' => 'driver_assigned_uuid', 'operator' => 'not_null']]));
171
+ }
172
+
173
+ if ($this->mentions($prompt, ['without vehicle', 'unassigned vehicle', 'no vehicle'])) {
174
+ $queries['without_vehicle'] = $executor->count('fleet-ops.orders', array_merge($filters, [['field' => 'vehicle_assigned_uuid', 'operator' => 'null']]));
175
+ }
176
+
177
+ return $queries;
178
+ }
179
+
180
+ protected function driverGeofenceDistribution(array $filters = []): array
181
+ {
182
+ if (!$this->can('fleet-ops list driver')) {
183
+ return ['authorized' => false, 'resource' => 'fleet-ops.drivers'];
184
+ }
185
+
186
+ $query = Driver::where('company_uuid', session('company'))
187
+ ->applyDirectivesForPermissions('fleet-ops list driver');
188
+
189
+ foreach ($filters as $filter) {
190
+ if (($filter['field'] ?? null) === 'online') {
191
+ $query->where('online', (bool) ($filter['value'] ?? false));
192
+ }
193
+
194
+ if (($filter['field'] ?? null) === 'updated_at' && in_array($filter['operator'] ?? null, ['>=', '<='], true)) {
195
+ $query->where('updated_at', $filter['operator'], $filter['value']);
196
+ }
197
+ }
198
+
199
+ $drivers = $this->whereValidDriverLocation($query)
200
+ ->latest()
201
+ ->limit(250)
202
+ ->get(['uuid', 'public_id', 'company_uuid', 'location', 'city', 'country', 'online']);
203
+
204
+ if ($drivers->isEmpty()) {
205
+ return [
206
+ 'authorized' => true,
207
+ 'valid_location_count' => 0,
208
+ 'service_areas' => [],
209
+ 'zones' => [],
210
+ ];
211
+ }
212
+
213
+ $serviceAreaCounts = [];
214
+ $zoneCounts = [];
215
+
216
+ foreach ($drivers as $driver) {
217
+ $point = $driver->location;
218
+ if (!is_object($point) || !method_exists($point, 'getLat') || !method_exists($point, 'getLng')) {
219
+ continue;
220
+ }
221
+
222
+ $wkt = sprintf('POINT(%s %s)', $point->getLng(), $point->getLat());
223
+
224
+ if ($this->can('fleet-ops list service-area')) {
225
+ ServiceArea::where('company_uuid', session('company'))
226
+ ->whereNotNull('border')
227
+ ->whereRaw('MBRContains(`border`, ST_GeomFromText(?))', [$wkt])
228
+ ->whereRaw('ST_Contains(`border`, ST_GeomFromText(?))', [$wkt])
229
+ ->get(['uuid', 'name', 'public_id'])
230
+ ->each(function (ServiceArea $serviceArea) use (&$serviceAreaCounts) {
231
+ $key = $serviceArea->uuid;
232
+ $serviceAreaCounts[$key] ??= [
233
+ 'uuid' => $serviceArea->uuid,
234
+ 'public_id' => $serviceArea->public_id,
235
+ 'name' => $serviceArea->name,
236
+ 'count' => 0,
237
+ ];
238
+ $serviceAreaCounts[$key]['count']++;
239
+ });
240
+ }
241
+
242
+ if ($this->can('fleet-ops list zone')) {
243
+ Zone::where('company_uuid', session('company'))
244
+ ->whereNotNull('border')
245
+ ->whereRaw('MBRContains(`border`, ST_GeomFromText(?))', [$wkt])
246
+ ->whereRaw('ST_Contains(`border`, ST_GeomFromText(?))', [$wkt])
247
+ ->get(['uuid', 'name', 'public_id', 'service_area_uuid'])
248
+ ->each(function (Zone $zone) use (&$zoneCounts) {
249
+ $key = $zone->uuid;
250
+ $zoneCounts[$key] ??= [
251
+ 'uuid' => $zone->uuid,
252
+ 'public_id' => $zone->public_id,
253
+ 'name' => $zone->name,
254
+ 'service_area_uuid' => $zone->service_area_uuid,
255
+ 'count' => 0,
256
+ ];
257
+ $zoneCounts[$key]['count']++;
258
+ });
259
+ }
260
+ }
261
+
262
+ return [
263
+ 'authorized' => true,
264
+ 'valid_location_count' => $drivers->count(),
265
+ 'service_areas' => collect($serviceAreaCounts)->sortByDesc('count')->take(10)->values()->all(),
266
+ 'zones' => collect($zoneCounts)->sortByDesc('count')->take(10)->values()->all(),
267
+ ];
268
+ }
269
+
270
+ protected function whereValidDriverLocation(Builder $query): Builder
271
+ {
272
+ return $query->whereNotNull('location')->whereRaw('
273
+ ST_Y(location) BETWEEN -90 AND 90
274
+ AND ST_X(location) BETWEEN -180 AND 180
275
+ AND NOT (ST_X(location) = 0 AND ST_Y(location) = 0)
276
+ ');
277
+ }
278
+
279
+ protected function orderDateFilters(string $prompt): array
280
+ {
281
+ $window = $this->dateWindow($prompt);
282
+
283
+ return $window ? $this->dateFilters($window, 'created_at') : [];
284
+ }
285
+
286
+ protected function dateWindow(string $prompt): ?array
287
+ {
288
+ return $this->relativeDateResolver()->resolveWindow($prompt);
289
+ }
290
+
291
+ protected function dateFilters(array $window, string $field): array
292
+ {
293
+ return [
294
+ ['field' => $field, 'operator' => '>=', 'value' => $window['start']],
295
+ ['field' => $field, 'operator' => '<=', 'value' => $window['end']],
296
+ ];
297
+ }
298
+
299
+ protected function dateWindowPayload(array $window, string $field): array
300
+ {
301
+ return [
302
+ 'label' => $window['label'],
303
+ 'timezone' => $window['timezone'],
304
+ 'field' => $field,
305
+ 'start' => $window['start']->toIso8601String(),
306
+ 'end' => $window['end']->toIso8601String(),
307
+ ];
308
+ }
309
+
310
+ protected function relativeDateResolver(): AiRelativeDateResolver
311
+ {
312
+ return function_exists('app') ? app(AiRelativeDateResolver::class) : new AiRelativeDateResolver(null);
313
+ }
314
+
315
+ protected function mentions(string $prompt, array $terms): bool
316
+ {
317
+ return $this->containsAny($prompt, $terms);
318
+ }
319
+ }
@@ -0,0 +1,240 @@
1
+ <?php
2
+
3
+ namespace Fleetbase\FleetOps\Support\Ai\Capabilities;
4
+
5
+ use Fleetbase\Ai\Contracts\AIActionCapabilityInterface;
6
+ use Fleetbase\Ai\Models\AiTask;
7
+ use Fleetbase\FleetOps\Models\Order;
8
+ use Fleetbase\FleetOps\Models\Place;
9
+ use Illuminate\Support\Facades\DB;
10
+
11
+ class OptimizeOrderRouteCapability extends AbstractFleetOpsAICapability implements AIActionCapabilityInterface
12
+ {
13
+ public function key(): string
14
+ {
15
+ return 'fleet-ops.optimize_order_route';
16
+ }
17
+
18
+ public function label(): string
19
+ {
20
+ return 'Optimize Fleet-Ops order route';
21
+ }
22
+
23
+ public function description(): string
24
+ {
25
+ return 'Previews and applies confirmed waypoint resequencing for a Fleet-Ops order route.';
26
+ }
27
+
28
+ public function type(): string
29
+ {
30
+ return 'action';
31
+ }
32
+
33
+ public function mode(): string
34
+ {
35
+ return 'confirmation_required';
36
+ }
37
+
38
+ public function permissions(): array
39
+ {
40
+ return ['fleet-ops optimize order', 'fleet-ops update-route-for order'];
41
+ }
42
+
43
+ public function previewOnly(): bool
44
+ {
45
+ return false;
46
+ }
47
+
48
+ public function executable(): bool
49
+ {
50
+ return true;
51
+ }
52
+
53
+ public function inputSchema(): array
54
+ {
55
+ return [
56
+ 'order' => 'Fleet-Ops order UUID or public ID.',
57
+ 'waypoints' => 'Ordered waypoint place UUIDs generated by the preview.',
58
+ ];
59
+ }
60
+
61
+ public function shouldPreview(AiTask $task): bool
62
+ {
63
+ return $this->matchesPrompt($this->prompt($task));
64
+ }
65
+
66
+ public function resolve(AiTask $task): array
67
+ {
68
+ return $this->preview($task);
69
+ }
70
+
71
+ public function preview(AiTask $task, array $input = []): array
72
+ {
73
+ $authorized = $this->canAll($this->permissions());
74
+ $orders = $this->resolveOrders($task);
75
+
76
+ if ($orders->count() > 1) {
77
+ return [
78
+ 'action' => $this->key(),
79
+ 'authorized' => $authorized,
80
+ 'ready' => false,
81
+ 'preview_only' => true,
82
+ 'message' => 'Fleetbase AI found multiple orders. Multi-order route sequencing should use the Fleet-Ops Orchestrator preview before commit.',
83
+ 'apply_label' => 'Open Orchestrator',
84
+ 'cancel_label' => 'Cancel',
85
+ 'missing_fields' => ['single order selection for direct route apply'],
86
+ 'fields' => $orders->map(fn (Order $order) => ['label' => 'Order', 'value' => $order->public_id ?: $order->uuid])->values()->all(),
87
+ 'draft' => ['orders' => $orders->pluck('public_id')->filter()->values()->all(), 'mode' => 'optimize_routes'],
88
+ 'risks' => ['Multi-order optimization can affect manifests and should be committed through Orchestrator review.'],
89
+ ];
90
+ }
91
+
92
+ $order = $orders->first();
93
+ if (!$order) {
94
+ return [
95
+ 'action' => $this->key(),
96
+ 'authorized' => $authorized,
97
+ 'ready' => false,
98
+ 'message' => 'Fleetbase AI needs a specific Fleet-Ops order ID before it can optimize a route.',
99
+ 'apply_label' => 'Optimize route',
100
+ 'cancel_label' => 'Cancel',
101
+ 'missing_fields' => ['order UUID or public ID'],
102
+ 'fields' => [],
103
+ 'draft' => [],
104
+ ];
105
+ }
106
+
107
+ $order->loadMissing(['payload.pickup', 'payload.dropoff', 'payload.waypoints.place']);
108
+ $waypoints = $order->payload?->waypoints?->sortBy('order')->values() ?? collect();
109
+ $optimized = $this->optimizeWaypoints($order, $waypoints);
110
+ $changed = $waypoints->pluck('place_uuid')->values()->all() !== $optimized->pluck('place_uuid')->values()->all();
111
+ $missing = array_values(array_filter([
112
+ $authorized ? null : 'permission to optimize and update Fleet-Ops order routes',
113
+ $waypoints->count() >= 2 ? null : 'at least two route waypoints',
114
+ $changed ? null : 'a route with a different optimized waypoint sequence',
115
+ ]));
116
+
117
+ return [
118
+ 'action' => $this->key(),
119
+ 'authorized' => $authorized,
120
+ 'ready' => empty($missing),
121
+ 'message' => empty($missing) ? 'Fleetbase AI prepared an optimized waypoint sequence. Review it before applying.' : 'Fleetbase AI cannot apply a route optimization until the route has optimizable waypoints.',
122
+ 'apply_label' => 'Apply route',
123
+ 'cancel_label' => 'Cancel',
124
+ 'missing_fields' => $missing,
125
+ 'fields' => [
126
+ ['label' => 'Order', 'value' => $order->public_id ?: $order->uuid],
127
+ ['label' => 'Current sequence', 'value' => $this->stopLabels($waypoints)->implode(' -> ')],
128
+ ['label' => 'Proposed sequence', 'value' => $this->stopLabels($optimized)->implode(' -> ')],
129
+ ],
130
+ 'draft' => [
131
+ 'order_uuid' => $order->uuid,
132
+ 'waypoints' => $optimized->values()->map(fn ($waypoint, int $index) => [
133
+ 'place_uuid' => $waypoint->place_uuid,
134
+ 'order' => $index,
135
+ 'type' => $waypoint->type ?: 'dropoff',
136
+ ])->all(),
137
+ ],
138
+ 'risks' => ['Applying this changes the order waypoint sequence only after confirmation.'],
139
+ ];
140
+ }
141
+
142
+ public function apply(AiTask $task, array $preview = [], array $input = []): array
143
+ {
144
+ if (!$this->canAll($this->permissions())) {
145
+ throw new \RuntimeException('You do not have permission to optimize Fleet-Ops order routes.');
146
+ }
147
+
148
+ if (!data_get($preview, 'ready')) {
149
+ throw new \RuntimeException('This route optimization preview is not ready to apply.');
150
+ }
151
+
152
+ $order = Order::where('company_uuid', session('company'))
153
+ ->where('uuid', data_get($preview, 'draft.order_uuid'))
154
+ ->with(['payload'])
155
+ ->first();
156
+
157
+ if (!$order || !$order->payload) {
158
+ throw new \RuntimeException('Unable to find the Fleet-Ops order route to update.');
159
+ }
160
+
161
+ $waypoints = array_values((array) data_get($preview, 'draft.waypoints', []));
162
+ if (count($waypoints) < 2) {
163
+ throw new \RuntimeException('The route optimization preview does not include enough waypoints to apply.');
164
+ }
165
+
166
+ DB::transaction(function () use ($order, $waypoints) {
167
+ $order->payload->updateWaypoints($waypoints);
168
+ $order->is_route_optimized = true;
169
+ $order->save();
170
+ });
171
+
172
+ return [
173
+ 'action' => $this->key(),
174
+ 'status' => 'completed',
175
+ 'message' => 'Fleet-Ops order route was optimized.',
176
+ 'resource' => [
177
+ 'type' => 'order',
178
+ 'id' => $order->public_id,
179
+ 'uuid' => $order->uuid,
180
+ 'route' => 'console.fleet-ops.operations.orders.index.details',
181
+ 'models' => array_filter([$order->public_id ?: $order->uuid]),
182
+ ],
183
+ ];
184
+ }
185
+
186
+ protected function matchesPrompt(string $prompt): bool
187
+ {
188
+ return str_contains($prompt, 'order') && $this->containsAny($prompt, ['optimize route', 'optimise route', 'route optimization', 'route optimisation', 'optimize the route', 'optimise the route']);
189
+ }
190
+
191
+ protected function resolveOrders(AiTask $task)
192
+ {
193
+ $terms = $this->searchTerms((string) $task->prompt);
194
+
195
+ return Order::where('company_uuid', session('company'))
196
+ ->where(function ($query) use ($terms) {
197
+ $this->whereLikeAny($query, ['uuid', 'public_id', 'internal_id'], $terms);
198
+ })
199
+ ->with(['payload.pickup', 'payload.dropoff', 'payload.waypoints.place'])
200
+ ->limit(10)
201
+ ->get();
202
+ }
203
+
204
+ protected function optimizeWaypoints(Order $order, $waypoints)
205
+ {
206
+ if ($waypoints->count() < 2) {
207
+ return $waypoints;
208
+ }
209
+
210
+ $remaining = $waypoints->values();
211
+ $optimized = collect();
212
+ $cursor = $order->payload?->pickup;
213
+
214
+ while ($remaining->isNotEmpty()) {
215
+ $nearest = $remaining
216
+ ->sortBy(fn ($waypoint) => $this->distance($cursor, $waypoint->place))
217
+ ->first();
218
+
219
+ $optimized->push($nearest);
220
+ $remaining = $remaining->reject(fn ($waypoint) => $waypoint->uuid === $nearest->uuid)->values();
221
+ $cursor = $nearest->place;
222
+ }
223
+
224
+ return $optimized;
225
+ }
226
+
227
+ protected function distance(?Place $from, ?Place $to): float
228
+ {
229
+ if (!$from || !$to || !is_numeric($from->lat) || !is_numeric($from->lng) || !is_numeric($to->lat) || !is_numeric($to->lng)) {
230
+ return PHP_FLOAT_MAX;
231
+ }
232
+
233
+ return sqrt((float) (($from->lat - $to->lat) ** 2 + ($from->lng - $to->lng) ** 2));
234
+ }
235
+
236
+ protected function stopLabels($waypoints)
237
+ {
238
+ return $waypoints->map(fn ($waypoint) => $waypoint->place?->name ?: $waypoint->place?->public_id ?: $waypoint->place_uuid);
239
+ }
240
+ }