@fleetbase/fleetops-engine 0.6.56 → 0.6.58

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 (82) hide show
  1. package/addon/components/ai/create-order-preview.hbs +297 -0
  2. package/addon/components/ai/create-order-preview.js +362 -0
  3. package/addon/components/ai/route-preview-map.hbs +79 -0
  4. package/addon/components/ai/route-preview-map.js +527 -0
  5. package/addon/components/fleet/details.hbs +21 -8
  6. package/addon/components/fleet/details.js +55 -1
  7. package/addon/components/fleet/hierarchy-tree.hbs +137 -0
  8. package/addon/components/fleet/hierarchy-tree.js +528 -0
  9. package/addon/components/map/drawer/device-event-listing.js +1 -0
  10. package/addon/components/map/drawer/driver-listing.js +1 -0
  11. package/addon/components/map/drawer/place-listing.js +1 -0
  12. package/addon/components/map/drawer/vehicle-listing.js +1 -0
  13. package/addon/components/order/details.hbs +34 -1
  14. package/addon/components/order/form/details.hbs +4 -0
  15. package/addon/components/order/form.hbs +9 -1
  16. package/addon/components/vehicle/card.hbs +14 -2
  17. package/addon/components/vehicle/form.hbs +13 -1
  18. package/addon/controllers/settings/map.js +4 -0
  19. package/addon/extension.js +14 -0
  20. package/addon/routes/management/fleets/index/details.js +1 -1
  21. package/addon/services/map-adapter/google.js +56 -16
  22. package/addon/services/map-manager.js +12 -0
  23. package/addon/styles/fleetops-engine.css +487 -1
  24. package/addon/utils/map-drawer-dropdown-position.js +11 -13
  25. package/app/components/ai/create-order-preview.js +1 -0
  26. package/app/components/ai/route-preview-map.js +1 -0
  27. package/app/components/fleet/hierarchy-tree.js +1 -0
  28. package/app/services/route-engine.js +1 -0
  29. package/composer.json +9 -2
  30. package/extension.json +1 -1
  31. package/package.json +6 -4
  32. package/phpunit.xml.dist +4 -4
  33. package/pnpm-workspace.yaml +2 -1
  34. package/scripts/coverage-runner.php +51 -0
  35. package/scripts/coverage-summary.php +110 -0
  36. package/scripts/normalize-rgba-alpha.js +90 -0
  37. package/scripts/pest-bootstrap.php +177 -0
  38. package/scripts/pest-file-runner.php +82 -0
  39. package/scripts/pest-runner.php +64 -0
  40. package/server/src/Http/Resources/v1/Fleet.php +8 -2
  41. package/server/src/Models/Driver.php +6 -0
  42. package/server/src/Models/Order.php +18 -6
  43. package/server/src/Models/Place.php +11 -3
  44. package/server/src/Orchestration/Support/OrchestrationPayloadBuilder.php +3 -1
  45. package/server/src/Providers/FleetOpsServiceProvider.php +26 -0
  46. package/server/src/Support/Ai/Capabilities/AbstractFleetOpsAICapability.php +88 -0
  47. package/server/src/Support/Ai/Capabilities/AssetStatusCapability.php +108 -0
  48. package/server/src/Support/Ai/Capabilities/ConsoleNavigationCapability.php +55 -0
  49. package/server/src/Support/Ai/Capabilities/CreateOrderPreviewCapability.php +508 -0
  50. package/server/src/Support/Ai/Capabilities/DocsHelpCapability.php +49 -0
  51. package/server/src/Support/Ai/Capabilities/ImportOrdersPreviewCapability.php +66 -0
  52. package/server/src/Support/Ai/Capabilities/OperationalQueryCapability.php +319 -0
  53. package/server/src/Support/Ai/Capabilities/OptimizeOrderRouteCapability.php +240 -0
  54. package/server/src/Support/Ai/Capabilities/OrderInsightsCapability.php +95 -0
  55. package/server/src/Support/Ai/Capabilities/SearchResourcesCapability.php +195 -0
  56. package/server/src/Support/Ai/FleetOpsAiQueryResources.php +150 -0
  57. package/server/src/Support/Utils.php +52 -0
  58. package/server/src/Tracking/TrackingOptions.php +6 -1
  59. package/server/src/routes.php +0 -11
  60. package/server/tests/AiCreateOrderPreviewCapabilityTest.php +131 -0
  61. package/server/tests/AiOperationalQueryCapabilityTest.php +75 -0
  62. package/server/tests/AnalyticsRoutesTest.php +5 -3
  63. package/server/tests/CustomerEndpointTest.php +1 -0
  64. package/server/tests/DeviceFilterTest.php +1 -2
  65. package/server/tests/FleetHierarchyResourceTest.php +12 -0
  66. package/server/tests/FuelProviderRegistryTest.php +7 -9
  67. package/server/tests/LiveControllerViewportTest.php +8 -16
  68. package/server/tests/LiveOrderQueryTest.php +18 -24
  69. package/server/tests/OrderPayloadPlaceResolutionTest.php +53 -0
  70. package/server/tests/PointResolutionTest.php +63 -0
  71. package/server/tests/ServiceAreaZoneApiShapeTest.php +8 -9
  72. package/server/tests/ServiceRateAlgorithmWeightVariablesTest.php +1 -1
  73. package/server/tests/TelematicsHardeningTest.php +31 -26
  74. package/server/tests/TrackingIntelligenceTest.php +65 -25
  75. package/server/tests/VehicleDriverAssignmentTest.php +8 -9
  76. package/addon/components/table/cell/dropdown/action-item.hbs +0 -31
  77. package/addon/components/table/cell/dropdown/action-item.js +0 -62
  78. package/addon/components/table/cell/dropdown.hbs +0 -28
  79. package/addon/components/table/cell/dropdown.js +0 -91
  80. package/app/components/table/cell/dropdown/action-item.js +0 -1
  81. package/app/components/table/cell/dropdown.js +0 -1
  82. package/server/src/Http/Controllers/Api/v1/OrganizationController.php +0 -45
@@ -471,9 +471,17 @@ class Place extends Model
471
471
  return null;
472
472
  }
473
473
 
474
- $postalCode = static::normalizePlaceValue(data_get($place, 'postal_code'));
475
- $street2 = static::normalizePlaceValue(data_get($place, 'street2'));
476
- $location = Utils::getPointFromMixed(data_get($place, 'location'));
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)
@@ -337,7 +337,9 @@ class OrchestrationPayloadBuilder
337
337
  protected static function resolveServiceTime(Order $order): int
338
338
  {
339
339
  $payload = $order->payload;
340
- $waypointMarker = $payload?->waypointMarkers?->last();
340
+ $waypointMarker = method_exists($payload, 'relationLoaded') && $payload->relationLoaded('waypointMarkers')
341
+ ? $payload->waypointMarkers?->last()
342
+ : null;
341
343
 
342
344
  return (int) (
343
345
  $waypointMarker?->service_time
@@ -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
+ }
@@ -0,0 +1,108 @@
1
+ <?php
2
+
3
+ namespace Fleetbase\FleetOps\Support\Ai\Capabilities;
4
+
5
+ use Fleetbase\Ai\Models\AiTask;
6
+ use Fleetbase\FleetOps\Models\Device;
7
+ use Fleetbase\FleetOps\Models\Driver;
8
+ use Fleetbase\FleetOps\Models\Sensor;
9
+ use Fleetbase\FleetOps\Models\Telematic;
10
+ use Fleetbase\FleetOps\Models\Vehicle;
11
+
12
+ class AssetStatusCapability extends AbstractFleetOpsAICapability
13
+ {
14
+ public function key(): string
15
+ {
16
+ return 'fleet-ops.asset_status';
17
+ }
18
+
19
+ public function label(): string
20
+ {
21
+ return 'Fleet-Ops asset status';
22
+ }
23
+
24
+ public function description(): string
25
+ {
26
+ return 'Summarizes Fleet-Ops vehicle, device, sensor, and telematic status counts.';
27
+ }
28
+
29
+ public function permissions(): array
30
+ {
31
+ return ['fleet-ops see vehicle', 'fleet-ops see device', 'fleet-ops see sensor', 'fleet-ops see telematic'];
32
+ }
33
+
34
+ public function resolve(AiTask $task): array
35
+ {
36
+ return [
37
+ 'drivers' => $this->driverStatus(),
38
+ 'vehicles' => $this->statusCounts(Vehicle::class, 'fleet-ops see vehicle'),
39
+ 'devices' => $this->deviceStatus(),
40
+ 'sensors' => $this->statusCounts(Sensor::class, 'fleet-ops see sensor'),
41
+ 'telematics' => $this->statusCounts(Telematic::class, 'fleet-ops see telematic'),
42
+ ];
43
+ }
44
+
45
+ protected function matchesPrompt(string $prompt): bool
46
+ {
47
+ return $this->containsAny($prompt, ['offline', 'online', 'asset status', 'driver status', 'vehicle status', 'device status', 'sensor status', 'telematic status', 'drivers down', 'devices down', 'vehicles down']);
48
+ }
49
+
50
+ protected function statusCounts(string $modelClass, string $permission): array
51
+ {
52
+ if (!$this->can($permission)) {
53
+ return ['authorized' => false];
54
+ }
55
+
56
+ return [
57
+ 'authorized' => true,
58
+ 'total' => $modelClass::where('company_uuid', session('company'))->count(),
59
+ 'counts_by_status' => $modelClass::where('company_uuid', session('company'))
60
+ ->selectRaw('status, count(*) as aggregate')
61
+ ->groupBy('status')
62
+ ->pluck('aggregate', 'status')
63
+ ->all(),
64
+ ];
65
+ }
66
+
67
+ protected function deviceStatus(): array
68
+ {
69
+ if (!$this->can('fleet-ops see device')) {
70
+ return ['authorized' => false];
71
+ }
72
+
73
+ return [
74
+ 'authorized' => true,
75
+ 'total' => Device::where('company_uuid', session('company'))->count(),
76
+ 'online' => Device::where('company_uuid', session('company'))->where('online', true)->count(),
77
+ 'offline' => Device::where('company_uuid', session('company'))->where(function ($query) {
78
+ $query->where('online', false)->orWhereNull('online');
79
+ })->count(),
80
+ 'counts_by_status' => Device::where('company_uuid', session('company'))
81
+ ->selectRaw('status, count(*) as aggregate')
82
+ ->groupBy('status')
83
+ ->pluck('aggregate', 'status')
84
+ ->all(),
85
+ ];
86
+ }
87
+
88
+ protected function driverStatus(): array
89
+ {
90
+ if (!$this->can('fleet-ops see driver')) {
91
+ return ['authorized' => false];
92
+ }
93
+
94
+ return [
95
+ 'authorized' => true,
96
+ 'total' => Driver::where('company_uuid', session('company'))->count(),
97
+ 'online' => Driver::where('company_uuid', session('company'))->where('online', true)->count(),
98
+ 'offline' => Driver::where('company_uuid', session('company'))->where(function ($query) {
99
+ $query->where('online', false)->orWhereNull('online');
100
+ })->count(),
101
+ 'counts_by_status' => Driver::where('company_uuid', session('company'))
102
+ ->selectRaw('status, count(*) as aggregate')
103
+ ->groupBy('status')
104
+ ->pluck('aggregate', 'status')
105
+ ->all(),
106
+ ];
107
+ }
108
+ }
@@ -0,0 +1,55 @@
1
+ <?php
2
+
3
+ namespace Fleetbase\FleetOps\Support\Ai\Capabilities;
4
+
5
+ use Fleetbase\Ai\Models\AiTask;
6
+
7
+ class ConsoleNavigationCapability extends SearchResourcesCapability
8
+ {
9
+ public function key(): string
10
+ {
11
+ return 'fleet-ops.console_navigation';
12
+ }
13
+
14
+ public function label(): string
15
+ {
16
+ return 'Fleet-Ops console navigation preview';
17
+ }
18
+
19
+ public function description(): string
20
+ {
21
+ return 'Returns preview-only console route suggestions for Fleet-Ops resources.';
22
+ }
23
+
24
+ public function type(): string
25
+ {
26
+ return 'ui';
27
+ }
28
+
29
+ public function mode(): string
30
+ {
31
+ return 'navigation_preview';
32
+ }
33
+
34
+ public function resolve(AiTask $task): array
35
+ {
36
+ $search = parent::resolve($task);
37
+ $items = collect(data_get($search, 'results', []))
38
+ ->flatten(1)
39
+ ->filter(fn ($result) => is_array($result) && isset($result['route']))
40
+ ->take(5)
41
+ ->values()
42
+ ->all();
43
+
44
+ return [
45
+ 'preview_only' => true,
46
+ 'message' => 'Fleetbase AI can suggest where to navigate, but this pilot does not directly open panels yet.',
47
+ 'suggestions' => $items,
48
+ ];
49
+ }
50
+
51
+ protected function matchesPrompt(string $prompt): bool
52
+ {
53
+ return $this->containsAny($prompt, ['open', 'show me', 'go to', 'navigate', 'take me to']) && parent::matchesPrompt($prompt);
54
+ }
55
+ }