@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
@@ -10,16 +10,45 @@ use Fleetbase\FleetOps\Tracking\TrackingContextBuilder;
10
10
  use Fleetbase\FleetOps\Tracking\TrackingOptions;
11
11
  use Fleetbase\FleetOps\Tracking\TrackingProviderManager;
12
12
  use Fleetbase\FleetOps\Tracking\TrackingProviderRegistry;
13
+ use Fleetbase\FleetOps\Tracking\TrackingProviderResult;
13
14
  use Fleetbase\LaravelMysqlSpatial\Types\Point;
14
15
  use Illuminate\Support\Carbon;
15
16
 
17
+ class TrackingTestOrder extends Order
18
+ {
19
+ public function loadMissing($relations)
20
+ {
21
+ return $this;
22
+ }
23
+ }
24
+
25
+ class TrackingTestPayload extends Payload
26
+ {
27
+ public function loadMissing($relations)
28
+ {
29
+ return $this;
30
+ }
31
+ }
32
+
33
+ function trackingModel(string $class): object
34
+ {
35
+ return (new ReflectionClass($class))->newInstanceWithoutConstructor();
36
+ }
37
+
38
+ function trackingSetAttributes($model, array $attributes): void
39
+ {
40
+ $model->setRawAttributes(array_merge($model->getAttributes(), $attributes), true);
41
+ }
42
+
16
43
  function trackingPlace(string $uuid, float $lat, float $lng): Place
17
44
  {
18
- $place = new Place();
19
- $place->uuid = $uuid;
20
- $place->public_id = 'place_' . substr($uuid, 0, 8);
21
- $place->address = 'Test address ' . $uuid;
22
- $place->location = new Point($lat, $lng);
45
+ $place = trackingModel(Place::class);
46
+ trackingSetAttributes($place, [
47
+ 'uuid' => $uuid,
48
+ 'public_id' => 'place_' . substr($uuid, 0, 8),
49
+ 'address' => 'Test address ' . $uuid,
50
+ 'location' => new Point($lat, $lng),
51
+ ]);
23
52
 
24
53
  return $place;
25
54
  }
@@ -28,25 +57,32 @@ function trackingOrderWithStops(): Order
28
57
  {
29
58
  $pickup = trackingPlace('11111111-1111-1111-1111-111111111111', 1.30, 103.80);
30
59
  $dropoff = trackingPlace('22222222-2222-2222-2222-222222222222', 1.35, 103.85);
31
- $payload = new Payload();
32
- $payload->uuid = '33333333-3333-3333-3333-333333333333';
33
- $payload->current_waypoint_uuid = $pickup->uuid;
60
+ $payload = trackingModel(TrackingTestPayload::class);
61
+ trackingSetAttributes($payload, [
62
+ 'uuid' => '33333333-3333-3333-3333-333333333333',
63
+ 'current_waypoint_uuid' => $pickup->uuid,
64
+ ]);
34
65
  $payload->setRelation('pickup', $pickup);
35
66
  $payload->setRelation('dropoff', $dropoff);
67
+ $payload->setRelation('return', null);
36
68
  $payload->setRelation('waypoints', collect());
37
69
  $payload->setRelation('waypointMarkers', collect());
38
70
 
39
- $driver = new Driver();
40
- $driver->uuid = '44444444-4444-4444-4444-444444444444';
41
- $driver->location = new Point(1.29, 103.79);
42
- $driver->online = true;
43
- $driver->updated_at = Carbon::now();
44
-
45
- $order = new Order();
46
- $order->uuid = '55555555-5555-5555-5555-555555555555';
47
- $order->public_id = 'order_test';
48
- $order->status = 'started';
49
- $order->updated_at = Carbon::now();
71
+ $driver = trackingModel(Driver::class);
72
+ trackingSetAttributes($driver, [
73
+ 'uuid' => '44444444-4444-4444-4444-444444444444',
74
+ 'location' => new Point(1.29, 103.79),
75
+ 'online' => true,
76
+ 'updated_at' => Carbon::now(),
77
+ ]);
78
+
79
+ $order = trackingModel(TrackingTestOrder::class);
80
+ trackingSetAttributes($order, [
81
+ 'uuid' => '55555555-5555-5555-5555-555555555555',
82
+ 'public_id' => 'order_test',
83
+ 'status' => 'started',
84
+ 'updated_at' => Carbon::now(),
85
+ ]);
50
86
  $order->setRelation('payload', $payload);
51
87
  $order->setRelation('driverAssigned', $driver);
52
88
 
@@ -59,15 +95,15 @@ test('tracking context builder normalizes order stops and driver telemetry', fun
59
95
  ]));
60
96
 
61
97
  expect($context->stops)->toHaveCount(2)
62
- ->and($context->activeStop?->type)->toBe('dropoff')
63
- ->and($context->nextStop)->toBeNull()
98
+ ->and($context->activeStop?->type)->toBe('pickup')
99
+ ->and($context->nextStop?->type)->toBe('dropoff')
64
100
  ->and($context->driverLocationAgeSeconds)->toBeInt()
65
101
  ->and($context->warnings)->toBe([]);
66
102
  });
67
103
 
68
104
  test('tracking context ignores zero coordinate driver location and falls back to pickup origin', function () {
69
105
  $order = trackingOrderWithStops();
70
- $order->driverAssigned->location = new Point(0, 0);
106
+ trackingSetAttributes($order->driverAssigned, ['location' => new Point(0, 0)]);
71
107
 
72
108
  $context = (new TrackingContextBuilder())->build($order, TrackingOptions::fromArray([
73
109
  'provider' => 'calculated',
@@ -102,9 +138,13 @@ test('tracking route legs include cumulative stop eta values', function () {
102
138
  'provider' => 'calculated',
103
139
  ]));
104
140
 
105
- $result = (new CalculatedTrackingProvider())->track($context, TrackingOptions::fromArray([
106
- 'provider' => 'calculated',
107
- ]));
141
+ $result = new TrackingProviderResult(
142
+ provider: 'test',
143
+ legs: [
144
+ ['duration_s' => 120],
145
+ ['duration_s' => 180],
146
+ ]
147
+ );
108
148
  $service = app(Fleetbase\FleetOps\Tracking\TrackingIntelligenceService::class);
109
149
  $method = new ReflectionMethod($service, 'legs');
110
150
  $method->setAccessible(true);
@@ -56,12 +56,11 @@ test('driver assigned-order workflows expose list and multi-unassign endpoints',
56
56
  ->and($controller)
57
57
  ->toContain('public function assignedOrders(string $id): JsonResponse')
58
58
  ->toContain('public function unassignOrders(Request $request, string $id): JsonResponse')
59
- ->toContain("->where('driver_assigned_uuid', \$driver->uuid)")
59
+ ->toContain("Order::where('driver_assigned_uuid', \$driver->uuid)")
60
60
  ->toContain("->orderByRaw('uuid = ? desc', [\$driver->current_job_uuid])")
61
- ->toContain("\$request->validate(['orders' => ['required', 'array', 'min:1']])")
62
- ->toContain("->where('driver_assigned_uuid', \$driver->uuid)")
63
- ->toContain("->whereIn('uuid', \$selectedOrderUuids)")
64
- ->toContain("->update(['driver_assigned_uuid' => null])")
61
+ ->toContain("'orders' => 'required|array|min:1'")
62
+ ->toContain("->whereIn('uuid', \$ids)->orWhereIn('public_id', \$ids)")
63
+ ->toContain("'driver_assigned_uuid' => null")
65
64
  ->toContain("if (\$driver->current_job_uuid && \$orders->contains('uuid', \$driver->current_job_uuid))")
66
65
  ->toContain("\$driver->update(['current_job_uuid' => null])")
67
66
  ->and($resource)
@@ -83,10 +82,10 @@ test('vehicle assigned-order workflows expose list and multi-unassign endpoints
83
82
  ->and($controller)
84
83
  ->toContain('public function assignedOrders(string $id): JsonResponse')
85
84
  ->toContain('public function unassignOrders(Request $request, string $id): JsonResponse')
86
- ->toContain("->where('vehicle_assigned_uuid', \$vehicle->uuid)")
87
- ->toContain("\$request->validate(['orders' => ['required', 'array', 'min:1']])")
88
- ->toContain("->whereIn('uuid', \$selectedOrderUuids)")
89
- ->toContain("->update(['vehicle_assigned_uuid' => null])")
85
+ ->toContain("Order::where('vehicle_assigned_uuid', \$vehicle->uuid)")
86
+ ->toContain("'orders' => 'required|array|min:1'")
87
+ ->toContain("->whereIn('uuid', \$ids)->orWhereIn('public_id', \$ids)")
88
+ ->toContain("'vehicle_assigned_uuid' => null")
90
89
  ->not->toContain("->update(['vehicle_uuid' => null])")
91
90
  ->and($resource)
92
91
  ->toContain("'assigned_orders_count'")
@@ -1,31 +0,0 @@
1
- {{#if this.visible}}
2
- {{#if @columnAction.separator}}
3
- <div class="next-dd-menu-seperator"></div>
4
- {{else}}
5
- <div role="group" class="px-1">
6
- <a
7
- href="javascript:;"
8
- role="menuitem"
9
- class="next-dd-item {{@columnAction.class}} {{if this.disabled 'disabled'}}"
10
- disabled={{this.disabled}}
11
- {{on "click" (fn this.onClick @columnAction @row @dd)}}
12
- >
13
- {{#if @columnAction.icon}}
14
- <div class="w-6">
15
- <FaIcon class={{@columnAction.iconClass}} @icon={{@columnAction.icon}} @prefix={{@columnAction.iconPrefix}} />
16
- </div>
17
- {{/if}}
18
- {{@columnAction.label}}
19
- {{#if this.doesntHavePermissions}}
20
- <Attach::Tooltip @class="clean" @animation="scale" @placement={{or @columnAction.tooltipPlacement "right"}}>
21
- <InputInfo @text={{t "common.unauthorized"}} />
22
- </Attach::Tooltip>
23
- {{else if @columnAction.helpText}}
24
- <Attach::Tooltip @class="clean" @animation="scale" @placement={{or @columnAction.tooltipPlacement "right"}}>
25
- <InputInfo @text={{@columnAction.helpText}} @exampleText={{@columnAction.exampleText}} />
26
- </Attach::Tooltip>
27
- {{/if}}
28
- </a>
29
- </div>
30
- {{/if}}
31
- {{/if}}
@@ -1,62 +0,0 @@
1
- import Component from '@glimmer/component';
2
- import { tracked } from '@glimmer/tracking';
3
- import { inject as service } from '@ember/service';
4
- import { action } from '@ember/object';
5
- import { isNone } from '@ember/utils';
6
-
7
- export default class TableCellDropdownActionItemComponent extends Component {
8
- @service abilities;
9
- @tracked permissionRequired;
10
- @tracked doesntHavePermissions = false;
11
- @tracked disabled = false;
12
- @tracked visible = true;
13
-
14
- constructor(owner, { columnAction = {}, row = {}, disabled = false, permission = null }) {
15
- super(...arguments);
16
- this.permissionRequired = columnAction.permission ?? permission;
17
- this.disabled = this.disabledCheck(columnAction, this.permissionRequired, disabled);
18
- this.visible = (columnAction.visible ?? true) && this.visibilityCheck(columnAction, row);
19
- }
20
-
21
- @action onClick(columnAction, row, dd) {
22
- if (this.disabled) {
23
- return;
24
- }
25
-
26
- if (typeof dd?.actions?.close === 'function') {
27
- dd.actions.close();
28
- }
29
-
30
- if (typeof columnAction?.fn === 'function') {
31
- columnAction.fn(row);
32
- }
33
- }
34
-
35
- disabledCheck(columnAction, permission, defaultValue = false) {
36
- let disabled = columnAction.disabled ?? defaultValue;
37
- if (!disabled) {
38
- disabled = permission && this.abilities.cannot(permission);
39
- this.doesntHavePermissions = disabled;
40
- }
41
-
42
- return disabled;
43
- }
44
-
45
- visibilityCheck(columnAction, context) {
46
- const isVisible = columnAction.isVisible;
47
-
48
- if (isNone(context) || !isVisible) {
49
- return true;
50
- }
51
-
52
- if (typeof isVisible === 'boolean') {
53
- return isVisible;
54
- }
55
-
56
- if (typeof isVisible === 'function') {
57
- return isVisible(context);
58
- }
59
-
60
- return true;
61
- }
62
- }
@@ -1,28 +0,0 @@
1
- <div class="cell-dropdown-button overflow-visible {{@column.wrapperClass}}" {{did-insert this.setupComponent}} ...attributes>
2
- <DropdownButton
3
- @icon={{@column.ddButtonIcon}}
4
- @iconPrefix={{@column.ddButtonIconPrefix}}
5
- @text={{this.buttonText}}
6
- @size="xs"
7
- @horizontalPosition="left"
8
- @calculatePosition={{this.calculatePosition}}
9
- @renderInPlace={{true}}
10
- @onOpen={{this.onOpen}}
11
- @onClose={{this.onClose}}
12
- as |dd|
13
- >
14
- <div class="next-dd-menu next-dd-menu-table-dd mt-0i {{@column.ddMenuWrapperClass}}" role="menu" aria-orientation="vertical" aria-labelledby="user-menu">
15
- {{#if @column.ddMenuLabel}}
16
- <div class="next-dd-menu-label-container px-1">
17
- <div class="next-dd-menu-label {{@column.ddMenuLabelClass}}">
18
- {{@column.ddMenuLabel}}
19
- </div>
20
- </div>
21
- <div class="next-dd-menu-seperator"></div>
22
- {{/if}}
23
- {{#each @column.actions as |columnAction|}}
24
- <Table::Cell::Dropdown::ActionItem @columnAction={{columnAction}} @row={{@row}} @column={{@column}} @dd={{dd}} />
25
- {{/each}}
26
- </div>
27
- </DropdownButton>
28
- </div>
@@ -1,91 +0,0 @@
1
- import Component from '@glimmer/component';
2
- import { tracked } from '@glimmer/tracking';
3
- import { action, computed } from '@ember/object';
4
-
5
- export default class TableCellDropdownComponent extends Component {
6
- @tracked tableCellNode;
7
- defaultButtonText = 'Actions';
8
-
9
- @computed('args.column.ddButtonText', 'defaultButtonText') get buttonText() {
10
- const { ddButtonText } = this.args.column;
11
-
12
- if (ddButtonText === undefined) {
13
- return this.defaultButtonText;
14
- }
15
-
16
- if (ddButtonText === false) {
17
- return null;
18
- }
19
-
20
- return ddButtonText;
21
- }
22
-
23
- @action setupComponent(dropdownWrapperNode) {
24
- const tableCellNode = this.getOwnerTableCell(dropdownWrapperNode);
25
-
26
- if (tableCellNode) {
27
- tableCellNode.style.overflow = 'visible';
28
- }
29
-
30
- this.tableCellNode = tableCellNode;
31
- }
32
-
33
- @action onOpen() {
34
- if (!this.tableCellNode) {
35
- return;
36
- }
37
-
38
- const currentZIndex = Number.parseInt(this.tableCellNode.style.zIndex, 10) || 0;
39
- this.tableCellNode.style.zIndex = currentZIndex + 1;
40
- }
41
-
42
- @action onClose() {
43
- if (!this.tableCellNode) {
44
- return;
45
- }
46
-
47
- const currentZIndex = Number.parseInt(this.tableCellNode.style.zIndex, 10) || 1;
48
- this.tableCellNode.style.zIndex = currentZIndex - 1;
49
- }
50
-
51
- @action getOwnerTableCell(dropdownWrapperNode) {
52
- while (dropdownWrapperNode) {
53
- dropdownWrapperNode = dropdownWrapperNode.parentNode;
54
-
55
- if (dropdownWrapperNode?.tagName?.toLowerCase() === 'td') {
56
- return dropdownWrapperNode;
57
- }
58
- }
59
-
60
- return undefined;
61
- }
62
-
63
- @action onDropdownItemClick(columnAction, row, dd) {
64
- if (typeof dd?.actions?.close === 'function') {
65
- dd.actions.close();
66
- }
67
-
68
- if (typeof columnAction?.fn === 'function') {
69
- columnAction.fn(row);
70
- }
71
- }
72
-
73
- @action calculatePosition(trigger, content) {
74
- if (typeof this.args.column?.calculatePosition === 'function') {
75
- return this.args.column.calculatePosition(trigger, content);
76
- }
77
-
78
- const triggerRect = trigger.getBoundingClientRect();
79
- const contentRect = content?.getBoundingClientRect?.();
80
- const contentWidth = contentRect?.width || 224;
81
-
82
- const style = {
83
- position: 'fixed',
84
- marginTop: '0px',
85
- left: `${triggerRect.left - contentWidth - 3}px`,
86
- top: `${triggerRect.top}px`,
87
- };
88
-
89
- return { style };
90
- }
91
- }
@@ -1 +0,0 @@
1
- export { default } from '@fleetbase/fleetops-engine/components/table/cell/dropdown/action-item';
@@ -1 +0,0 @@
1
- export { default } from '@fleetbase/fleetops-engine/components/table/cell/dropdown';
@@ -1,45 +0,0 @@
1
- <?php
2
-
3
- namespace Fleetbase\FleetOps\Http\Controllers\Api\v1;
4
-
5
- use Fleetbase\Http\Controllers\Controller;
6
- use Fleetbase\Http\Resources\Organization;
7
- use Fleetbase\Models\Company;
8
- use Fleetbase\Models\Setting;
9
- use Illuminate\Http\Request;
10
-
11
- class OrganizationController extends Controller
12
- {
13
- /**
14
- * Return List organizations.
15
- *
16
- * @return Organization
17
- */
18
- public function listOrganizations(Request $request)
19
- {
20
- $limit = $request->input('limit', 10);
21
- $withDriverOnboardEnabled = $request->boolean('with_driver_onboard');
22
-
23
- $companies = Company::whereHas('users')->get()->map(function ($company) {
24
- return [
25
- 'name' => $company->name,
26
- 'id' => $company->public_id,
27
- ];
28
- });
29
-
30
- if ($withDriverOnboardEnabled) {
31
- $driverOnboardSettings = Setting::where('key', 'fleet-ops.driver-onboard-settings')->value('value');
32
-
33
- $companies = $companies->filter(function ($company) use ($driverOnboardSettings) {
34
- return $driverOnboardSettings && isset($driverOnboardSettings[$company['uuid']]) && data_get($driverOnboardSettings[$company['uuid']], 'enableDriverOnboardFromApp') === true;
35
- });
36
- }
37
-
38
- // limit
39
- $companies = $companies->take($limit);
40
-
41
- $companiesArray = $companies->values()->toArray();
42
-
43
- return response()->json($companiesArray);
44
- }
45
- }