@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
|
@@ -28,8 +28,9 @@ class SyncTelematicDevicesJob implements ShouldQueue
|
|
|
28
28
|
public Telematic $telematic;
|
|
29
29
|
public array $options;
|
|
30
30
|
public string $jobId;
|
|
31
|
-
public int $tries
|
|
32
|
-
public int $timeout
|
|
31
|
+
public int $tries = 1;
|
|
32
|
+
public int $timeout = 3600;
|
|
33
|
+
public bool $failOnTimeout = true;
|
|
33
34
|
|
|
34
35
|
/**
|
|
35
36
|
* Create a new job instance.
|
|
@@ -79,12 +80,22 @@ class SyncTelematicDevicesJob implements ShouldQueue
|
|
|
79
80
|
$cursor = null;
|
|
80
81
|
$totalFetched = 0;
|
|
81
82
|
$totalLinked = 0;
|
|
83
|
+
$totalLinkAttempts = 0;
|
|
82
84
|
$totalEvents = 0;
|
|
83
85
|
$totalSensors = 0;
|
|
84
86
|
$totalSkipped = 0;
|
|
85
87
|
$pageCount = 0;
|
|
86
88
|
$lastProviderAllCount = null;
|
|
87
89
|
$lastProviderFiltersCount = null;
|
|
90
|
+
$providerSyncMeta = [];
|
|
91
|
+
$linkedDeviceKeys = [];
|
|
92
|
+
$inventoryPayloads = [];
|
|
93
|
+
$inventoryFetched = 0;
|
|
94
|
+
$inventoryLinked = 0;
|
|
95
|
+
$inventorySkipped = 0;
|
|
96
|
+
$totalEnrichment = 0;
|
|
97
|
+
$totalEnrichmentCompleted = 0;
|
|
98
|
+
$totalEnrichmentFailures = 0;
|
|
88
99
|
|
|
89
100
|
try {
|
|
90
101
|
$provider = $registry->resolve($this->telematic->provider);
|
|
@@ -97,10 +108,12 @@ class SyncTelematicDevicesJob implements ShouldQueue
|
|
|
97
108
|
'filters' => $this->options['filters'] ?? [],
|
|
98
109
|
]);
|
|
99
110
|
|
|
100
|
-
$devices
|
|
101
|
-
$pagination
|
|
111
|
+
$devices = $response['devices'] ?? [];
|
|
112
|
+
$pagination = $response['pagination'] ?? [];
|
|
113
|
+
$providerSyncMeta = array_merge($providerSyncMeta, $response['sync_meta'] ?? []);
|
|
102
114
|
$pageCount++;
|
|
103
115
|
$totalFetched += count($devices);
|
|
116
|
+
$inventoryPayloads = array_merge($inventoryPayloads, $devices);
|
|
104
117
|
|
|
105
118
|
if (isset($pagination['allCount'])) {
|
|
106
119
|
$lastProviderAllCount = $pagination['allCount'];
|
|
@@ -128,10 +141,15 @@ class SyncTelematicDevicesJob implements ShouldQueue
|
|
|
128
141
|
$normalizedDevice = $provider->normalizeDevice($devicePayload);
|
|
129
142
|
try {
|
|
130
143
|
$result = $service->ingestDeviceSnapshot($this->telematic, $provider, $devicePayload);
|
|
131
|
-
$
|
|
132
|
-
|
|
133
|
-
|
|
144
|
+
$totalLinkAttempts++;
|
|
145
|
+
$linkedDeviceKey = $this->resolveLinkedDeviceKey($result, $normalizedDevice);
|
|
146
|
+
|
|
147
|
+
if ($linkedDeviceKey && !isset($linkedDeviceKeys[$linkedDeviceKey])) {
|
|
148
|
+
$linkedDeviceKeys[$linkedDeviceKey] = true;
|
|
149
|
+
$totalLinked++;
|
|
134
150
|
}
|
|
151
|
+
|
|
152
|
+
$totalEvents += count($result['events'] ?? array_filter([$result['event'] ?? null]));
|
|
135
153
|
$totalSensors += $result['sensors'] ?? 0;
|
|
136
154
|
} catch (\Illuminate\Validation\ValidationException $e) {
|
|
137
155
|
$totalSkipped++;
|
|
@@ -152,6 +170,7 @@ class SyncTelematicDevicesJob implements ShouldQueue
|
|
|
152
170
|
'correlation_id' => $correlationId,
|
|
153
171
|
'fetched' => $totalFetched,
|
|
154
172
|
'linked' => $totalLinked,
|
|
173
|
+
'link_attempts' => $totalLinkAttempts,
|
|
155
174
|
'events' => $totalEvents,
|
|
156
175
|
'sensors' => $totalSensors,
|
|
157
176
|
'skipped' => $totalSkipped,
|
|
@@ -161,10 +180,102 @@ class SyncTelematicDevicesJob implements ShouldQueue
|
|
|
161
180
|
// Broadcast progress (TODO: implement WebSocket broadcasting)
|
|
162
181
|
} while (($response['has_more'] ?? false) && $cursor);
|
|
163
182
|
|
|
183
|
+
$inventoryFetched = $totalFetched;
|
|
184
|
+
$inventoryLinked = $totalLinked;
|
|
185
|
+
$inventorySkipped = $totalSkipped;
|
|
186
|
+
|
|
187
|
+
$this->telematic->status = 'synchronizing';
|
|
188
|
+
$this->telematic->meta = array_merge($this->telematic->meta ?? [], $providerSyncMeta, [
|
|
189
|
+
'last_sync_job_id' => $this->jobId,
|
|
190
|
+
'last_sync_result' => 'inventory_synced',
|
|
191
|
+
'last_sync_total' => $totalLinked,
|
|
192
|
+
'last_sync_inventory_completed_at' => now()->toDateTimeString(),
|
|
193
|
+
'last_sync_inventory_total' => $inventoryFetched,
|
|
194
|
+
'last_sync_inventory_linked_total' => $inventoryLinked,
|
|
195
|
+
'last_sync_inventory_skipped_total' => $inventorySkipped,
|
|
196
|
+
'last_sync_fetched_total' => $totalFetched,
|
|
197
|
+
'last_sync_linked_total' => $totalLinked,
|
|
198
|
+
'last_sync_link_attempts_total' => $totalLinkAttempts,
|
|
199
|
+
'last_sync_skipped_total' => $totalSkipped,
|
|
200
|
+
'last_sync_page_count' => $pageCount,
|
|
201
|
+
'last_sync_provider_total' => $lastProviderFiltersCount ?? $lastProviderAllCount,
|
|
202
|
+
'last_sync_provider_all_count' => $lastProviderAllCount,
|
|
203
|
+
'last_sync_provider_filters_count' => $lastProviderFiltersCount,
|
|
204
|
+
]);
|
|
205
|
+
$this->telematic->save();
|
|
206
|
+
|
|
207
|
+
Log::info($this->telematic->provider === 'safee' ? 'Safee inventory sync completed' : 'Telematics inventory sync completed', [
|
|
208
|
+
'correlation_id' => $correlationId,
|
|
209
|
+
'telematic_uuid' => $this->telematic->uuid,
|
|
210
|
+
'provider' => $this->telematic->provider,
|
|
211
|
+
'inventory_total' => $inventoryFetched,
|
|
212
|
+
'inventory_linked' => $inventoryLinked,
|
|
213
|
+
'skipped' => $inventorySkipped,
|
|
214
|
+
]);
|
|
215
|
+
|
|
216
|
+
if (method_exists($provider, 'fetchDeviceTelemetrySnapshots') && !empty($inventoryPayloads)) {
|
|
217
|
+
Log::info($this->telematic->provider === 'safee' ? 'Safee telemetry enrichment started' : 'Telematics telemetry enrichment started', [
|
|
218
|
+
'correlation_id' => $correlationId,
|
|
219
|
+
'telematic_uuid' => $this->telematic->uuid,
|
|
220
|
+
'provider' => $this->telematic->provider,
|
|
221
|
+
'device_count' => count($inventoryPayloads),
|
|
222
|
+
]);
|
|
223
|
+
|
|
224
|
+
$enrichmentResponse = $provider->fetchDeviceTelemetrySnapshots($inventoryPayloads, [
|
|
225
|
+
'limit' => $this->options['limit'] ?? null,
|
|
226
|
+
'filters' => $this->options['filters'] ?? [],
|
|
227
|
+
]);
|
|
228
|
+
$providerSyncMeta = array_merge($providerSyncMeta, $enrichmentResponse['sync_meta'] ?? []);
|
|
229
|
+
$enrichedDevices = $enrichmentResponse['devices'] ?? [];
|
|
230
|
+
$totalEnrichment += count($enrichedDevices);
|
|
231
|
+
|
|
232
|
+
foreach ($enrichedDevices as $devicePayload) {
|
|
233
|
+
$normalizedDevice = $provider->normalizeDevice($devicePayload);
|
|
234
|
+
try {
|
|
235
|
+
$result = $service->ingestDeviceSnapshot($this->telematic, $provider, $devicePayload);
|
|
236
|
+
$totalLinkAttempts++;
|
|
237
|
+
$linkedDeviceKey = $this->resolveLinkedDeviceKey($result, $normalizedDevice);
|
|
238
|
+
|
|
239
|
+
if ($linkedDeviceKey && !isset($linkedDeviceKeys[$linkedDeviceKey])) {
|
|
240
|
+
$linkedDeviceKeys[$linkedDeviceKey] = true;
|
|
241
|
+
$totalLinked++;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
$totalEnrichmentCompleted++;
|
|
245
|
+
$totalEvents += count($result['events'] ?? array_filter([$result['event'] ?? null]));
|
|
246
|
+
$totalSensors += $result['sensors'] ?? 0;
|
|
247
|
+
} catch (\Illuminate\Validation\ValidationException $e) {
|
|
248
|
+
$totalSkipped++;
|
|
249
|
+
$totalEnrichmentFailures++;
|
|
250
|
+
Log::warning('Skipping telematics enrichment without provider identity', [
|
|
251
|
+
'correlation_id' => $correlationId,
|
|
252
|
+
'provider' => $this->telematic->provider,
|
|
253
|
+
'provider_unit_id' => $devicePayload['_id'] ?? $devicePayload['id'] ?? null,
|
|
254
|
+
'device_id' => $normalizedDevice['device_id'] ?? null,
|
|
255
|
+
'name' => $normalizedDevice['name'] ?? $devicePayload['name'] ?? null,
|
|
256
|
+
'imei' => $devicePayload['imei'] ?? null,
|
|
257
|
+
]);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
Log::info($this->telematic->provider === 'safee' ? 'Safee telemetry enrichment completed' : 'Telematics telemetry enrichment completed', [
|
|
262
|
+
'correlation_id' => $correlationId,
|
|
263
|
+
'telematic_uuid' => $this->telematic->uuid,
|
|
264
|
+
'provider' => $this->telematic->provider,
|
|
265
|
+
'total' => $totalEnrichment,
|
|
266
|
+
'completed' => $totalEnrichmentCompleted,
|
|
267
|
+
'failures' => $totalEnrichmentFailures,
|
|
268
|
+
]);
|
|
269
|
+
}
|
|
270
|
+
|
|
164
271
|
Log::info('Device discovery completed', [
|
|
165
272
|
'correlation_id' => $correlationId,
|
|
166
273
|
'total_fetched' => $totalFetched,
|
|
167
274
|
'total_linked' => $totalLinked,
|
|
275
|
+
'total_link_attempts' => $totalLinkAttempts,
|
|
276
|
+
'total_enrichment' => $totalEnrichment,
|
|
277
|
+
'enrichment_completed' => $totalEnrichmentCompleted,
|
|
278
|
+
'enrichment_failures' => $totalEnrichmentFailures,
|
|
168
279
|
'total_events' => $totalEvents,
|
|
169
280
|
'total_sensors' => $totalSensors,
|
|
170
281
|
'total_skipped' => $totalSkipped,
|
|
@@ -174,22 +285,29 @@ class SyncTelematicDevicesJob implements ShouldQueue
|
|
|
174
285
|
]);
|
|
175
286
|
|
|
176
287
|
$this->telematic->status = 'active';
|
|
177
|
-
$this->telematic->meta = array_merge($this->telematic->meta ?? [], [
|
|
178
|
-
'last_sync_job_id'
|
|
179
|
-
'last_sync_completed_at'
|
|
180
|
-
'last_sync_result'
|
|
181
|
-
'last_sync_total'
|
|
182
|
-
'last_sync_fetched_total'
|
|
183
|
-
'last_sync_linked_total'
|
|
184
|
-
'
|
|
185
|
-
'
|
|
186
|
-
'
|
|
187
|
-
'
|
|
188
|
-
'
|
|
189
|
-
'
|
|
190
|
-
'
|
|
191
|
-
'
|
|
192
|
-
'
|
|
288
|
+
$this->telematic->meta = array_merge($this->telematic->meta ?? [], $providerSyncMeta, [
|
|
289
|
+
'last_sync_job_id' => $this->jobId,
|
|
290
|
+
'last_sync_completed_at' => now()->toDateTimeString(),
|
|
291
|
+
'last_sync_result' => 'success',
|
|
292
|
+
'last_sync_total' => $totalLinked,
|
|
293
|
+
'last_sync_fetched_total' => $totalFetched,
|
|
294
|
+
'last_sync_linked_total' => $totalLinked,
|
|
295
|
+
'last_sync_link_attempts_total' => $totalLinkAttempts,
|
|
296
|
+
'last_sync_inventory_total' => $inventoryFetched,
|
|
297
|
+
'last_sync_inventory_linked_total' => $inventoryLinked,
|
|
298
|
+
'last_sync_inventory_skipped_total' => $inventorySkipped,
|
|
299
|
+
'last_sync_enrichment_total' => $totalEnrichment,
|
|
300
|
+
'last_sync_enrichment_completed' => $totalEnrichmentCompleted,
|
|
301
|
+
'last_sync_enrichment_failures' => $totalEnrichmentFailures,
|
|
302
|
+
'last_sync_events_total' => $totalEvents,
|
|
303
|
+
'last_sync_sensors_total' => $totalSensors,
|
|
304
|
+
'last_sync_skipped_total' => $totalSkipped,
|
|
305
|
+
'last_sync_page_count' => $pageCount,
|
|
306
|
+
'last_sync_provider_total' => $lastProviderFiltersCount ?? $lastProviderAllCount,
|
|
307
|
+
'last_sync_provider_all_count' => $lastProviderAllCount,
|
|
308
|
+
'last_sync_provider_filters_count' => $lastProviderFiltersCount,
|
|
309
|
+
'last_sync_error' => null,
|
|
310
|
+
'last_sync_error_context' => null,
|
|
193
311
|
]);
|
|
194
312
|
$this->telematic->save();
|
|
195
313
|
} catch (\Exception $e) {
|
|
@@ -205,21 +323,28 @@ class SyncTelematicDevicesJob implements ShouldQueue
|
|
|
205
323
|
|
|
206
324
|
$this->telematic->status = 'error';
|
|
207
325
|
$this->telematic->meta = array_merge($this->telematic->meta ?? [], [
|
|
208
|
-
'last_sync_job_id'
|
|
209
|
-
'last_sync_result'
|
|
210
|
-
'last_sync_fetched_total'
|
|
211
|
-
'last_sync_linked_total'
|
|
212
|
-
'
|
|
213
|
-
'
|
|
214
|
-
'
|
|
215
|
-
'
|
|
216
|
-
'
|
|
217
|
-
'
|
|
218
|
-
'
|
|
219
|
-
'
|
|
220
|
-
'
|
|
221
|
-
'
|
|
222
|
-
'
|
|
326
|
+
'last_sync_job_id' => $this->jobId,
|
|
327
|
+
'last_sync_result' => 'failed',
|
|
328
|
+
'last_sync_fetched_total' => $totalFetched,
|
|
329
|
+
'last_sync_linked_total' => $totalLinked,
|
|
330
|
+
'last_sync_link_attempts_total' => $totalLinkAttempts,
|
|
331
|
+
'last_sync_inventory_total' => $inventoryFetched,
|
|
332
|
+
'last_sync_inventory_linked_total' => $inventoryLinked,
|
|
333
|
+
'last_sync_inventory_skipped_total' => $inventorySkipped,
|
|
334
|
+
'last_sync_enrichment_total' => $totalEnrichment,
|
|
335
|
+
'last_sync_enrichment_completed' => $totalEnrichmentCompleted,
|
|
336
|
+
'last_sync_enrichment_failures' => $totalEnrichmentFailures,
|
|
337
|
+
'last_sync_events_total' => $totalEvents,
|
|
338
|
+
'last_sync_sensors_total' => $totalSensors,
|
|
339
|
+
'last_sync_skipped_total' => $totalSkipped,
|
|
340
|
+
'last_sync_page_count' => $pageCount,
|
|
341
|
+
'last_sync_provider_total' => $lastProviderFiltersCount ?? $lastProviderAllCount,
|
|
342
|
+
'last_sync_provider_all_count' => $lastProviderAllCount,
|
|
343
|
+
'last_sync_provider_filters_count' => $lastProviderFiltersCount,
|
|
344
|
+
'last_sync_error' => $failureMessage,
|
|
345
|
+
'last_sync_error_type' => class_basename($e),
|
|
346
|
+
'last_sync_error_context' => $failureContext,
|
|
347
|
+
'last_sync_failed_at' => now()->toDateTimeString(),
|
|
223
348
|
]);
|
|
224
349
|
$this->telematic->save();
|
|
225
350
|
|
|
@@ -238,6 +363,37 @@ class SyncTelematicDevicesJob implements ShouldQueue
|
|
|
238
363
|
return $this->jobId;
|
|
239
364
|
}
|
|
240
365
|
|
|
366
|
+
public function failed(\Throwable $e): void
|
|
367
|
+
{
|
|
368
|
+
$this->telematic->refresh();
|
|
369
|
+
$this->telematic->status = 'error';
|
|
370
|
+
$this->telematic->meta = array_merge($this->telematic->meta ?? [], [
|
|
371
|
+
'last_sync_job_id' => $this->jobId,
|
|
372
|
+
'last_sync_result' => 'failed',
|
|
373
|
+
'last_sync_error' => $this->safeSyncErrorMessage($e),
|
|
374
|
+
'last_sync_error_type' => class_basename($e),
|
|
375
|
+
'last_sync_failed_at' => now()->toDateTimeString(),
|
|
376
|
+
'last_sync_failed_reason' => $e instanceof \Illuminate\Queue\TimeoutExceededException ? 'job_timeout' : null,
|
|
377
|
+
]);
|
|
378
|
+
$this->telematic->save();
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
protected function resolveLinkedDeviceKey(array $result, array $normalizedDevice): ?string
|
|
382
|
+
{
|
|
383
|
+
$device = $result['device'] ?? null;
|
|
384
|
+
$value = null;
|
|
385
|
+
|
|
386
|
+
if (is_object($device)) {
|
|
387
|
+
$value = $device->uuid ?? $device->device_id ?? null;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
$value ??= $normalizedDevice['device_id']
|
|
391
|
+
?? $normalizedDevice['external_id']
|
|
392
|
+
?? null;
|
|
393
|
+
|
|
394
|
+
return $value === null || $value === '' ? null : (string) $value;
|
|
395
|
+
}
|
|
396
|
+
|
|
241
397
|
protected function safeSyncErrorMessage(\Throwable $e): string
|
|
242
398
|
{
|
|
243
399
|
$message = $e->getMessage();
|
|
@@ -278,6 +278,8 @@ class Driver extends Model
|
|
|
278
278
|
'photo_uuid',
|
|
279
279
|
'avatar_url',
|
|
280
280
|
'public_id',
|
|
281
|
+
'name',
|
|
282
|
+
'internal_id',
|
|
281
283
|
'location',
|
|
282
284
|
'online',
|
|
283
285
|
'updated_at',
|
|
@@ -288,6 +290,10 @@ class Driver extends Model
|
|
|
288
290
|
'year',
|
|
289
291
|
'make',
|
|
290
292
|
'model',
|
|
293
|
+
'class',
|
|
294
|
+
'color',
|
|
295
|
+
'call_sign',
|
|
296
|
+
'status',
|
|
291
297
|
'specs',
|
|
292
298
|
'vin_data',
|
|
293
299
|
'telematics',
|
|
@@ -849,7 +849,7 @@ class Order extends Model
|
|
|
849
849
|
$attributes['type'] = $this->type;
|
|
850
850
|
}
|
|
851
851
|
|
|
852
|
-
if (
|
|
852
|
+
if ($this->shouldResolvePayloadPlace($attributes, 'pickup')) {
|
|
853
853
|
$pickup = Place::createFromMixed($attributes['pickup']);
|
|
854
854
|
|
|
855
855
|
if ($pickup instanceof Place) {
|
|
@@ -857,7 +857,7 @@ class Order extends Model
|
|
|
857
857
|
}
|
|
858
858
|
}
|
|
859
859
|
|
|
860
|
-
if (
|
|
860
|
+
if ($this->shouldResolvePayloadPlace($attributes, 'dropoff')) {
|
|
861
861
|
$dropoff = Place::createFromMixed($attributes['dropoff']);
|
|
862
862
|
|
|
863
863
|
if ($dropoff instanceof Place) {
|
|
@@ -865,7 +865,7 @@ class Order extends Model
|
|
|
865
865
|
}
|
|
866
866
|
}
|
|
867
867
|
|
|
868
|
-
if (
|
|
868
|
+
if ($this->shouldResolvePayloadPlace($attributes, 'return')) {
|
|
869
869
|
$return = Place::createFromMixed($attributes['return']);
|
|
870
870
|
|
|
871
871
|
if ($return instanceof Place) {
|
|
@@ -897,19 +897,19 @@ class Order extends Model
|
|
|
897
897
|
$attributes['type'] = $this->type;
|
|
898
898
|
}
|
|
899
899
|
|
|
900
|
-
if (
|
|
900
|
+
if ($this->shouldResolvePayloadPlace($attributes, 'pickup')) {
|
|
901
901
|
$pickupId = Place::insertFromMixed($attributes['pickup']);
|
|
902
902
|
|
|
903
903
|
$attributes['pickup_uuid'] = $pickupId;
|
|
904
904
|
}
|
|
905
905
|
|
|
906
|
-
if (
|
|
906
|
+
if ($this->shouldResolvePayloadPlace($attributes, 'dropoff')) {
|
|
907
907
|
$dropoffId = Place::insertFromMixed($attributes['dropoff']);
|
|
908
908
|
|
|
909
909
|
$attributes['dropoff_uuid'] = $dropoffId;
|
|
910
910
|
}
|
|
911
911
|
|
|
912
|
-
if (
|
|
912
|
+
if ($this->shouldResolvePayloadPlace($attributes, 'return')) {
|
|
913
913
|
$returnId = Place::insertFromMixed($attributes['return']);
|
|
914
914
|
|
|
915
915
|
$attributes['return_uuid'] = $returnId;
|
|
@@ -947,6 +947,18 @@ class Order extends Model
|
|
|
947
947
|
return $payload;
|
|
948
948
|
}
|
|
949
949
|
|
|
950
|
+
protected function shouldResolvePayloadPlace(?array $attributes, string $role): bool
|
|
951
|
+
{
|
|
952
|
+
return !$this->hasExistingPayloadPlaceUuid($attributes, $role) && isset($attributes[$role]) && is_array($attributes[$role]);
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
protected function hasExistingPayloadPlaceUuid(?array $attributes, string $role): bool
|
|
956
|
+
{
|
|
957
|
+
$uuid = data_get($attributes, "{$role}_uuid");
|
|
958
|
+
|
|
959
|
+
return is_string($uuid) && Str::isUuid($uuid) && Place::where('uuid', $uuid)->exists();
|
|
960
|
+
}
|
|
961
|
+
|
|
950
962
|
/**
|
|
951
963
|
* Retrieves the payload associated with the order.
|
|
952
964
|
* If the payload is not already loaded, it loads the payload relationship.
|
|
@@ -1208,7 +1220,8 @@ class Order extends Model
|
|
|
1208
1220
|
'description' => 'Dispatch order',
|
|
1209
1221
|
'type' => 'dispatch',
|
|
1210
1222
|
'direction' => Transaction::DIRECTION_CREDIT,
|
|
1211
|
-
'status' =>
|
|
1223
|
+
'status' => Transaction::STATUS_SUCCESS,
|
|
1224
|
+
'settlement_status' => Transaction::SETTLEMENT_STATUS_UNPAID,
|
|
1212
1225
|
]);
|
|
1213
1226
|
|
|
1214
1227
|
$this->updateQuietly(['transaction_uuid' => $transaction->uuid]);
|
|
@@ -471,9 +471,17 @@ class Place extends Model
|
|
|
471
471
|
return null;
|
|
472
472
|
}
|
|
473
473
|
|
|
474
|
-
$postalCode
|
|
475
|
-
$street2
|
|
476
|
-
$
|
|
474
|
+
$postalCode = static::normalizePlaceValue(data_get($place, 'postal_code'));
|
|
475
|
+
$street2 = static::normalizePlaceValue(data_get($place, 'street2'));
|
|
476
|
+
$locationValue = data_get($place, 'location');
|
|
477
|
+
$location = new SpatialPoint(0, 0);
|
|
478
|
+
if (!empty($locationValue)) {
|
|
479
|
+
try {
|
|
480
|
+
$location = Utils::getPointFromMixed($locationValue);
|
|
481
|
+
} catch (\Throwable $e) {
|
|
482
|
+
$location = new SpatialPoint(0, 0);
|
|
483
|
+
}
|
|
484
|
+
}
|
|
477
485
|
|
|
478
486
|
$existingPlace = static::query()
|
|
479
487
|
->where('company_uuid', $companyUuid)
|
|
@@ -49,7 +49,8 @@ class PurchaseRateObserver
|
|
|
49
49
|
'description' => 'Dispatch order',
|
|
50
50
|
'type' => 'dispatch',
|
|
51
51
|
'direction' => Transaction::DIRECTION_CREDIT,
|
|
52
|
-
'status' =>
|
|
52
|
+
'status' => Transaction::STATUS_SUCCESS,
|
|
53
|
+
'settlement_status' => Transaction::SETTLEMENT_STATUS_UNPAID,
|
|
53
54
|
]);
|
|
54
55
|
|
|
55
56
|
if (isset($purchaseRate->serviceQuote)) {
|
|
@@ -145,6 +145,7 @@ class FleetOpsServiceProvider extends CoreServiceProvider
|
|
|
145
145
|
$schedule->command('fleetops:sync-telematics')->everyMinute()->withoutOverlapping()->storeOutputInDb();
|
|
146
146
|
});
|
|
147
147
|
$this->registerNotifications();
|
|
148
|
+
$this->registerAiCapabilities();
|
|
148
149
|
$this->registerExpansionsFrom(__DIR__ . '/../Expansions');
|
|
149
150
|
|
|
150
151
|
// Register built-in orchestration engines.
|
|
@@ -234,4 +235,29 @@ class FleetOpsServiceProvider extends CoreServiceProvider
|
|
|
234
235
|
'dynamic:facilitator',
|
|
235
236
|
]);
|
|
236
237
|
}
|
|
238
|
+
|
|
239
|
+
protected function registerAiCapabilities(): void
|
|
240
|
+
{
|
|
241
|
+
if (!Utils::classExists(\Fleetbase\Ai\Support\AiCapabilityRegistry::class)) {
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
if (Utils::classExists(\Fleetbase\Ai\Support\AiQueryRegistry::class)) {
|
|
246
|
+
$this->callAfterResolving(\Fleetbase\Ai\Support\AiQueryRegistry::class, function (\Fleetbase\Ai\Support\AiQueryRegistry $registry) {
|
|
247
|
+
\Fleetbase\FleetOps\Support\Ai\FleetOpsAiQueryResources::register($registry);
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
$this->callAfterResolving(\Fleetbase\Ai\Support\AiCapabilityRegistry::class, function (\Fleetbase\Ai\Support\AiCapabilityRegistry $registry) {
|
|
252
|
+
$registry->register(new \Fleetbase\FleetOps\Support\Ai\Capabilities\SearchResourcesCapability());
|
|
253
|
+
$registry->register(new \Fleetbase\FleetOps\Support\Ai\Capabilities\OperationalQueryCapability());
|
|
254
|
+
$registry->register(new \Fleetbase\FleetOps\Support\Ai\Capabilities\OrderInsightsCapability());
|
|
255
|
+
$registry->register(new \Fleetbase\FleetOps\Support\Ai\Capabilities\AssetStatusCapability());
|
|
256
|
+
$registry->register(new \Fleetbase\FleetOps\Support\Ai\Capabilities\DocsHelpCapability());
|
|
257
|
+
$registry->register(new \Fleetbase\FleetOps\Support\Ai\Capabilities\ConsoleNavigationCapability());
|
|
258
|
+
$registry->register(new \Fleetbase\FleetOps\Support\Ai\Capabilities\CreateOrderPreviewCapability());
|
|
259
|
+
$registry->register(new \Fleetbase\FleetOps\Support\Ai\Capabilities\OptimizeOrderRouteCapability());
|
|
260
|
+
$registry->register(new \Fleetbase\FleetOps\Support\Ai\Capabilities\ImportOrdersPreviewCapability());
|
|
261
|
+
});
|
|
262
|
+
}
|
|
237
263
|
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
<?php
|
|
2
|
+
|
|
3
|
+
namespace Fleetbase\FleetOps\Support\Ai\Capabilities;
|
|
4
|
+
|
|
5
|
+
use Fleetbase\Ai\Contracts\AIContextCapabilityInterface;
|
|
6
|
+
use Fleetbase\Ai\Models\AiTask;
|
|
7
|
+
use Fleetbase\Ai\Support\Capabilities\AbstractAICapability;
|
|
8
|
+
use Fleetbase\Support\Auth;
|
|
9
|
+
use Illuminate\Database\Eloquent\Builder;
|
|
10
|
+
use Illuminate\Support\Str;
|
|
11
|
+
|
|
12
|
+
abstract class AbstractFleetOpsAICapability extends AbstractAICapability implements AIContextCapabilityInterface
|
|
13
|
+
{
|
|
14
|
+
public function module(): string
|
|
15
|
+
{
|
|
16
|
+
return 'fleet-ops';
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
public function shouldResolve(AiTask $task): bool
|
|
20
|
+
{
|
|
21
|
+
return $this->matchesPrompt($this->prompt($task));
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
abstract protected function matchesPrompt(string $prompt): bool;
|
|
25
|
+
|
|
26
|
+
protected function prompt(AiTask $task): string
|
|
27
|
+
{
|
|
28
|
+
return Str::lower(trim((string) $task->prompt));
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
protected function containsAny(string $prompt, array $terms): bool
|
|
32
|
+
{
|
|
33
|
+
foreach ($terms as $term) {
|
|
34
|
+
if (str_contains($prompt, Str::lower($term))) {
|
|
35
|
+
return true;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
protected function can(string $permission): bool
|
|
43
|
+
{
|
|
44
|
+
$user = Auth::getUserFromSession();
|
|
45
|
+
|
|
46
|
+
if ($user?->isAdmin()) {
|
|
47
|
+
return true;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return Auth::can($permission);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
protected function canAll(array $permissions): bool
|
|
54
|
+
{
|
|
55
|
+
foreach ($permissions as $permission) {
|
|
56
|
+
if (!$this->can($permission)) {
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return true;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
protected function searchTerms(string $prompt): array
|
|
65
|
+
{
|
|
66
|
+
preg_match_all('/[A-Z]{2,}[-_][A-Z0-9-_]+|[A-Za-z0-9][A-Za-z0-9-_]{2,}/', (string) $prompt, $matches);
|
|
67
|
+
|
|
68
|
+
$terms = collect($matches[0] ?? [])
|
|
69
|
+
->reject(fn ($term) => in_array(Str::lower($term), ['find', 'show', 'open', 'order', 'orders', 'vehicle', 'vehicles', 'driver', 'drivers', 'work', 'status', 'about', 'fleet', 'ops'], true))
|
|
70
|
+
->unique()
|
|
71
|
+
->take(6)
|
|
72
|
+
->values()
|
|
73
|
+
->all();
|
|
74
|
+
|
|
75
|
+
return empty($terms) ? [trim((string) $prompt)] : $terms;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
protected function whereLikeAny(Builder $builder, array $columns, array $terms): void
|
|
79
|
+
{
|
|
80
|
+
$builder->where(function (Builder $query) use ($columns, $terms) {
|
|
81
|
+
foreach ($columns as $column) {
|
|
82
|
+
foreach ($terms as $term) {
|
|
83
|
+
$query->orWhere($column, 'like', '%' . $term . '%');
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
}
|