@fleetbase/fleetops-engine 0.6.56 → 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 (60) 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 +1 -1
  30. package/extension.json +1 -1
  31. package/package.json +6 -4
  32. package/scripts/normalize-rgba-alpha.js +90 -0
  33. package/server/src/Http/Resources/v1/Fleet.php +8 -2
  34. package/server/src/Models/Driver.php +6 -0
  35. package/server/src/Models/Order.php +18 -6
  36. package/server/src/Models/Place.php +11 -3
  37. package/server/src/Providers/FleetOpsServiceProvider.php +26 -0
  38. package/server/src/Support/Ai/Capabilities/AbstractFleetOpsAICapability.php +88 -0
  39. package/server/src/Support/Ai/Capabilities/AssetStatusCapability.php +108 -0
  40. package/server/src/Support/Ai/Capabilities/ConsoleNavigationCapability.php +55 -0
  41. package/server/src/Support/Ai/Capabilities/CreateOrderPreviewCapability.php +508 -0
  42. package/server/src/Support/Ai/Capabilities/DocsHelpCapability.php +49 -0
  43. package/server/src/Support/Ai/Capabilities/ImportOrdersPreviewCapability.php +66 -0
  44. package/server/src/Support/Ai/Capabilities/OperationalQueryCapability.php +319 -0
  45. package/server/src/Support/Ai/Capabilities/OptimizeOrderRouteCapability.php +240 -0
  46. package/server/src/Support/Ai/Capabilities/OrderInsightsCapability.php +95 -0
  47. package/server/src/Support/Ai/Capabilities/SearchResourcesCapability.php +195 -0
  48. package/server/src/Support/Ai/FleetOpsAiQueryResources.php +150 -0
  49. package/server/src/Support/Utils.php +52 -0
  50. package/server/tests/AiCreateOrderPreviewCapabilityTest.php +131 -0
  51. package/server/tests/AiOperationalQueryCapabilityTest.php +75 -0
  52. package/server/tests/FleetHierarchyResourceTest.php +12 -0
  53. package/server/tests/OrderPayloadPlaceResolutionTest.php +53 -0
  54. package/server/tests/PointResolutionTest.php +63 -0
  55. package/addon/components/table/cell/dropdown/action-item.hbs +0 -31
  56. package/addon/components/table/cell/dropdown/action-item.js +0 -62
  57. package/addon/components/table/cell/dropdown.hbs +0 -28
  58. package/addon/components/table/cell/dropdown.js +0 -91
  59. package/app/components/table/cell/dropdown/action-item.js +0 -1
  60. package/app/components/table/cell/dropdown.js +0 -1
@@ -0,0 +1,508 @@
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\Ai\Support\AiRelativeDateResolver;
8
+ use Fleetbase\FleetOps\Http\Controllers\Internal\v1\OrderController;
9
+ use Fleetbase\FleetOps\Models\Driver;
10
+ use Fleetbase\FleetOps\Models\OrderConfig;
11
+ use Fleetbase\FleetOps\Models\Place;
12
+ use Fleetbase\FleetOps\Models\Vehicle;
13
+ use Fleetbase\FleetOps\Support\PlaceSearch;
14
+ use Fleetbase\FleetOps\Support\Utils;
15
+ use Illuminate\Http\JsonResponse;
16
+ use Illuminate\Http\Request;
17
+ use Illuminate\Support\Arr;
18
+ use Illuminate\Support\Str;
19
+
20
+ class CreateOrderPreviewCapability extends AbstractFleetOpsAICapability implements AIActionCapabilityInterface
21
+ {
22
+ public function key(): string
23
+ {
24
+ return 'fleet-ops.create_order';
25
+ }
26
+
27
+ public function label(): string
28
+ {
29
+ return 'Create Fleet-Ops order preview';
30
+ }
31
+
32
+ public function description(): string
33
+ {
34
+ return 'Builds and applies confirmed Fleet-Ops order creation drafts.';
35
+ }
36
+
37
+ public function type(): string
38
+ {
39
+ return 'action';
40
+ }
41
+
42
+ public function mode(): string
43
+ {
44
+ return 'confirmation_required';
45
+ }
46
+
47
+ public function permissions(): array
48
+ {
49
+ return ['fleet-ops create order'];
50
+ }
51
+
52
+ public function previewOnly(): bool
53
+ {
54
+ return false;
55
+ }
56
+
57
+ public function executable(): bool
58
+ {
59
+ return true;
60
+ }
61
+
62
+ public function inputSchema(): array
63
+ {
64
+ return [
65
+ 'order_config_uuid' => 'Fleet-Ops order config UUID. Defaults to the company transport config when available.',
66
+ 'payload.pickup_uuid' => 'Existing Fleet-Ops place UUID for pickup.',
67
+ 'payload.dropoff_uuid' => 'Existing Fleet-Ops place UUID for dropoff.',
68
+ 'payload.waypoints' => 'Optional ordered waypoint place UUIDs.',
69
+ 'customer' => 'Optional contact/vendor UUID.',
70
+ 'scheduled_at' => 'Optional scheduled date/time for the order.',
71
+ 'dispatched' => 'Optional boolean. Defaults to false for AI-created drafts.',
72
+ ];
73
+ }
74
+
75
+ public function shouldPreview(AiTask $task): bool
76
+ {
77
+ return $this->matchesPrompt($this->prompt($task));
78
+ }
79
+
80
+ public function resolve(AiTask $task): array
81
+ {
82
+ return $this->preview($task);
83
+ }
84
+
85
+ public function preview(AiTask $task, array $input = []): array
86
+ {
87
+ $authorized = $this->canAll($this->permissions());
88
+ $draft = $this->buildDraft($task, $input);
89
+ $orderConfig = $this->resolveOrderConfig($draft);
90
+ $pickup = data_get($draft, 'payload.pickup') ?: data_get($draft, 'payload.pickup_query');
91
+ $dropoff = data_get($draft, 'payload.dropoff') ?: data_get($draft, 'payload.dropoff_query');
92
+ $driver = $this->resolveDriver($draft);
93
+ $vehicle = $this->resolveVehicle($draft);
94
+
95
+ if ($orderConfig) {
96
+ $draft['order_config_uuid'] = $orderConfig->uuid;
97
+ $draft['type'] = $orderConfig->key;
98
+ }
99
+
100
+ if ($driver) {
101
+ $draft['driver'] = $driver->uuid;
102
+ $draft['driver_assigned_uuid'] = $driver->uuid;
103
+ }
104
+
105
+ if ($vehicle) {
106
+ $draft['vehicle_assigned_uuid'] = $vehicle->uuid;
107
+ }
108
+
109
+ $missing = array_values(array_filter([
110
+ $authorized ? null : 'permission to create Fleet-Ops orders',
111
+ $orderConfig ? null : 'order configuration',
112
+ $pickup ? null : 'pickup address or place',
113
+ $dropoff ? null : 'dropoff address or place',
114
+ ]));
115
+
116
+ return [
117
+ 'action' => $this->key(),
118
+ 'authorized' => $authorized,
119
+ 'ready' => empty($missing),
120
+ 'missing_fields' => $missing,
121
+ 'message' => empty($missing) ? 'Fleetbase AI prepared a Fleet-Ops order draft. Review it before applying.' : 'Fleetbase AI can create the order after these required details are resolved.',
122
+ 'apply_label' => 'Create order',
123
+ 'cancel_label' => 'Cancel',
124
+ 'draft' => $draft,
125
+ 'route_preview' => $this->routePreview($draft),
126
+ 'options' => [
127
+ 'pod_methods' => $this->podMethods(),
128
+ ],
129
+ 'fields' => [
130
+ ['label' => 'Order config', 'value' => $orderConfig?->name ?? $orderConfig?->key],
131
+ ['label' => 'Schedule', 'value' => data_get($draft, 'scheduled_at')],
132
+ ['label' => 'Pickup', 'value' => data_get($pickup, 'address') ?? data_get($pickup, 'name')],
133
+ ['label' => 'Dropoff', 'value' => data_get($dropoff, 'address') ?? data_get($dropoff, 'name')],
134
+ ['label' => 'Driver', 'value' => $driver?->name],
135
+ ['label' => 'Vehicle', 'value' => $vehicle?->display_name ?? $vehicle?->name],
136
+ ['label' => 'Dispatch immediately', 'value' => data_get($draft, 'dispatched') ? 'Yes' : 'No'],
137
+ ],
138
+ 'risks' => [
139
+ 'The order will be created only after confirmation.',
140
+ 'AI-created drafts do not dispatch by default.',
141
+ ],
142
+ ];
143
+ }
144
+
145
+ public function apply(AiTask $task, array $preview = [], array $input = []): array
146
+ {
147
+ if (!$this->canAll($this->permissions())) {
148
+ throw new \RuntimeException('You do not have permission to create Fleet-Ops orders.');
149
+ }
150
+
151
+ if (!data_get($preview, 'ready')) {
152
+ throw new \RuntimeException('This order draft is missing required fields and cannot be applied yet.');
153
+ }
154
+
155
+ $draft = array_replace_recursive((array) data_get($preview, 'draft', []), (array) data_get($input, 'draft', $input));
156
+ $draft = $this->sanitizeDraftForApply($draft);
157
+ Arr::forget($draft, [
158
+ 'payload.pickup_query',
159
+ 'payload.dropoff_query',
160
+ 'driver_query',
161
+ 'vehicle_query',
162
+ ]);
163
+
164
+ /** @var OrderController $controller */
165
+ $controller = app(OrderController::class);
166
+ $response = $controller->createRecord(new Request(['order' => $draft]));
167
+
168
+ if ($response instanceof JsonResponse && $response->getStatusCode() >= 400) {
169
+ throw new \RuntimeException((string) $response->getContent());
170
+ }
171
+
172
+ $order = data_get($response, 'order');
173
+
174
+ return [
175
+ 'action' => $this->key(),
176
+ 'status' => 'completed',
177
+ 'message' => 'Fleet-Ops order was created.',
178
+ 'resource' => [
179
+ 'type' => 'order',
180
+ 'id' => data_get($order, 'public_id') ?? data_get($order, 'id'),
181
+ 'uuid' => data_get($order, 'uuid'),
182
+ 'route' => 'console.fleet-ops.operations.orders.index.details',
183
+ 'models' => array_filter([data_get($order, 'public_id') ?? data_get($order, 'uuid')]),
184
+ ],
185
+ ];
186
+ }
187
+
188
+ protected function sanitizeDraftForApply(array $draft): array
189
+ {
190
+ foreach (['pickup', 'dropoff', 'return'] as $role) {
191
+ $uuid = data_get($draft, "payload.{$role}_uuid");
192
+ if ($this->hasExistingPlaceUuid($uuid)) {
193
+ Arr::forget($draft, "payload.{$role}");
194
+ }
195
+ }
196
+
197
+ return $draft;
198
+ }
199
+
200
+ protected function hasExistingPlaceUuid($uuid): bool
201
+ {
202
+ return is_string($uuid) && Str::isUuid($uuid) && Place::where('uuid', $uuid)->exists();
203
+ }
204
+
205
+ protected function matchesPrompt(string $prompt): bool
206
+ {
207
+ return str_contains($prompt, 'order') && $this->containsAny($prompt, ['create', 'new order', 'make an order', 'book an order']);
208
+ }
209
+
210
+ protected function buildDraft(AiTask $task, array $input = []): array
211
+ {
212
+ $existing = (array) data_get($task->metadata, 'action_previews.0.draft', []);
213
+ $draft = array_replace_recursive($this->draftFromPrompt((string) $task->prompt), $existing, (array) data_get($input, 'draft', $input));
214
+
215
+ $draft['dispatched'] = filter_var(data_get($draft, 'dispatched', false), FILTER_VALIDATE_BOOLEAN);
216
+ $draft['payload'] = (array) data_get($draft, 'payload', []);
217
+ if (blank(data_get($draft, 'scheduled_at'))) {
218
+ unset($draft['scheduled_at']);
219
+ }
220
+
221
+ foreach (['pickup', 'dropoff'] as $role) {
222
+ $query = data_get($draft, "payload.{$role}_query");
223
+ $place = data_get($draft, "payload.{$role}");
224
+
225
+ if (!$place && $query) {
226
+ $place = $this->resolvePlace($query);
227
+ }
228
+
229
+ if ($place) {
230
+ $draft['payload'][$role] = $place;
231
+ if (!empty($place['uuid'])) {
232
+ $draft['payload']["{$role}_uuid"] = $place['uuid'];
233
+ }
234
+ } elseif ($query) {
235
+ $draft['payload'][$role] = $this->provisionalPlace($query);
236
+ unset($draft['payload']["{$role}_uuid"]);
237
+ }
238
+ }
239
+
240
+ return $draft;
241
+ }
242
+
243
+ protected function draftFromPrompt(string $prompt): array
244
+ {
245
+ $orderConfig = OrderConfig::defaultOrCreate();
246
+ $draft = [
247
+ 'order_config_uuid' => $orderConfig?->uuid,
248
+ 'type' => $orderConfig?->key,
249
+ 'payload' => [],
250
+ 'dispatched' => false,
251
+ ];
252
+
253
+ [$pickup, $dropoff] = $this->addressPairFromPrompt($prompt);
254
+ if ($pickup) {
255
+ $draft['payload']['pickup_query'] = $pickup;
256
+ if ($place = $this->resolvePlace($pickup)) {
257
+ $draft['payload']['pickup'] = $place;
258
+ if (!empty($place['uuid'])) {
259
+ $draft['payload']['pickup_uuid'] = $place['uuid'];
260
+ }
261
+ }
262
+ }
263
+
264
+ if ($dropoff) {
265
+ $draft['payload']['dropoff_query'] = $dropoff;
266
+ if ($place = $this->resolvePlace($dropoff)) {
267
+ $draft['payload']['dropoff'] = $place;
268
+ if (!empty($place['uuid'])) {
269
+ $draft['payload']['dropoff_uuid'] = $place['uuid'];
270
+ }
271
+ }
272
+ }
273
+
274
+ if (str_contains(Str::lower($prompt), 'dispatch')) {
275
+ $draft['dispatched'] = true;
276
+ }
277
+
278
+ if ($scheduledAt = $this->scheduledAtFromPrompt($prompt)) {
279
+ $draft['scheduled_at'] = $scheduledAt;
280
+ }
281
+
282
+ if (preg_match('/(?:note|notes)[:\s]+(.+)$/i', $prompt, $matches)) {
283
+ $draft['notes'] = trim($matches[1]);
284
+ }
285
+
286
+ if ($this->containsAny(Str::lower($prompt), ['proof of delivery', 'pod', 'signature', 'photo proof', 'scan proof'])) {
287
+ $draft['pod_required'] = true;
288
+ $draft['pod_method'] = str_contains(Str::lower($prompt), 'signature') ? 'signature' : (str_contains(Str::lower($prompt), 'photo') ? 'photo' : 'scan');
289
+ }
290
+
291
+ return $draft;
292
+ }
293
+
294
+ protected function scheduledAtFromPrompt(string $prompt): ?string
295
+ {
296
+ return $this->relativeDateResolver()->resolveDateTime($prompt)?->toIso8601String();
297
+ }
298
+
299
+ protected function relativeDateResolver(): AiRelativeDateResolver
300
+ {
301
+ return function_exists('app') ? app(AiRelativeDateResolver::class) : new AiRelativeDateResolver(null);
302
+ }
303
+
304
+ protected function addressPairFromPrompt(string $prompt): array
305
+ {
306
+ preg_match_all('/"([^"]+)"|\'([^\']+)\'/', $prompt, $quoted);
307
+ $quotedValues = collect($quoted[1] ?? [])
308
+ ->merge($quoted[2] ?? [])
309
+ ->filter()
310
+ ->values();
311
+
312
+ if ($quotedValues->count() >= 2) {
313
+ return [$quotedValues->get(0), $quotedValues->get(1)];
314
+ }
315
+
316
+ if (preg_match('/\bfrom\s+(.+?)(?=\s*,?\s+(?:to|towards)\s+)\s*,?\s+(?:to|towards)\s+(.+?)(?=\s+\b(?:with|for|on|using|require|requiring)\b|$)/i', $prompt, $matches)) {
317
+ return [$this->cleanAddress($matches[1]), $this->cleanAddress($matches[2])];
318
+ }
319
+
320
+ $pickup = $this->extractLabeledAddress($prompt, ['pickup', 'pick up'], ['dropoff', 'drop off', 'delivery']);
321
+ $dropoff = $this->extractLabeledAddress($prompt, ['dropoff', 'drop off', 'delivery'], ['return', 'with', 'for', 'on', 'using', 'require', 'requiring']);
322
+
323
+ if ($pickup || $dropoff) {
324
+ return [$pickup, $dropoff];
325
+ }
326
+
327
+ return [null, null];
328
+ }
329
+
330
+ protected function extractLabeledAddress(string $prompt, array $labels, array $nextLabels = []): ?string
331
+ {
332
+ $labelPattern = collect($labels)
333
+ ->map(fn ($label) => preg_quote($label, '/'))
334
+ ->implode('|');
335
+ $nextPattern = collect($nextLabels)
336
+ ->map(fn ($label) => preg_quote($label, '/'))
337
+ ->implode('|');
338
+ $lookahead = $nextPattern ? '(?=\s*,?\s*(?:and\s+)?(?:the\s+)?(?:' . $nextPattern . ')(?:\s+(?:at|to|from))?\s+|$)' : '(?=$)';
339
+
340
+ if (!preg_match('/\b(?:the\s+)?(?:' . $labelPattern . ')(?:\s+(?:at|to|from))?\s+(.+?)' . $lookahead . '/i', $prompt, $matches)) {
341
+ return null;
342
+ }
343
+
344
+ return $this->cleanAddress($matches[1]);
345
+ }
346
+
347
+ protected function cleanAddress(?string $address): ?string
348
+ {
349
+ $address = trim((string) $address);
350
+ $address = preg_replace('/^(?:,|\band\b|\s)+/i', '', $address);
351
+ $address = preg_replace('/(?:,|\band\b|\s)+$/i', '', $address);
352
+ $address = preg_replace('/\s+/', ' ', $address);
353
+
354
+ return $address !== '' ? $address : null;
355
+ }
356
+
357
+ protected function resolvePlace(?string $query): ?array
358
+ {
359
+ $query = $this->cleanAddress($query);
360
+ if (!$query) {
361
+ return null;
362
+ }
363
+
364
+ $places = PlaceSearch::search(Place::where('company_uuid', session('company')), $query, ['geo' => true, 'limit' => 8]);
365
+ $place = $places->first();
366
+
367
+ return $place instanceof Place ? $this->serializePlace($place, $query) : null;
368
+ }
369
+
370
+ protected function provisionalPlace(string $query): array
371
+ {
372
+ return [
373
+ 'id' => null,
374
+ 'uuid' => null,
375
+ 'public_id' => null,
376
+ 'name' => $query,
377
+ 'address' => $query,
378
+ 'latitude' => null,
379
+ 'longitude' => null,
380
+ 'query' => $query,
381
+ 'source' => 'unresolved',
382
+ ];
383
+ }
384
+
385
+ protected function serializePlace(Place $place, ?string $query = null): array
386
+ {
387
+ [$latitude, $longitude] = $this->placeCoordinates($place);
388
+
389
+ return [
390
+ 'id' => $place->public_id,
391
+ 'uuid' => $place->uuid,
392
+ 'public_id' => $place->public_id,
393
+ 'name' => $place->name,
394
+ 'street1' => $place->street1,
395
+ 'street2' => $place->street2,
396
+ 'city' => $place->city,
397
+ 'province' => $place->province,
398
+ 'postal_code' => $place->postal_code,
399
+ 'country' => $place->country,
400
+ 'address' => $place->address ?: $place->toAddressString(['name']),
401
+ 'latitude' => $latitude,
402
+ 'longitude' => $longitude,
403
+ 'query' => $query,
404
+ 'source' => $place->exists ? 'saved' : 'geocoded',
405
+ ];
406
+ }
407
+
408
+ protected function placeCoordinates(Place $place): array
409
+ {
410
+ $latitude = data_get($place, 'latitude');
411
+ $longitude = data_get($place, 'longitude');
412
+
413
+ if ((!is_numeric($latitude) || !is_numeric($longitude)) && $place->location) {
414
+ $point = Utils::getPointFromCoordinates($place->location);
415
+
416
+ if ($point) {
417
+ $latitude = method_exists($point, 'getLat') ? $point->getLat() : data_get($point, 'coordinates.1');
418
+ $longitude = method_exists($point, 'getLng') ? $point->getLng() : data_get($point, 'coordinates.0');
419
+ }
420
+ }
421
+
422
+ return [
423
+ is_numeric($latitude) ? (float) $latitude : null,
424
+ is_numeric($longitude) ? (float) $longitude : null,
425
+ ];
426
+ }
427
+
428
+ protected function routePreview(array $draft): array
429
+ {
430
+ $rawStops = [
431
+ ['role' => 'pickup', 'place' => data_get($draft, 'payload.pickup')],
432
+ ];
433
+ foreach ((array) data_get($draft, 'payload.waypoints', []) as $index => $waypoint) {
434
+ $rawStops[] = ['role' => 'waypoint', 'label' => 'Stop ' . ($index + 1), 'place' => data_get($waypoint, 'place') ?: $waypoint];
435
+ }
436
+ $rawStops[] = ['role' => 'dropoff', 'place' => data_get($draft, 'payload.dropoff')];
437
+
438
+ $stops = collect($rawStops)
439
+ ->filter(fn ($stop) => is_array($stop['place'] ?? null))
440
+ ->map(fn ($stop) => [
441
+ 'role' => $stop['role'],
442
+ 'label' => $stop['label'] ?? Str::title($stop['role']),
443
+ 'address' => data_get($stop, 'place.address') ?? data_get($stop, 'place.name'),
444
+ 'latitude' => data_get($stop, 'place.latitude'),
445
+ 'longitude' => data_get($stop, 'place.longitude'),
446
+ 'coordinates' => is_numeric(data_get($stop, 'place.latitude')) && is_numeric(data_get($stop, 'place.longitude')) ? [(float) data_get($stop, 'place.latitude'), (float) data_get($stop, 'place.longitude')] : null,
447
+ ])
448
+ ->values()
449
+ ->all();
450
+
451
+ return [
452
+ 'stops' => $stops,
453
+ 'coordinates' => collect($stops)
454
+ ->filter(fn ($stop) => is_numeric($stop['latitude']) && is_numeric($stop['longitude']))
455
+ ->map(fn ($stop) => [$stop['latitude'], $stop['longitude']])
456
+ ->values()
457
+ ->all(),
458
+ ];
459
+ }
460
+
461
+ protected function resolveOrderConfig(array $draft): ?OrderConfig
462
+ {
463
+ return OrderConfig::resolveFromIdentifier([data_get($draft, 'order_config_uuid'), data_get($draft, 'type')]) ?? OrderConfig::defaultOrCreate();
464
+ }
465
+
466
+ protected function resolveDriver(array $draft): ?Driver
467
+ {
468
+ $identifier = data_get($draft, 'driver') ?? data_get($draft, 'driver_assigned_uuid') ?? data_get($draft, 'driver_query');
469
+ if (!$identifier) {
470
+ return null;
471
+ }
472
+
473
+ return Driver::where('company_uuid', session('company'))
474
+ ->where(function ($query) use ($identifier) {
475
+ $query->where('uuid', $identifier)->orWhere('public_id', $identifier)->orWhereHas('user', fn ($user) => $user->where('name', 'like', '%' . $identifier . '%'));
476
+ })
477
+ ->first();
478
+ }
479
+
480
+ protected function resolveVehicle(array $draft): ?Vehicle
481
+ {
482
+ $identifier = data_get($draft, 'vehicle') ?? data_get($draft, 'vehicle_assigned_uuid') ?? data_get($draft, 'vehicle_query');
483
+ if (!$identifier) {
484
+ return null;
485
+ }
486
+
487
+ return Vehicle::where('company_uuid', session('company'))
488
+ ->where(function ($query) use ($identifier) {
489
+ $query->where('uuid', $identifier)->orWhere('public_id', $identifier)->orWhere('plate_number', 'like', '%' . $identifier . '%')->orWhere('name', 'like', '%' . $identifier . '%');
490
+ })
491
+ ->first();
492
+ }
493
+
494
+ protected function podMethods(): array
495
+ {
496
+ $methods = config('fleetops.pod_methods', ['scan', 'signature', 'photo']);
497
+ if (is_string($methods)) {
498
+ $methods = explode(',', $methods);
499
+ }
500
+
501
+ return collect((array) $methods)
502
+ ->map(fn ($method) => trim((string) $method))
503
+ ->filter()
504
+ ->unique()
505
+ ->values()
506
+ ->all();
507
+ }
508
+ }
@@ -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
+ }