@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.
- 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/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/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/vehicle/card.hbs +14 -2
- package/addon/components/vehicle/form.hbs +13 -1
- package/addon/controllers/settings/map.js +4 -0
- package/addon/extension.js +14 -0
- package/addon/routes/management/fleets/index/details.js +1 -1
- package/addon/services/map-adapter/google.js +56 -16
- package/addon/services/map-manager.js +12 -0
- package/addon/styles/fleetops-engine.css +487 -1
- package/addon/utils/map-drawer-dropdown-position.js +11 -13
- package/app/components/ai/create-order-preview.js +1 -0
- package/app/components/ai/route-preview-map.js +1 -0
- package/app/components/fleet/hierarchy-tree.js +1 -0
- package/app/services/route-engine.js +1 -0
- package/composer.json +9 -2
- package/extension.json +1 -1
- package/package.json +6 -4
- package/phpunit.xml.dist +4 -4
- package/pnpm-workspace.yaml +2 -1
- package/scripts/coverage-runner.php +51 -0
- package/scripts/coverage-summary.php +110 -0
- package/scripts/normalize-rgba-alpha.js +90 -0
- package/scripts/pest-bootstrap.php +177 -0
- package/scripts/pest-file-runner.php +82 -0
- package/scripts/pest-runner.php +64 -0
- package/server/src/Http/Resources/v1/Fleet.php +8 -2
- package/server/src/Models/Driver.php +6 -0
- package/server/src/Models/Order.php +18 -6
- package/server/src/Models/Place.php +11 -3
- package/server/src/Orchestration/Support/OrchestrationPayloadBuilder.php +3 -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/Utils.php +52 -0
- package/server/src/Tracking/TrackingOptions.php +6 -1
- package/server/src/routes.php +0 -11
- package/server/tests/AiCreateOrderPreviewCapabilityTest.php +131 -0
- package/server/tests/AiOperationalQueryCapabilityTest.php +75 -0
- package/server/tests/AnalyticsRoutesTest.php +5 -3
- package/server/tests/CustomerEndpointTest.php +1 -0
- package/server/tests/DeviceFilterTest.php +1 -2
- package/server/tests/FleetHierarchyResourceTest.php +12 -0
- package/server/tests/FuelProviderRegistryTest.php +7 -9
- package/server/tests/LiveControllerViewportTest.php +8 -16
- package/server/tests/LiveOrderQueryTest.php +18 -24
- package/server/tests/OrderPayloadPlaceResolutionTest.php +53 -0
- package/server/tests/PointResolutionTest.php +63 -0
- package/server/tests/ServiceAreaZoneApiShapeTest.php +8 -9
- package/server/tests/ServiceRateAlgorithmWeightVariablesTest.php +1 -1
- package/server/tests/TelematicsHardeningTest.php +31 -26
- package/server/tests/TrackingIntelligenceTest.php +65 -25
- package/server/tests/VehicleDriverAssignmentTest.php +8 -9
- 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
- package/server/src/Http/Controllers/Api/v1/OrganizationController.php +0 -45
|
@@ -0,0 +1,195 @@
|
|
|
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\Maintenance;
|
|
9
|
+
use Fleetbase\FleetOps\Models\Order;
|
|
10
|
+
use Fleetbase\FleetOps\Models\Sensor;
|
|
11
|
+
use Fleetbase\FleetOps\Models\Telematic;
|
|
12
|
+
use Fleetbase\FleetOps\Models\Vehicle;
|
|
13
|
+
use Fleetbase\FleetOps\Models\WorkOrder;
|
|
14
|
+
|
|
15
|
+
class SearchResourcesCapability extends AbstractFleetOpsAICapability
|
|
16
|
+
{
|
|
17
|
+
public function key(): string
|
|
18
|
+
{
|
|
19
|
+
return 'fleet-ops.search_resources';
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
public function label(): string
|
|
23
|
+
{
|
|
24
|
+
return 'Search Fleet-Ops resources';
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
public function description(): string
|
|
28
|
+
{
|
|
29
|
+
return 'Finds relevant Fleet-Ops orders, vehicles, drivers, work orders, maintenances, devices, sensors, and telematics.';
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
public function permissions(): array
|
|
33
|
+
{
|
|
34
|
+
return [
|
|
35
|
+
'fleet-ops see order',
|
|
36
|
+
'fleet-ops see vehicle',
|
|
37
|
+
'fleet-ops see driver',
|
|
38
|
+
'fleet-ops see work-order',
|
|
39
|
+
'fleet-ops see maintenance',
|
|
40
|
+
'fleet-ops see device',
|
|
41
|
+
'fleet-ops see sensor',
|
|
42
|
+
'fleet-ops see telematic',
|
|
43
|
+
];
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
public function resolve(AiTask $task): array
|
|
47
|
+
{
|
|
48
|
+
$prompt = (string) $task->prompt;
|
|
49
|
+
$terms = $this->searchTerms($prompt);
|
|
50
|
+
|
|
51
|
+
return [
|
|
52
|
+
'query_terms' => $terms,
|
|
53
|
+
'results' => array_filter([
|
|
54
|
+
'orders' => $this->orders($terms),
|
|
55
|
+
'vehicles' => $this->vehicles($terms),
|
|
56
|
+
'drivers' => $this->drivers($terms),
|
|
57
|
+
'work_orders' => $this->workOrders($terms),
|
|
58
|
+
'maintenances' => $this->maintenances($terms),
|
|
59
|
+
'devices' => $this->devices($terms),
|
|
60
|
+
'sensors' => $this->sensors($terms),
|
|
61
|
+
'telematics' => $this->telematics($terms),
|
|
62
|
+
]),
|
|
63
|
+
];
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
protected function matchesPrompt(string $prompt): bool
|
|
67
|
+
{
|
|
68
|
+
return $this->containsAny($prompt, ['find', 'show', 'open', 'look up', 'tell me about', 'status of', 'order', 'vehicle', 'driver', 'work order', 'maintenance', 'device', 'sensor', 'telematic']);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
protected function orders(array $terms): array
|
|
72
|
+
{
|
|
73
|
+
if (!$this->can('fleet-ops see order')) {
|
|
74
|
+
return [];
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return Order::with(['transaction', 'trackingNumber'])
|
|
78
|
+
->where('company_uuid', session('company'))
|
|
79
|
+
->where(function ($query) use ($terms) {
|
|
80
|
+
$this->whereLikeAny($query, ['public_id', 'internal_id', 'uuid', 'status', 'type'], $terms);
|
|
81
|
+
$query->orWhereHas('trackingNumber', fn ($tracking) => $this->whereLikeAny($tracking, ['tracking_number', 'barcode'], $terms));
|
|
82
|
+
})
|
|
83
|
+
->limit(5)
|
|
84
|
+
->get()
|
|
85
|
+
->map(fn (Order $order) => [
|
|
86
|
+
'id' => $order->public_id,
|
|
87
|
+
'uuid' => $order->uuid,
|
|
88
|
+
'tracking' => $order->tracking,
|
|
89
|
+
'status' => $order->status,
|
|
90
|
+
'type' => $order->type,
|
|
91
|
+
'transaction_amount' => $order->transaction_amount,
|
|
92
|
+
'transaction_currency' => $order->transaction_currency,
|
|
93
|
+
'route' => 'console.fleet-ops.operations.orders.index.details',
|
|
94
|
+
'models' => [$order->public_id ?: $order->uuid],
|
|
95
|
+
])
|
|
96
|
+
->values()
|
|
97
|
+
->all();
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
protected function vehicles(array $terms): array
|
|
101
|
+
{
|
|
102
|
+
if (!$this->can('fleet-ops see vehicle')) {
|
|
103
|
+
return [];
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return Vehicle::where('company_uuid', session('company'))
|
|
107
|
+
->where(fn ($query) => $this->whereLikeAny($query, ['name', 'make', 'model', 'plate_number', 'vin', 'public_id', 'internal_id'], $terms))
|
|
108
|
+
->limit(5)
|
|
109
|
+
->get()
|
|
110
|
+
->map(fn (Vehicle $vehicle) => [
|
|
111
|
+
'id' => $vehicle->public_id,
|
|
112
|
+
'uuid' => $vehicle->uuid,
|
|
113
|
+
'name' => $vehicle->display_name ?: $vehicle->name,
|
|
114
|
+
'plate' => $vehicle->plate_number,
|
|
115
|
+
'status' => $vehicle->status,
|
|
116
|
+
'route' => 'console.fleet-ops.management.vehicles.index.details',
|
|
117
|
+
'models' => [$vehicle->public_id ?: $vehicle->uuid],
|
|
118
|
+
])
|
|
119
|
+
->values()
|
|
120
|
+
->all();
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
protected function drivers(array $terms): array
|
|
124
|
+
{
|
|
125
|
+
if (!$this->can('fleet-ops see driver')) {
|
|
126
|
+
return [];
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return Driver::with('user')
|
|
130
|
+
->where('company_uuid', session('company'))
|
|
131
|
+
->where(function ($query) use ($terms) {
|
|
132
|
+
$this->whereLikeAny($query, ['public_id', 'uuid', 'drivers_license_number', 'status'], $terms);
|
|
133
|
+
$query->orWhereHas('user', fn ($user) => $this->whereLikeAny($user, ['name', 'email', 'phone'], $terms));
|
|
134
|
+
})
|
|
135
|
+
->limit(5)
|
|
136
|
+
->get()
|
|
137
|
+
->map(fn (Driver $driver) => [
|
|
138
|
+
'id' => $driver->public_id,
|
|
139
|
+
'uuid' => $driver->uuid,
|
|
140
|
+
'name' => data_get($driver, 'user.name'),
|
|
141
|
+
'status' => $driver->status,
|
|
142
|
+
'route' => 'console.fleet-ops.management.drivers.index.details',
|
|
143
|
+
'models' => [$driver->public_id ?: $driver->uuid],
|
|
144
|
+
])
|
|
145
|
+
->values()
|
|
146
|
+
->all();
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
protected function workOrders(array $terms): array
|
|
150
|
+
{
|
|
151
|
+
return $this->generic(WorkOrder::class, 'fleet-ops see work-order', ['public_id', 'uuid', 'code', 'subject', 'status', 'priority'], 'console.fleet-ops.maintenance.work-orders.index.details', $terms);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
protected function maintenances(array $terms): array
|
|
155
|
+
{
|
|
156
|
+
return $this->generic(Maintenance::class, 'fleet-ops see maintenance', ['public_id', 'uuid', 'status', 'type', 'summary', 'notes'], 'console.fleet-ops.maintenance.maintenances.index.details', $terms);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
protected function devices(array $terms): array
|
|
160
|
+
{
|
|
161
|
+
return $this->generic(Device::class, 'fleet-ops see device', ['public_id', 'uuid', 'name', 'device_id', 'imei', 'serial_number', 'status'], 'console.fleet-ops.connectivity.devices.index.details', $terms);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
protected function sensors(array $terms): array
|
|
165
|
+
{
|
|
166
|
+
return $this->generic(Sensor::class, 'fleet-ops see sensor', ['public_id', 'uuid', 'name', 'internal_id', 'serial_number', 'imei', 'type', 'sensor_type', 'status'], 'console.fleet-ops.connectivity.sensors.index.details', $terms);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
protected function telematics(array $terms): array
|
|
170
|
+
{
|
|
171
|
+
return $this->generic(Telematic::class, 'fleet-ops see telematic', ['public_id', 'uuid', 'name', 'provider', 'status'], 'console.fleet-ops.connectivity.telematics.details', $terms);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
protected function generic(string $modelClass, string $permission, array $columns, string $route, array $terms): array
|
|
175
|
+
{
|
|
176
|
+
if (!$this->can($permission)) {
|
|
177
|
+
return [];
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
return $modelClass::where('company_uuid', session('company'))
|
|
181
|
+
->where(fn ($query) => $this->whereLikeAny($query, $columns, $terms))
|
|
182
|
+
->limit(5)
|
|
183
|
+
->get()
|
|
184
|
+
->map(fn ($record) => [
|
|
185
|
+
'id' => $record->public_id,
|
|
186
|
+
'uuid' => $record->uuid,
|
|
187
|
+
'name' => $record->name ?? $record->subject ?? $record->summary ?? $record->description ?? null,
|
|
188
|
+
'status' => $record->status ?? null,
|
|
189
|
+
'route' => $route,
|
|
190
|
+
'models' => [$record->public_id ?: $record->uuid],
|
|
191
|
+
])
|
|
192
|
+
->values()
|
|
193
|
+
->all();
|
|
194
|
+
}
|
|
195
|
+
}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
<?php
|
|
2
|
+
|
|
3
|
+
namespace Fleetbase\FleetOps\Support\Ai;
|
|
4
|
+
|
|
5
|
+
use Fleetbase\Ai\Support\AiQueryableResource;
|
|
6
|
+
use Fleetbase\Ai\Support\AiQueryRegistry;
|
|
7
|
+
use Fleetbase\FleetOps\Models\Device;
|
|
8
|
+
use Fleetbase\FleetOps\Models\Driver;
|
|
9
|
+
use Fleetbase\FleetOps\Models\Fleet;
|
|
10
|
+
use Fleetbase\FleetOps\Models\Order;
|
|
11
|
+
use Fleetbase\FleetOps\Models\ServiceArea;
|
|
12
|
+
use Fleetbase\FleetOps\Models\Vehicle;
|
|
13
|
+
use Fleetbase\FleetOps\Models\Zone;
|
|
14
|
+
|
|
15
|
+
class FleetOpsAiQueryResources
|
|
16
|
+
{
|
|
17
|
+
public static function register(AiQueryRegistry $registry): void
|
|
18
|
+
{
|
|
19
|
+
$registry->register(new AiQueryableResource(
|
|
20
|
+
key: 'fleet-ops.drivers',
|
|
21
|
+
label: 'Fleet-Ops drivers',
|
|
22
|
+
module: 'fleet-ops',
|
|
23
|
+
modelClass: Driver::class,
|
|
24
|
+
permission: 'fleet-ops see driver',
|
|
25
|
+
aliases: ['drivers', 'driver'],
|
|
26
|
+
fields: [
|
|
27
|
+
'online' => ['column' => 'online', 'type' => 'boolean'],
|
|
28
|
+
'status' => ['column' => 'status', 'type' => 'string'],
|
|
29
|
+
'current_status' => ['column' => 'current_status', 'type' => 'string'],
|
|
30
|
+
'city' => ['column' => 'city', 'type' => 'string'],
|
|
31
|
+
'country' => ['column' => 'country', 'type' => 'string'],
|
|
32
|
+
'vehicle_uuid' => ['column' => 'vehicle_uuid', 'type' => 'uuid'],
|
|
33
|
+
'updated_at' => ['column' => 'updated_at', 'type' => 'datetime'],
|
|
34
|
+
'created_at' => ['column' => 'created_at', 'type' => 'datetime'],
|
|
35
|
+
],
|
|
36
|
+
sampleFields: ['public_id', 'name', 'status', 'current_status', 'online', 'city', 'country', 'vehicle_uuid'],
|
|
37
|
+
locationField: 'location',
|
|
38
|
+
directivePermission: 'fleet-ops list driver',
|
|
39
|
+
maxLimit: 500,
|
|
40
|
+
));
|
|
41
|
+
|
|
42
|
+
$registry->register(new AiQueryableResource(
|
|
43
|
+
key: 'fleet-ops.vehicles',
|
|
44
|
+
label: 'Fleet-Ops vehicles',
|
|
45
|
+
module: 'fleet-ops',
|
|
46
|
+
modelClass: Vehicle::class,
|
|
47
|
+
permission: 'fleet-ops see vehicle',
|
|
48
|
+
aliases: ['vehicles', 'vehicle'],
|
|
49
|
+
fields: [
|
|
50
|
+
'online' => ['column' => 'online', 'type' => 'boolean'],
|
|
51
|
+
'status' => ['column' => 'status', 'type' => 'string'],
|
|
52
|
+
'driver_uuid' => ['column' => 'driver_uuid', 'type' => 'uuid'],
|
|
53
|
+
'city' => ['column' => 'city', 'type' => 'string'],
|
|
54
|
+
'country' => ['column' => 'country', 'type' => 'string'],
|
|
55
|
+
'updated_at' => ['column' => 'updated_at', 'type' => 'datetime'],
|
|
56
|
+
'created_at' => ['column' => 'created_at', 'type' => 'datetime'],
|
|
57
|
+
],
|
|
58
|
+
sampleFields: ['public_id', 'display_name', 'status', 'online', 'city', 'country', 'driver_uuid'],
|
|
59
|
+
locationField: 'location',
|
|
60
|
+
directivePermission: 'fleet-ops list vehicle',
|
|
61
|
+
maxLimit: 500,
|
|
62
|
+
));
|
|
63
|
+
|
|
64
|
+
$registry->register(new AiQueryableResource(
|
|
65
|
+
key: 'fleet-ops.devices',
|
|
66
|
+
label: 'Fleet-Ops devices',
|
|
67
|
+
module: 'fleet-ops',
|
|
68
|
+
modelClass: Device::class,
|
|
69
|
+
permission: 'fleet-ops see device',
|
|
70
|
+
aliases: ['devices', 'device'],
|
|
71
|
+
fields: [
|
|
72
|
+
'online' => ['column' => 'online', 'type' => 'boolean'],
|
|
73
|
+
'status' => ['column' => 'status', 'type' => 'string'],
|
|
74
|
+
'provider' => ['column' => 'provider', 'type' => 'string'],
|
|
75
|
+
'telematic_uuid' => ['column' => 'telematic_uuid', 'type' => 'uuid'],
|
|
76
|
+
'attachable_uuid' => ['column' => 'attachable_uuid', 'type' => 'uuid'],
|
|
77
|
+
'last_online_at' => ['column' => 'last_online_at', 'type' => 'datetime'],
|
|
78
|
+
'updated_at' => ['column' => 'updated_at', 'type' => 'datetime'],
|
|
79
|
+
],
|
|
80
|
+
sampleFields: ['public_id', 'name', 'status', 'online', 'provider', 'telematic_uuid', 'attachable_uuid'],
|
|
81
|
+
directivePermission: 'fleet-ops list device',
|
|
82
|
+
maxLimit: 500,
|
|
83
|
+
));
|
|
84
|
+
|
|
85
|
+
$registry->register(new AiQueryableResource(
|
|
86
|
+
key: 'fleet-ops.orders',
|
|
87
|
+
label: 'Fleet-Ops orders',
|
|
88
|
+
module: 'fleet-ops',
|
|
89
|
+
modelClass: Order::class,
|
|
90
|
+
permission: 'fleet-ops see order',
|
|
91
|
+
aliases: ['orders', 'order'],
|
|
92
|
+
fields: [
|
|
93
|
+
'status' => ['column' => 'status', 'type' => 'string'],
|
|
94
|
+
'type' => ['column' => 'type', 'type' => 'string'],
|
|
95
|
+
'driver_assigned_uuid' => ['column' => 'driver_assigned_uuid', 'type' => 'uuid'],
|
|
96
|
+
'vehicle_assigned_uuid' => ['column' => 'vehicle_assigned_uuid', 'type' => 'uuid'],
|
|
97
|
+
'created_at' => ['column' => 'created_at', 'type' => 'datetime'],
|
|
98
|
+
'scheduled_at' => ['column' => 'scheduled_at', 'type' => 'datetime'],
|
|
99
|
+
],
|
|
100
|
+
sampleFields: ['public_id', 'status', 'type', 'driver_assigned_uuid', 'vehicle_assigned_uuid', 'created_at'],
|
|
101
|
+
directivePermission: 'fleet-ops list order',
|
|
102
|
+
maxLimit: 500,
|
|
103
|
+
));
|
|
104
|
+
|
|
105
|
+
$registry->register(new AiQueryableResource(
|
|
106
|
+
key: 'fleet-ops.fleets',
|
|
107
|
+
label: 'Fleet-Ops fleets',
|
|
108
|
+
module: 'fleet-ops',
|
|
109
|
+
modelClass: Fleet::class,
|
|
110
|
+
permission: 'fleet-ops see fleet',
|
|
111
|
+
aliases: ['fleets', 'fleet'],
|
|
112
|
+
fields: [
|
|
113
|
+
'status' => ['column' => 'status', 'type' => 'string'],
|
|
114
|
+
'task' => ['column' => 'task', 'type' => 'string'],
|
|
115
|
+
'updated_at' => ['column' => 'updated_at', 'type' => 'datetime'],
|
|
116
|
+
],
|
|
117
|
+
sampleFields: ['public_id', 'name', 'status', 'task'],
|
|
118
|
+
directivePermission: 'fleet-ops list fleet',
|
|
119
|
+
));
|
|
120
|
+
|
|
121
|
+
$registry->register(new AiQueryableResource(
|
|
122
|
+
key: 'fleet-ops.service_areas',
|
|
123
|
+
label: 'Fleet-Ops service areas',
|
|
124
|
+
module: 'fleet-ops',
|
|
125
|
+
modelClass: ServiceArea::class,
|
|
126
|
+
permission: 'fleet-ops see service-area',
|
|
127
|
+
aliases: ['service areas', 'service area', 'service_areas'],
|
|
128
|
+
fields: [
|
|
129
|
+
'status' => ['column' => 'status', 'type' => 'string'],
|
|
130
|
+
'country' => ['column' => 'country', 'type' => 'string'],
|
|
131
|
+
'type' => ['column' => 'type', 'type' => 'string'],
|
|
132
|
+
],
|
|
133
|
+
sampleFields: ['public_id', 'name', 'status', 'country', 'type'],
|
|
134
|
+
));
|
|
135
|
+
|
|
136
|
+
$registry->register(new AiQueryableResource(
|
|
137
|
+
key: 'fleet-ops.zones',
|
|
138
|
+
label: 'Fleet-Ops zones',
|
|
139
|
+
module: 'fleet-ops',
|
|
140
|
+
modelClass: Zone::class,
|
|
141
|
+
permission: 'fleet-ops see zone',
|
|
142
|
+
aliases: ['zones', 'zone'],
|
|
143
|
+
fields: [
|
|
144
|
+
'status' => ['column' => 'status', 'type' => 'string'],
|
|
145
|
+
'service_area_uuid' => ['column' => 'service_area_uuid', 'type' => 'uuid'],
|
|
146
|
+
],
|
|
147
|
+
sampleFields: ['public_id', 'name', 'status', 'service_area_uuid'],
|
|
148
|
+
));
|
|
149
|
+
}
|
|
150
|
+
}
|
|
@@ -248,6 +248,10 @@ class Utils extends FleetbaseUtils
|
|
|
248
248
|
$coordinates = static::extractPointWktFromQueryExpression($coordinates) ?? $coordinates;
|
|
249
249
|
}
|
|
250
250
|
|
|
251
|
+
if ($geoJsonPoint = static::pointFromGeoJson($coordinates)) {
|
|
252
|
+
return $geoJsonPoint;
|
|
253
|
+
}
|
|
254
|
+
|
|
251
255
|
if ($coordinates instanceof Point) {
|
|
252
256
|
$latitude = $coordinates->getLat();
|
|
253
257
|
$longitude = $coordinates->getLng();
|
|
@@ -452,6 +456,10 @@ class Utils extends FleetbaseUtils
|
|
|
452
456
|
return $coordinates;
|
|
453
457
|
}
|
|
454
458
|
|
|
459
|
+
if ($geoJsonPoint = static::pointFromGeoJson($coordinates)) {
|
|
460
|
+
return $geoJsonPoint;
|
|
461
|
+
}
|
|
462
|
+
|
|
455
463
|
if (static::isGeoJson($coordinates)) {
|
|
456
464
|
$coordinatesJson = null;
|
|
457
465
|
|
|
@@ -541,6 +549,10 @@ class Utils extends FleetbaseUtils
|
|
|
541
549
|
$coordinates = $coordinates->location;
|
|
542
550
|
}
|
|
543
551
|
|
|
552
|
+
if ($geoJsonPoint = static::pointFromGeoJson($coordinates)) {
|
|
553
|
+
$coordinates = $geoJsonPoint;
|
|
554
|
+
}
|
|
555
|
+
|
|
544
556
|
if ($coordinates instanceof Point) {
|
|
545
557
|
$latitude = $coordinates->getLat();
|
|
546
558
|
$longitude = $coordinates->getLng();
|
|
@@ -639,6 +651,10 @@ class Utils extends FleetbaseUtils
|
|
|
639
651
|
return $coordinates;
|
|
640
652
|
}
|
|
641
653
|
|
|
654
|
+
if ($geoJsonPoint = static::pointFromGeoJson($coordinates)) {
|
|
655
|
+
return $geoJsonPoint;
|
|
656
|
+
}
|
|
657
|
+
|
|
642
658
|
if (is_null($coordinates) || !static::isCoordinates($coordinates)) {
|
|
643
659
|
return new Point(0, 0);
|
|
644
660
|
}
|
|
@@ -649,6 +665,42 @@ class Utils extends FleetbaseUtils
|
|
|
649
665
|
return new Point($latitude, $longitude);
|
|
650
666
|
}
|
|
651
667
|
|
|
668
|
+
/**
|
|
669
|
+
* Resolve direct GeoJSON Point values and Feature-wrapped Point values.
|
|
670
|
+
*/
|
|
671
|
+
protected static function pointFromGeoJson($coordinates): ?Point
|
|
672
|
+
{
|
|
673
|
+
if (is_string($coordinates) && static::isJson($coordinates)) {
|
|
674
|
+
$coordinates = json_decode($coordinates, true);
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
if (!is_array($coordinates) && !is_object($coordinates)) {
|
|
678
|
+
return null;
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
if (data_get($coordinates, 'type') === 'Feature') {
|
|
682
|
+
$coordinates = data_get($coordinates, 'geometry');
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
if (data_get($coordinates, 'type') !== 'Point') {
|
|
686
|
+
return null;
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
$pointCoordinates = data_get($coordinates, 'coordinates');
|
|
690
|
+
if (!is_array($pointCoordinates) || count($pointCoordinates) < 2) {
|
|
691
|
+
return null;
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
$longitude = $pointCoordinates[0];
|
|
695
|
+
$latitude = $pointCoordinates[1];
|
|
696
|
+
|
|
697
|
+
if (!is_numeric($latitude) || !is_numeric($longitude)) {
|
|
698
|
+
return null;
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
return new Point((float) $latitude, (float) $longitude);
|
|
702
|
+
}
|
|
703
|
+
|
|
652
704
|
/**
|
|
653
705
|
* Converts a point to a WKT (Well-Known Text) representation for SQL insert.
|
|
654
706
|
*
|
|
@@ -21,7 +21,12 @@ class TrackingOptions
|
|
|
21
21
|
|
|
22
22
|
public static function fromArray(array $options = []): self
|
|
23
23
|
{
|
|
24
|
-
|
|
24
|
+
try {
|
|
25
|
+
$systemSettings = Setting::lookup('fleet-ops.tracking-settings', []);
|
|
26
|
+
} catch (\Throwable) {
|
|
27
|
+
$systemSettings = [];
|
|
28
|
+
}
|
|
29
|
+
|
|
25
30
|
$config = array_merge(config('fleetops.tracking', []), is_array($systemSettings) ? $systemSettings : []);
|
|
26
31
|
$companySettings = [];
|
|
27
32
|
try {
|
package/server/src/routes.php
CHANGED
|
@@ -302,17 +302,6 @@ Route::prefix(config('fleetops.api.routing.prefix'))->namespace('Fleetbase\Fleet
|
|
|
302
302
|
});
|
|
303
303
|
});
|
|
304
304
|
|
|
305
|
-
/*
|
|
306
|
-
|--------------------------------------------------------------------------
|
|
307
|
-
| Publicly Consumable FleetOps API Routes
|
|
308
|
-
|--------------------------------------------------------------------------
|
|
309
|
-
|
|
|
310
|
-
| End-user API routes, these are routes that the SDK and applications will interface with, that DO NOT REQUIRE API credentials.
|
|
311
|
-
*/
|
|
312
|
-
$router->group(['prefix' => 'v1', 'namespace' => 'Api\v1'], function () use ($router) {
|
|
313
|
-
$router->get('organizations', 'OrganizationController@listOrganizations');
|
|
314
|
-
});
|
|
315
|
-
|
|
316
305
|
/*
|
|
317
306
|
|--------------------------------------------------------------------------
|
|
318
307
|
| Webhook Integration Routes
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
<?php
|
|
2
|
+
|
|
3
|
+
use Fleetbase\FleetOps\Models\Place;
|
|
4
|
+
use Fleetbase\FleetOps\Support\Ai\Capabilities\CreateOrderPreviewCapability;
|
|
5
|
+
use Fleetbase\LaravelMysqlSpatial\Types\Point as SpatialPoint;
|
|
6
|
+
use Illuminate\Support\Carbon;
|
|
7
|
+
|
|
8
|
+
function fleetopsAiCreateOrderCapability()
|
|
9
|
+
{
|
|
10
|
+
return (new ReflectionClass(CreateOrderPreviewCapability::class))->newInstanceWithoutConstructor();
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function fleetopsAiCreateOrderProtectedMethod(string $method): ReflectionMethod
|
|
14
|
+
{
|
|
15
|
+
$reflection = new ReflectionClass(CreateOrderPreviewCapability::class);
|
|
16
|
+
$method = $reflection->getMethod($method);
|
|
17
|
+
$method->setAccessible(true);
|
|
18
|
+
|
|
19
|
+
return $method;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
test('create order preview parser extracts pickup and dropoff labels', function (string $prompt, array $expected) {
|
|
23
|
+
$capability = fleetopsAiCreateOrderCapability();
|
|
24
|
+
$method = fleetopsAiCreateOrderProtectedMethod('addressPairFromPrompt');
|
|
25
|
+
|
|
26
|
+
expect($method->invoke($capability, $prompt))->toBe($expected);
|
|
27
|
+
})->with([
|
|
28
|
+
'pickup comma and dropoff' => [
|
|
29
|
+
'Create an order with the pickup 16 simon walk singapore, and the dropoff 18 hougang ave',
|
|
30
|
+
['16 simon walk singapore', '18 hougang ave'],
|
|
31
|
+
],
|
|
32
|
+
'pickup at and drop off to' => [
|
|
33
|
+
'Create an order pickup at 16 simon walk singapore and drop off to 18 hougang ave',
|
|
34
|
+
['16 simon walk singapore', '18 hougang ave'],
|
|
35
|
+
],
|
|
36
|
+
'from to' => [
|
|
37
|
+
'Create an order from 16 simon walk singapore to 18 hougang ave',
|
|
38
|
+
['16 simon walk singapore', '18 hougang ave'],
|
|
39
|
+
],
|
|
40
|
+
]);
|
|
41
|
+
|
|
42
|
+
test('create order preview keeps unresolved addresses as provisional places', function () {
|
|
43
|
+
$capability = fleetopsAiCreateOrderCapability();
|
|
44
|
+
$method = fleetopsAiCreateOrderProtectedMethod('provisionalPlace');
|
|
45
|
+
|
|
46
|
+
expect($method->invoke($capability, '16 simon walk singapore'))->toMatchArray([
|
|
47
|
+
'uuid' => null,
|
|
48
|
+
'name' => '16 simon walk singapore',
|
|
49
|
+
'address' => '16 simon walk singapore',
|
|
50
|
+
'latitude' => null,
|
|
51
|
+
'longitude' => null,
|
|
52
|
+
'source' => 'unresolved',
|
|
53
|
+
]);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test('create order preview serializes coordinates from place location', function () {
|
|
57
|
+
$capability = fleetopsAiCreateOrderCapability();
|
|
58
|
+
$method = fleetopsAiCreateOrderProtectedMethod('serializePlace');
|
|
59
|
+
$place = new Place([
|
|
60
|
+
'name' => '16 Simon Walk',
|
|
61
|
+
'street1' => '16 Simon Walk',
|
|
62
|
+
'city' => 'Singapore',
|
|
63
|
+
'postal_code' => '545870',
|
|
64
|
+
'location' => new SpatialPoint(1.3621663, 103.8845049),
|
|
65
|
+
]);
|
|
66
|
+
|
|
67
|
+
expect($method->invoke($capability, $place, '16 simon walk singapore'))->toMatchArray([
|
|
68
|
+
'latitude' => 1.3621663,
|
|
69
|
+
'longitude' => 103.8845049,
|
|
70
|
+
]);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
test('create order preview route preview excludes return stops', function () {
|
|
74
|
+
$capability = fleetopsAiCreateOrderCapability();
|
|
75
|
+
$method = fleetopsAiCreateOrderProtectedMethod('routePreview');
|
|
76
|
+
|
|
77
|
+
$preview = $method->invoke($capability, [
|
|
78
|
+
'payload' => [
|
|
79
|
+
'pickup' => ['address' => '16 Simon Walk', 'latitude' => 1.3621663, 'longitude' => 103.8845049],
|
|
80
|
+
'dropoff' => ['address' => '18 Hougang Avenue 8', 'latitude' => 1.3701, 'longitude' => 103.8912],
|
|
81
|
+
'return' => ['address' => 'Ignored return', 'latitude' => 1.3, 'longitude' => 103.8],
|
|
82
|
+
],
|
|
83
|
+
]);
|
|
84
|
+
|
|
85
|
+
expect(collect($preview['stops'])->pluck('role')->all())
|
|
86
|
+
->toBe(['pickup', 'dropoff'])
|
|
87
|
+
->and($preview['coordinates'])->toHaveCount(2);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
test('create order apply draft removes embedded places when endpoint uuids exist', function () {
|
|
91
|
+
$capability = new class extends CreateOrderPreviewCapability {
|
|
92
|
+
public function sanitizeForTest(array $draft): array
|
|
93
|
+
{
|
|
94
|
+
return $this->sanitizeDraftForApply($draft);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
protected function hasExistingPlaceUuid($uuid): bool
|
|
98
|
+
{
|
|
99
|
+
return in_array($uuid, ['pickup_uuid_test', 'dropoff_uuid_test'], true);
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
$draft = $capability->sanitizeForTest([
|
|
104
|
+
'payload' => [
|
|
105
|
+
'pickup_uuid' => 'pickup_uuid_test',
|
|
106
|
+
'dropoff_uuid' => 'dropoff_uuid_test',
|
|
107
|
+
'pickup' => ['uuid' => 'pickup_uuid_test', 'location' => ['type' => 'Point', 'coordinates' => [103.851, 1.2816]]],
|
|
108
|
+
'dropoff' => ['uuid' => 'dropoff_uuid_test', 'location' => ['type' => 'Point', 'coordinates' => [103.8318, 1.3048]]],
|
|
109
|
+
'return' => ['location' => ['type' => 'Point', 'coordinates' => [103.8, 1.3]]],
|
|
110
|
+
],
|
|
111
|
+
]);
|
|
112
|
+
|
|
113
|
+
expect($draft['payload'])->toHaveKeys(['pickup_uuid', 'dropoff_uuid', 'return'])
|
|
114
|
+
->and($draft['payload'])->not->toHaveKeys(['pickup', 'dropoff']);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
test('create order preview resolves relative schedule phrases', function () {
|
|
118
|
+
$timezone = date_default_timezone_get();
|
|
119
|
+
date_default_timezone_set('Asia/Singapore');
|
|
120
|
+
Carbon::setTestNow(Carbon::parse('2026-06-30 15:00:00', 'Asia/Singapore'));
|
|
121
|
+
|
|
122
|
+
try {
|
|
123
|
+
$capability = fleetopsAiCreateOrderCapability();
|
|
124
|
+
$method = fleetopsAiCreateOrderProtectedMethod('scheduledAtFromPrompt');
|
|
125
|
+
|
|
126
|
+
expect($method->invoke($capability, 'Create an order scheduled for 3 days from now'))->toBe('2026-07-03T15:00:00+08:00');
|
|
127
|
+
} finally {
|
|
128
|
+
Carbon::setTestNow();
|
|
129
|
+
date_default_timezone_set($timezone);
|
|
130
|
+
}
|
|
131
|
+
});
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
<?php
|
|
2
|
+
|
|
3
|
+
use Fleetbase\Ai\Support\AiQueryRegistry;
|
|
4
|
+
use Fleetbase\FleetOps\Support\Ai\Capabilities\AssetStatusCapability;
|
|
5
|
+
use Fleetbase\FleetOps\Support\Ai\Capabilities\OperationalQueryCapability;
|
|
6
|
+
use Fleetbase\FleetOps\Support\Ai\FleetOpsAiQueryResources;
|
|
7
|
+
use Illuminate\Support\Carbon;
|
|
8
|
+
|
|
9
|
+
function fleetopsAiOperationalCapability()
|
|
10
|
+
{
|
|
11
|
+
return (new ReflectionClass(OperationalQueryCapability::class))->newInstanceWithoutConstructor();
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function fleetopsAiOperationalProtectedMethod(string $method): ReflectionMethod
|
|
15
|
+
{
|
|
16
|
+
$reflection = new ReflectionClass(OperationalQueryCapability::class);
|
|
17
|
+
$method = $reflection->getMethod($method);
|
|
18
|
+
$method->setAccessible(true);
|
|
19
|
+
|
|
20
|
+
return $method;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
test('fleet-ops registers safe ai query resources', function () {
|
|
24
|
+
$registry = new AiQueryRegistry();
|
|
25
|
+
|
|
26
|
+
FleetOpsAiQueryResources::register($registry);
|
|
27
|
+
|
|
28
|
+
expect($registry->find('drivers')->key)->toBe('fleet-ops.drivers')
|
|
29
|
+
->and($registry->find('orders')->hasField('driver_assigned_uuid'))->toBeTrue()
|
|
30
|
+
->and($registry->find('devices')->hasField('online'))->toBeTrue()
|
|
31
|
+
->and($registry->find('service area')->key)->toBe('fleet-ops.service_areas');
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
test('operational query capability matches common fleet-ops data questions', function (string $prompt) {
|
|
35
|
+
$capability = fleetopsAiOperationalCapability();
|
|
36
|
+
$method = fleetopsAiOperationalProtectedMethod('matchesPrompt');
|
|
37
|
+
|
|
38
|
+
expect($method->invoke($capability, strtolower($prompt)))->toBeTrue();
|
|
39
|
+
})->with([
|
|
40
|
+
'How many drivers are currently online?',
|
|
41
|
+
'Where are most of my drivers located?',
|
|
42
|
+
'How many vehicles were online yesterday?',
|
|
43
|
+
'Which service area has the most online drivers?',
|
|
44
|
+
'Show me drivers without vehicles.',
|
|
45
|
+
'How many active orders have no assigned driver?',
|
|
46
|
+
'Break down orders by status for this month.',
|
|
47
|
+
]);
|
|
48
|
+
|
|
49
|
+
test('asset status capability includes driver online prompts', function () {
|
|
50
|
+
$capability = (new ReflectionClass(AssetStatusCapability::class))->newInstanceWithoutConstructor();
|
|
51
|
+
$method = (new ReflectionClass(AssetStatusCapability::class))->getMethod('matchesPrompt');
|
|
52
|
+
$method->setAccessible(true);
|
|
53
|
+
|
|
54
|
+
expect($method->invoke($capability, 'how many drivers are online'))->toBeTrue();
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test('operational query date filters use resolved local windows', function () {
|
|
58
|
+
$timezone = date_default_timezone_get();
|
|
59
|
+
date_default_timezone_set('Asia/Singapore');
|
|
60
|
+
Carbon::setTestNow(Carbon::parse('2026-06-30 15:00:00', 'Asia/Singapore'));
|
|
61
|
+
|
|
62
|
+
try {
|
|
63
|
+
$capability = fleetopsAiOperationalCapability();
|
|
64
|
+
$method = fleetopsAiOperationalProtectedMethod('orderDateFilters');
|
|
65
|
+
$filters = $method->invoke($capability, 'How many orders were created last week?');
|
|
66
|
+
|
|
67
|
+
expect($filters)->toHaveCount(2)
|
|
68
|
+
->and($filters[0])->toMatchArray(['field' => 'created_at', 'operator' => '>='])
|
|
69
|
+
->and($filters[0]['value']->toIso8601String())->toBe('2026-06-22T00:00:00+08:00')
|
|
70
|
+
->and($filters[1]['value']->toIso8601String())->toBe('2026-06-28T23:59:59+08:00');
|
|
71
|
+
} finally {
|
|
72
|
+
Carbon::setTestNow();
|
|
73
|
+
date_default_timezone_set($timezone);
|
|
74
|
+
}
|
|
75
|
+
});
|