@fleetbase/fleetops-engine 0.6.45 → 0.6.47

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 (148) hide show
  1. package/addon/components/admin/map-settings.hbs +26 -0
  2. package/addon/components/admin/map-settings.js +46 -0
  3. package/addon/components/admin/routing-settings.hbs +11 -0
  4. package/addon/components/cell/order-route-type.hbs +46 -0
  5. package/addon/components/cell/order-route-type.js +90 -0
  6. package/addon/components/cell/place-address.hbs +1 -0
  7. package/addon/components/display-place.hbs +45 -56
  8. package/addon/components/display-place.js +21 -0
  9. package/addon/components/layout/fleet-ops-sidebar/driver-listing.js +5 -5
  10. package/addon/components/layout/fleet-ops-sidebar/fleet-listing.js +5 -5
  11. package/addon/components/layout/fleet-ops-sidebar.js +8 -0
  12. package/addon/components/maintenance-schedule/form.js +2 -1
  13. package/addon/components/map/drawer/driver-listing.js +5 -5
  14. package/addon/components/map/drawer/place-listing.js +5 -5
  15. package/addon/components/map/drawer/position-listing.js +102 -21
  16. package/addon/components/map/drawer/vehicle-listing.js +5 -5
  17. package/addon/components/map/google-live-map.hbs +9 -0
  18. package/addon/components/map/google-live-map.js +445 -0
  19. package/addon/components/map/leaflet-live-map.hbs +186 -142
  20. package/addon/components/map/leaflet-live-map.js +121 -41
  21. package/addon/components/map/toolbar/visibility-control-panel.js +2 -2
  22. package/addon/components/map/toolbar/zones-panel.hbs +9 -9
  23. package/addon/components/map/toolbar.hbs +3 -3
  24. package/addon/components/map/toolbar.js +9 -1
  25. package/addon/components/modals/map-layer-form.hbs +132 -63
  26. package/addon/components/modals/orchestrator-import.hbs +4 -11
  27. package/addon/components/modals/orchestrator-import.js +17 -1
  28. package/addon/components/modals/update-order-activity.hbs +1 -1
  29. package/addon/components/orchestrator/order-pool.js +3 -2
  30. package/addon/components/orchestrator-workbench.js +29 -41
  31. package/addon/components/order/details/detail.js +2 -2
  32. package/addon/components/order/details/payload.hbs +5 -5
  33. package/addon/components/order/details/payload.js +18 -1
  34. package/addon/components/order/details/route.hbs +2 -2
  35. package/addon/components/order/details/route.js +76 -3
  36. package/addon/components/order/form/details.hbs +1 -2
  37. package/addon/components/order/form/details.js +6 -0
  38. package/addon/components/order/form/payload.hbs +33 -0
  39. package/addon/components/order/form/payload.js +1 -0
  40. package/addon/components/order/form/route.hbs +8 -8
  41. package/addon/components/order/form/route.js +86 -18
  42. package/addon/components/order/form.js +2 -2
  43. package/addon/components/order/route-editor.hbs +209 -147
  44. package/addon/components/order/route-editor.js +143 -102
  45. package/addon/components/place/address.hbs +1 -0
  46. package/addon/components/place/address.js +12 -0
  47. package/addon/components/route-list.hbs +43 -55
  48. package/addon/components/route-list.js +47 -0
  49. package/addon/components/route-optimization-wizard-panel.js +3 -4
  50. package/addon/components/vehicle/card.hbs +4 -4
  51. package/addon/controllers/maintenance/schedules/index.js +2 -1
  52. package/addon/controllers/operations/orders/index/details.js +110 -37
  53. package/addon/controllers/operations/orders/index/new.js +1 -1
  54. package/addon/controllers/operations/orders/index.js +8 -1
  55. package/addon/controllers/settings/map.js +97 -0
  56. package/addon/controllers/settings/payments/index.js +1 -1
  57. package/addon/controllers/settings/routing.js +47 -12
  58. package/addon/extension.js +11 -0
  59. package/addon/helpers/place-address-html.js +6 -0
  60. package/addon/instance-initializers/register-google-routes.js +17 -0
  61. package/addon/instance-initializers/register-osrm.js +5 -0
  62. package/addon/routes/application.js +6 -0
  63. package/addon/routes/operations/orchestrator.js +13 -0
  64. package/addon/routes/operations/orders/index/details.js +29 -22
  65. package/addon/routes/operations/orders/index/new.js +9 -0
  66. package/addon/routes/operations/orders/index.js +0 -1
  67. package/addon/routes/settings/map.js +3 -0
  68. package/addon/routes.js +1 -0
  69. package/addon/services/entity-actions.js +1 -1
  70. package/addon/services/geofence-event-bus.js +2 -1
  71. package/addon/services/geofence.js +204 -168
  72. package/addon/services/google-routes.js +300 -0
  73. package/addon/services/leaflet-draw-restriction.js +2 -1
  74. package/addon/services/leaflet-map-manager.js +17 -0
  75. package/addon/services/map-adapter/google.js +2205 -0
  76. package/addon/services/map-adapter/leaflet.js +1042 -0
  77. package/addon/services/map-adapter-interface.js +551 -0
  78. package/addon/services/map-manager.js +768 -0
  79. package/addon/services/map-settings.js +75 -0
  80. package/addon/services/movement-tracker.js +83 -32
  81. package/addon/services/order-actions.js +11 -1
  82. package/addon/services/osrm.js +79 -6
  83. package/addon/services/position-playback.js +135 -62
  84. package/addon/services/route-engine.js +83 -0
  85. package/addon/services/service-areas.js +8 -1
  86. package/addon/styles/fleetops-engine.css +555 -0
  87. package/addon/templates/management/vehicles/index.hbs +0 -1
  88. package/addon/templates/operations/orders/index/details.hbs +0 -1
  89. package/addon/templates/settings/map.hbs +53 -0
  90. package/addon/templates/settings/routing.hbs +15 -4
  91. package/addon/utils/fleet-ops-options.js +1 -0
  92. package/addon/utils/has-valid-coordinates.js +6 -0
  93. package/addon/utils/map-provider-options.js +6 -0
  94. package/addon/utils/order-route-editing.js +86 -0
  95. package/addon/utils/order-route-summary.js +20 -0
  96. package/addon/utils/place-address-html.js +41 -0
  97. package/addon/utils/route-colors.js +43 -12
  98. package/addon/utils/route-visualization.js +93 -0
  99. package/addon/utils/vendor-integration.js +2 -1
  100. package/app/components/admin/map-settings.js +1 -0
  101. package/app/components/admin/routing-settings.js +1 -0
  102. package/app/components/cell/order-route-type.js +1 -0
  103. package/app/components/cell/place-address.js +1 -0
  104. package/app/components/map/google-live-map.js +1 -0
  105. package/app/components/place/address.js +1 -0
  106. package/app/controllers/settings/map.js +1 -0
  107. package/app/helpers/place-address-html.js +1 -0
  108. package/app/routes/settings/map.js +1 -0
  109. package/app/services/map-adapter/google.js +1 -0
  110. package/app/services/map-adapter/leaflet.js +1 -0
  111. package/app/services/map-adapter-interface.js +1 -0
  112. package/app/services/map-manager.js +1 -0
  113. package/app/services/map-settings.js +1 -0
  114. package/app/templates/settings/map.js +1 -0
  115. package/app/utils/has-valid-coordinates.js +1 -0
  116. package/app/utils/order-route-editing.js +2 -0
  117. package/app/utils/order-route-summary.js +1 -0
  118. package/app/utils/place-address-html.js +1 -0
  119. package/app/utils/route-colors.js +1 -1
  120. package/app/utils/route-visualization.js +1 -0
  121. package/assets/images/drawing-controls-spritesheet.svg +156 -0
  122. package/composer.json +1 -1
  123. package/extension.json +1 -1
  124. package/package.json +4 -4
  125. package/pnpm-workspace.yaml +7 -0
  126. package/server/src/Auth/Schemas/FleetOps.php +5 -0
  127. package/server/src/Http/Controllers/Api/v1/OrderController.php +31 -3
  128. package/server/src/Http/Controllers/Api/v1/PayloadController.php +14 -1
  129. package/server/src/Http/Controllers/Api/v1/ServiceAreaController.php +2 -0
  130. package/server/src/Http/Controllers/Api/v1/ServiceQuoteController.php +3 -2
  131. package/server/src/Http/Controllers/Api/v1/ZoneController.php +2 -0
  132. package/server/src/Http/Controllers/Internal/v1/LiveController.php +2 -1
  133. package/server/src/Http/Controllers/Internal/v1/OrchestrationController.php +7 -4
  134. package/server/src/Http/Controllers/Internal/v1/OrderController.php +90 -34
  135. package/server/src/Http/Controllers/Internal/v1/ServiceQuoteController.php +3 -2
  136. package/server/src/Http/Controllers/Internal/v1/SettingController.php +106 -3
  137. package/server/src/Http/Filter/OrderFilter.php +2 -1
  138. package/server/src/Http/Resources/v1/Index/Payload.php +9 -7
  139. package/server/src/Http/Resources/v1/Order.php +20 -1
  140. package/server/src/Models/Order.php +42 -1
  141. package/server/src/Models/OrderConfig.php +15 -0
  142. package/server/src/Models/Payload.php +131 -27
  143. package/server/src/Models/Place.php +182 -48
  144. package/server/src/Models/ServiceRate.php +4 -3
  145. package/server/src/Orchestration/Engines/VroomOrchestrationEngine.php +59 -23
  146. package/server/src/Support/Utils.php +36 -2
  147. package/server/src/routes.php +4 -0
  148. package/translations/en-us.yaml +1937 -1916
@@ -100,6 +100,11 @@ class OrderController extends Controller
100
100
  $pickup = data_get($payloadInput, 'pickup');
101
101
  $dropoff = data_get($payloadInput, 'dropoff');
102
102
  $return = data_get($payloadInput, 'return');
103
+ $hasPickupField = array_key_exists('pickup', $payloadInput);
104
+ $hasDropoffField = array_key_exists('dropoff', $payloadInput);
105
+ $hasReturnField = array_key_exists('return', $payloadInput);
106
+ $hasWaypointsField = array_key_exists('waypoints', $payloadInput);
107
+ $hasRouteEndpointFields = $hasPickupField || $hasDropoffField || $hasReturnField;
103
108
 
104
109
  if ($pickup) {
105
110
  $payload->setPickup($pickup, [
@@ -414,14 +419,21 @@ class OrderController extends Controller
414
419
  $payload->save();
415
420
 
416
421
  // set waypoints and entities after payload is saved
417
- if ($waypoints) {
422
+ if ($hasWaypointsField && is_array($waypoints) && count($waypoints)) {
418
423
  $payload->setWaypoints($waypoints);
424
+ } elseif ($hasWaypointsField || $hasRouteEndpointFields) {
425
+ $payload->removeWaypoints();
419
426
  }
420
427
 
421
428
  if ($entities) {
422
429
  $payload->setEntities($entities);
423
430
  }
424
431
 
432
+ $firstWaypoint = $payload->getPickupOrFirstWaypoint();
433
+ if ($firstWaypoint instanceof Place) {
434
+ $payload->setCurrentWaypoint($firstWaypoint);
435
+ }
436
+
425
437
  $input['payload_uuid'] = $payload->uuid;
426
438
  } elseif ($request->has('payload')) {
427
439
  $input['payload_uuid'] = Utils::getUuid('payloads', [
@@ -440,6 +452,11 @@ class OrderController extends Controller
440
452
  $pickup = data_get($payloadInput, 'pickup');
441
453
  $dropoff = data_get($payloadInput, 'dropoff');
442
454
  $return = data_get($payloadInput, 'return');
455
+ $hasPickupField = $request->exists('pickup');
456
+ $hasDropoffField = $request->exists('dropoff');
457
+ $hasReturnField = $request->exists('return');
458
+ $hasWaypointsField = $request->exists('waypoints');
459
+ $hasRouteEndpointFields = $hasPickupField || $hasDropoffField || $hasReturnField;
443
460
 
444
461
  // if no pickup and dropoff extract from waypoints
445
462
  if (empty($pickup) && empty($dropoff) && count($waypoints)) {
@@ -462,14 +479,21 @@ class OrderController extends Controller
462
479
  $payload->save();
463
480
 
464
481
  // set waypoints and entities after payload is saved
465
- if ($waypoints) {
482
+ if ($hasWaypointsField && is_array($waypoints) && count($waypoints)) {
466
483
  $payload->setWaypoints($waypoints);
484
+ } elseif ($hasWaypointsField || $hasRouteEndpointFields) {
485
+ $payload->removeWaypoints();
467
486
  }
468
487
 
469
488
  if ($entities) {
470
489
  $payload->setEntities($entities);
471
490
  }
472
491
 
492
+ $firstWaypoint = $payload->getPickupOrFirstWaypoint();
493
+ if ($firstWaypoint instanceof Place) {
494
+ $payload->setCurrentWaypoint($firstWaypoint);
495
+ }
496
+
473
497
  $input['payload_uuid'] = $payload->uuid;
474
498
  }
475
499
 
@@ -1159,7 +1183,11 @@ class OrderController extends Controller
1159
1183
  })->first();
1160
1184
  }
1161
1185
 
1162
- $activities = $order->config()->nextActivity($waypoint);
1186
+ $orderConfig = $order->ensureOrderConfig();
1187
+ if (!$orderConfig) {
1188
+ return response()->apiError('No order config found for order.');
1189
+ }
1190
+ $activities = $orderConfig->nextActivity($waypoint);
1163
1191
 
1164
1192
  // If activity is to complete order add proof of delivery properties if required
1165
1193
  // This is a temporary fix until activity is updated to handle POD on it's own
@@ -31,6 +31,11 @@ class PayloadController extends Controller
31
31
  $pickup = data_get($input, 'pickup');
32
32
  $dropoff = data_get($input, 'dropoff');
33
33
  $return = data_get($input, 'return');
34
+ $hasPickupField = array_key_exists('pickup', $input);
35
+ $hasDropoffField = array_key_exists('dropoff', $input);
36
+ $hasReturnField = array_key_exists('return', $input);
37
+ $hasWaypointsField = array_key_exists('waypoints', $input);
38
+ $hasRouteEndpointFields = $hasPickupField || $hasDropoffField || $hasReturnField;
34
39
 
35
40
  // make sure company is set
36
41
  $input['company_uuid'] = session('company');
@@ -116,8 +121,10 @@ class PayloadController extends Controller
116
121
  }
117
122
 
118
123
  // set waypoints
119
- if ($waypoints) {
124
+ if ($hasWaypointsField && is_array($waypoints) && count($waypoints)) {
120
125
  $payload->setWaypoints($waypoints);
126
+ } elseif ($hasWaypointsField || $hasRouteEndpointFields) {
127
+ $payload->removeWaypoints();
121
128
  }
122
129
 
123
130
  // set entities
@@ -131,6 +138,12 @@ class PayloadController extends Controller
131
138
  // save the payload
132
139
  $payload->save();
133
140
 
141
+ // set the first / current waypoint from the effective route shape
142
+ $firstWaypoint = $payload->getPickupOrFirstWaypoint();
143
+ if ($firstWaypoint instanceof Place) {
144
+ $payload->setCurrentWaypoint($firstWaypoint);
145
+ }
146
+
134
147
  // make sure entities and waypoints is loaded
135
148
  $payload->load(['entities', 'waypoints', 'pickup', 'dropoff', 'return']);
136
149
 
@@ -65,6 +65,7 @@ class ServiceAreaController extends Controller
65
65
  // create the serviceArea
66
66
  try {
67
67
  $serviceArea = ServiceArea::create($input);
68
+ $serviceArea->refresh();
68
69
  } catch (\Throwable $e) {
69
70
  logger()->error('Unable to create service area.', ['error' => $e->getMessage()]);
70
71
 
@@ -135,6 +136,7 @@ class ServiceAreaController extends Controller
135
136
 
136
137
  // update the serviceArea
137
138
  $serviceArea->update($input);
139
+ $serviceArea->refresh();
138
140
 
139
141
  // response the serviceArea resource
140
142
  return new ServiceAreaResource($serviceArea);
@@ -232,6 +232,7 @@ class ServiceQuoteController extends Controller
232
232
 
233
233
  // should all be Place like
234
234
  $waypoints = collect([$pickup, ...$waypoints, $dropoff])->filter();
235
+ $endpointCount = (int) ($pickup instanceof Place) + (int) ($dropoff instanceof Place);
235
236
 
236
237
  // if facilitator is an integrated partner resolve service quotes from bridge
237
238
  if ($facilitator && Utils::isIntegratedVendorId($facilitator)) {
@@ -281,7 +282,7 @@ class ServiceQuoteController extends Controller
281
282
  $serviceQuotes = collect();
282
283
 
283
284
  if ($serviceRate) {
284
- [$subTotal, $lines] = $serviceRate->quoteFromPreliminaryData($entities, $waypoints, $totalDistance, $totalTime, $isCashOnDelivery);
285
+ [$subTotal, $lines] = $serviceRate->quoteFromPreliminaryData($entities, $waypoints, $totalDistance, $totalTime, $isCashOnDelivery, $endpointCount);
285
286
 
286
287
  $quote = ServiceQuote::create([
287
288
  'request_id' => $requestId,
@@ -329,7 +330,7 @@ class ServiceQuoteController extends Controller
329
330
 
330
331
  // calculate quotes
331
332
  foreach ($serviceRates as $serviceRate) {
332
- [$subTotal, $lines] = $serviceRate->quoteFromPreliminaryData($entities, $waypoints, $totalDistance, $totalTime, $isCashOnDelivery);
333
+ [$subTotal, $lines] = $serviceRate->quoteFromPreliminaryData($entities, $waypoints, $totalDistance, $totalTime, $isCashOnDelivery, $endpointCount);
333
334
 
334
335
  $quote = ServiceQuote::create([
335
336
  'request_id' => $requestId,
@@ -69,6 +69,7 @@ class ZoneController extends Controller
69
69
 
70
70
  // create the zone
71
71
  $zone = Zone::create($input);
72
+ $zone->refresh();
72
73
 
73
74
  // response the zone resource
74
75
  return new ZoneResource($zone);
@@ -134,6 +135,7 @@ class ZoneController extends Controller
134
135
 
135
136
  // update the zone
136
137
  $zone->update($input);
138
+ $zone->refresh();
137
139
 
138
140
  // response the zone resource
139
141
  return new ZoneResource($zone);
@@ -128,10 +128,11 @@ class LiveController extends Controller
128
128
  ->applyDirectivesForPermissions('fleet-ops list order')
129
129
  ->with([
130
130
  'payload.entities',
131
- 'payload.waypoints',
132
131
  'payload.dropoff',
133
132
  'payload.pickup',
134
133
  'payload.return',
134
+ 'payload.firstWaypointMarker.place',
135
+ 'payload.lastWaypointMarker.place',
135
136
  'trackingNumber',
136
137
  'trackingStatuses',
137
138
  'driverAssigned' => function ($query) {
@@ -574,12 +574,15 @@ class OrchestrationController extends Controller
574
574
 
575
575
  // ── Resolve OrderConfig ───────────────────────────────────────
576
576
  $orderConfigUuid = null;
577
+ $resolvedOrderConfig = null;
577
578
  if (!empty($firstRow['type'])) {
578
- $orderConfig = OrderConfig::resolveFromIdentifier($firstRow['type']);
579
- if ($orderConfig) {
580
- $orderConfigUuid = $orderConfig->uuid;
579
+ $resolvedOrderConfig = OrderConfig::resolveFromIdentifier($firstRow['type']);
580
+ if ($resolvedOrderConfig) {
581
+ $orderConfigUuid = $resolvedOrderConfig->uuid;
581
582
  }
582
583
  }
584
+ $resolvedOrderConfig ??= OrderConfig::defaultOrCreate();
585
+ $orderConfigUuid = $resolvedOrderConfig?->uuid;
583
586
 
584
587
  // ── Resolve Customer ─────────────────────────────────────────
585
588
  $customerUuid = null;
@@ -660,7 +663,7 @@ class OrchestrationController extends Controller
660
663
  'driver_assigned_uuid' => $driverUuid,
661
664
  'internal_id' => $firstRow['internal_id'] ?? null,
662
665
  'status' => $firstRow['status'] ?? 'created',
663
- 'type' => $firstRow['type'] ?? 'default',
666
+ 'type' => $resolvedOrderConfig?->key ?? 'transport',
664
667
  'notes' => $firstRow['notes'] ?? null,
665
668
  'scheduled_at' => !empty($firstRow['scheduled_at'])
666
669
  ? Carbon::parse($firstRow['scheduled_at'])
@@ -97,6 +97,8 @@ class OrderController extends FleetOpsController
97
97
  function ($request, &$input) {
98
98
  $serviceQuote = ServiceQuote::resolveFromRequest($request);
99
99
 
100
+ $this->normalizeCustomerType($input);
101
+
100
102
  // if service quote is applied, resolve it
101
103
  if ($serviceQuote instanceof ServiceQuote && $serviceQuote->fromIntegratedVendor()) {
102
104
  // create order with integrated vendor, then resume fleetbase order creation
@@ -109,9 +111,19 @@ class OrderController extends FleetOpsController
109
111
  $input['integrated_vendor_order'] = $integratedVendorOrder;
110
112
  }
111
113
 
112
- // if no type is set its default to default
113
- if (!isset($input['type'])) {
114
- $input['type'] = 'default';
114
+ // Normalize order config + type onto the company transport
115
+ // config when an explicit config was not provided.
116
+ $resolvedOrderConfig = null;
117
+ if (!empty($input['order_config_uuid']) || !empty($input['type'])) {
118
+ $resolvedOrderConfig = OrderConfig::resolveFromIdentifier([$input['order_config_uuid'] ?? null, $input['type'] ?? null]);
119
+ }
120
+ $resolvedOrderConfig ??= OrderConfig::defaultOrCreate();
121
+
122
+ if ($resolvedOrderConfig) {
123
+ $input['order_config_uuid'] = $resolvedOrderConfig->uuid;
124
+ $input['type'] = $resolvedOrderConfig->key;
125
+ } elseif (!isset($input['type'])) {
126
+ $input['type'] = 'transport';
115
127
  }
116
128
 
117
129
  // if no status is set its default to `created`
@@ -119,14 +131,6 @@ class OrderController extends FleetOpsController
119
131
  $input['status'] = 'created';
120
132
  }
121
133
 
122
- // Set order config
123
- if (!isset($input['order_config_uuid'])) {
124
- $defaultOrderConfig = OrderConfig::default();
125
- if ($defaultOrderConfig) {
126
- $input['order_config_uuid'] = $defaultOrderConfig->uuid;
127
- }
128
- }
129
-
130
134
  // Ensure orchestrator_priority is never null — the column is NOT NULL
131
135
  // and the DB default is bypassed when Eloquent receives an explicit null.
132
136
  if (!isset($input['orchestrator_priority']) || !is_numeric($input['orchestrator_priority'])) {
@@ -216,6 +220,33 @@ class OrderController extends FleetOpsController
216
220
  }
217
221
  }
218
222
 
223
+ /**
224
+ * Resolve the concrete polymorphic customer type from the submitted UUID.
225
+ */
226
+ protected function normalizeCustomerType(array &$input): void
227
+ {
228
+ $customerUuid = data_get($input, 'customer_uuid') ?? data_get($input, 'customer.uuid');
229
+ if (!$customerUuid) {
230
+ return;
231
+ }
232
+
233
+ $customer = Utils::getUuid(
234
+ ['contacts', 'vendors'],
235
+ [
236
+ 'uuid' => $customerUuid,
237
+ 'company_uuid' => session('company'),
238
+ ],
239
+ [
240
+ 'with_table' => true,
241
+ ]
242
+ );
243
+
244
+ if (is_array($customer)) {
245
+ $input['customer_uuid'] = Utils::get($customer, 'uuid');
246
+ $input['customer_type'] = Utils::getModelClassName(Utils::get($customer, 'table'));
247
+ }
248
+ }
249
+
219
250
  /**
220
251
  * Route which enables editing of an order route.
221
252
  *
@@ -225,10 +256,14 @@ class OrderController extends FleetOpsController
225
256
  */
226
257
  public function editOrderRoute(string $id, Request $request)
227
258
  {
228
- $pickup = $request->input('pickup');
229
- $dropoff = $request->input('dropoff');
230
- $return = $request->input('return');
231
- $waypoints = $request->array('waypoints', []);
259
+ $pickup = $request->input('pickup');
260
+ $dropoff = $request->input('dropoff');
261
+ $return = $request->input('return');
262
+ $waypoints = $request->array('waypoints', []);
263
+ $hasPickupInput = $request->exists('pickup');
264
+ $hasDropoffInput = $request->exists('dropoff');
265
+ $hasReturnInput = $request->exists('return');
266
+ $hasWaypointsInput = $request->exists('waypoints');
232
267
 
233
268
  // Get the order
234
269
  $order = Order::where('uuid', $id)->with(['payload'])->first();
@@ -236,30 +271,51 @@ class OrderController extends FleetOpsController
236
271
  return response()->error('Unable to find order to update route for.');
237
272
  }
238
273
 
239
- // Handle update of multiple waypoints
240
- if ($waypoints) {
241
- $order->payload->updateWaypoints($waypoints);
242
- $order->payload->removePlace(['pickup', 'dropoff', 'return'], ['save' => true]);
243
- } else {
244
- // Update pickup
274
+ if ($hasPickupInput) {
245
275
  if ($pickup) {
246
276
  $order->payload->setPickup($pickup, ['save' => true]);
277
+ } else {
278
+ $order->payload->removePlace('pickup', ['save' => true]);
247
279
  }
280
+ }
248
281
 
249
- // Update dropoff
282
+ if ($hasDropoffInput) {
250
283
  if ($dropoff) {
251
284
  $order->payload->setDropoff($dropoff, ['save' => true]);
285
+ } else {
286
+ $order->payload->removePlace('dropoff', ['save' => true]);
252
287
  }
288
+ }
253
289
 
254
- // Update return
290
+ if ($hasReturnInput) {
255
291
  if ($return) {
256
- $order->payload->setDropoff($return, ['save' => true]);
292
+ $order->payload->setReturn($return, ['save' => true]);
293
+ } else {
294
+ $order->payload->removePlace('return', ['save' => true]);
257
295
  }
296
+ }
258
297
 
259
- // Remove waypoints if any
298
+ if ($hasWaypointsInput) {
299
+ if (!empty($waypoints)) {
300
+ $order->payload->updateWaypoints($waypoints);
301
+ } else {
302
+ $order->payload->removeWaypoints();
303
+ }
304
+ } elseif ($hasPickupInput || $hasDropoffInput || $hasReturnInput) {
260
305
  $order->payload->removeWaypoints();
261
306
  }
262
307
 
308
+ $startingDestination = $order->payload->getPickupOrFirstWaypoint();
309
+ if (!$startingDestination) {
310
+ $startingDestination = $order->payload->getDropoffOrLastWaypoint();
311
+ }
312
+
313
+ if ($startingDestination instanceof Place || $startingDestination instanceof Waypoint) {
314
+ $order->payload->setCurrentWaypoint($startingDestination);
315
+ }
316
+
317
+ $order->load(['payload.pickup', 'payload.dropoff', 'payload.return', 'payload.waypoints']);
318
+
263
319
  return ['order' => new $this->resource($order)];
264
320
  }
265
321
 
@@ -531,14 +587,10 @@ class OrderController extends FleetOpsController
531
587
  return response()->error('No order found to dispatch.');
532
588
  }
533
589
 
534
- // if order has no config set, set default config
535
- $order->loadMissing('orderConfig');
536
- if (!$order->orderConfig) {
537
- $defaultOrderConfig = OrderConfig::default();
538
- if ($defaultOrderConfig) {
539
- $order->update(['order_config_uuid' => $defaultOrderConfig->uuid]);
540
- $order->loadMissing('orderConfig');
541
- }
590
+ // Ensure the order is normalized onto a real order config before
591
+ // dispatching activities.
592
+ if (!$order->ensureOrderConfig()) {
593
+ return response()->error('No order config found for dispatch.');
542
594
  }
543
595
 
544
596
  if (!$order->hasDriverAssigned && !$order->adhoc) {
@@ -711,7 +763,11 @@ class OrderController extends FleetOpsController
711
763
  }
712
764
 
713
765
  $waypoint = $request->filled('waypoint') ? Waypoint::findByPlace($request->input('waypoint'), $order) : null;
714
- $activities = $order->config()->nextActivity($waypoint);
766
+ $orderConfig = $order->ensureOrderConfig();
767
+ if (!$orderConfig) {
768
+ return response()->error('No order config found for order.');
769
+ }
770
+ $activities = $orderConfig->nextActivity($waypoint);
715
771
 
716
772
  // If activity is to complete order add proof of delivery properties if required
717
773
  // This is a temporary fix until activity is updated to handle POD on it's own
@@ -227,6 +227,7 @@ class ServiceQuoteController extends FleetOpsController
227
227
 
228
228
  // should all be Place like
229
229
  $waypoints = collect([$pickup, ...$waypoints, $dropoff])->filter();
230
+ $endpointCount = (int) ($pickup instanceof Place) + (int) ($dropoff instanceof Place);
230
231
 
231
232
  // if facilitator is an integrated partner resolve service quotes from bridge
232
233
  if ($facilitator && Str::startsWith($facilitator, 'integrated_vendor')) {
@@ -274,7 +275,7 @@ class ServiceQuoteController extends FleetOpsController
274
275
  $serviceQuotes = collect();
275
276
 
276
277
  if ($serviceRate) {
277
- [$subTotal, $lines] = $serviceRate->quoteFromPreliminaryData($entities, $waypoints, $totalDistance, $totalTime, $isCashOnDelivery);
278
+ [$subTotal, $lines] = $serviceRate->quoteFromPreliminaryData($entities, $waypoints, $totalDistance, $totalTime, $isCashOnDelivery, $endpointCount);
278
279
 
279
280
  $quote = ServiceQuote::create([
280
281
  'request_id' => $requestId,
@@ -322,7 +323,7 @@ class ServiceQuoteController extends FleetOpsController
322
323
 
323
324
  // calculate quotes
324
325
  foreach ($serviceRates as $serviceRate) {
325
- [$subTotal, $lines] = $serviceRate->quoteFromPreliminaryData($entities, $waypoints, $totalDistance, $totalTime, $isCashOnDelivery);
326
+ [$subTotal, $lines] = $serviceRate->quoteFromPreliminaryData($entities, $waypoints, $totalDistance, $totalTime, $isCashOnDelivery, $endpointCount);
326
327
 
327
328
  $quote = ServiceQuote::create([
328
329
  'request_id' => $requestId,
@@ -184,9 +184,17 @@ class SettingController extends Controller
184
184
  */
185
185
  public function saveRoutingSettings(Request $request)
186
186
  {
187
- $router = $request->input('router');
188
- $unit = $request->input('unit', 'km');
189
- Setting::configureCompany('routing', ['router' => $router, 'unit' => $unit]);
187
+ $displayEngine = $request->input('display_engine', $request->input('router', 'osrm'));
188
+ $optimizationEngine = $request->input('optimization_engine', $displayEngine);
189
+ $unit = $request->input('unit', 'km');
190
+ Setting::configureCompany('routing', [
191
+ 'router' => $displayEngine,
192
+ 'display_engine' => $displayEngine,
193
+ 'optimization_engine' => $optimizationEngine,
194
+ 'routing_display_engine' => $displayEngine,
195
+ 'routing_optimization_engine' => $optimizationEngine,
196
+ 'unit' => $unit,
197
+ ]);
190
198
 
191
199
  return response()->json([
192
200
  'status' => 'ok',
@@ -203,6 +211,14 @@ class SettingController extends Controller
203
211
  {
204
212
  $routingSettings = Setting::lookupCompany('routing', ['router' => 'osrm', 'unit' => 'km']);
205
213
 
214
+ $displayEngine = data_get($routingSettings, 'display_engine', data_get($routingSettings, 'routing_display_engine', data_get($routingSettings, 'router', 'osrm')));
215
+ $optimizationEngine = data_get($routingSettings, 'optimization_engine', data_get($routingSettings, 'routing_optimization_engine', $displayEngine));
216
+ $routingSettings['router'] = $displayEngine;
217
+ $routingSettings['display_engine'] = $displayEngine;
218
+ $routingSettings['optimization_engine'] = $optimizationEngine;
219
+ $routingSettings['routing_display_engine'] = $displayEngine;
220
+ $routingSettings['routing_optimization_engine'] = $optimizationEngine;
221
+
206
222
  // always default to km if no unit is set
207
223
  if (!isset($routingSettings['unit'])) {
208
224
  $routingSettings['unit'] = 'km';
@@ -211,6 +227,93 @@ class SettingController extends Controller
211
227
  return response()->json($routingSettings);
212
228
  }
213
229
 
230
+ /**
231
+ * Retrieve and return the map provider settings for the current company.
232
+ *
233
+ * The Google Maps API key is sourced exclusively from the system-level
234
+ * services configuration managed by the core-api admin settings panel
235
+ * (services.google_maps.api_key). FleetOps never stores or manages the
236
+ * key independently — it simply reads it from the shared system config
237
+ * and passes it to the frontend so the Google Maps adapter can initialise.
238
+ *
239
+ * @return \Illuminate\Http\JsonResponse
240
+ */
241
+ public function getMapSettings()
242
+ {
243
+ $defaults = [
244
+ 'mapProvider' => 'leaflet',
245
+ ];
246
+
247
+ $systemMapSettings = Setting::lookup('fleet-ops.map-settings', []);
248
+ $mapSettings = Setting::lookupFromCompany('fleet-ops.map-settings', $defaults);
249
+ $mapSettings['mapProvider'] = data_get($mapSettings, 'mapProvider') ?: data_get($systemMapSettings, 'mapProvider', 'leaflet');
250
+
251
+ // Source the Google Maps API key from the system-level services config
252
+ // that is managed by the core-api admin settings panel. This ensures a
253
+ // single source of truth and avoids duplicating key management.
254
+ $mapSettings['googleMapsApiKey'] = config('services.google_maps.api_key', env('GOOGLE_MAPS_API_KEY', ''));
255
+ $mapSettings['googleMapsMapId'] = data_get($systemMapSettings, 'googleMapsMapId', '');
256
+
257
+ return response()->json($mapSettings);
258
+ }
259
+
260
+ /**
261
+ * Persist the map provider settings for the current company.
262
+ *
263
+ * The Google Maps API key is managed entirely through the core-api admin
264
+ * settings panel (Settings → Services → Google Maps) and is therefore
265
+ * never accepted or stored by this endpoint. Only the provider selection
266
+ * and display preferences are persisted here.
267
+ *
268
+ * @return \Illuminate\Http\JsonResponse
269
+ */
270
+ public function saveMapSettings(Request $request)
271
+ {
272
+ $settings = $request->input('settings', []);
273
+
274
+ // The API key is managed at the system level via core-api — strip it
275
+ // from the payload in case a client accidentally sends it.
276
+ unset($settings['googleMapsApiKey']);
277
+
278
+ // Validate provider value
279
+ $allowedProviders = ['leaflet', 'google'];
280
+ if (isset($settings['mapProvider']) && !in_array($settings['mapProvider'], $allowedProviders)) {
281
+ $settings['mapProvider'] = 'leaflet';
282
+ }
283
+
284
+ Setting::configureCompany('fleet-ops.map-settings', $settings);
285
+
286
+ return response()->json($this->getMapSettings()->getData(true));
287
+ }
288
+
289
+ public function getAdminMapSettings()
290
+ {
291
+ $defaults = [
292
+ 'mapProvider' => 'leaflet',
293
+ 'googleMapsMapId' => '',
294
+ ];
295
+
296
+ return response()->json(Setting::lookup('fleet-ops.map-settings', $defaults));
297
+ }
298
+
299
+ public function saveAdminMapSettings(Request $request)
300
+ {
301
+ $allowedProviders = ['leaflet', 'google'];
302
+ $mapProvider = $request->input('mapProvider', 'leaflet');
303
+ if (!in_array($mapProvider, $allowedProviders)) {
304
+ $mapProvider = 'leaflet';
305
+ }
306
+
307
+ $settings = [
308
+ 'mapProvider' => $mapProvider,
309
+ 'googleMapsMapId' => (string) $request->input('googleMapsMapId', ''),
310
+ ];
311
+
312
+ Setting::configure('fleet-ops.map-settings', $settings);
313
+
314
+ return response()->json($this->getAdminMapSettings()->getData(true));
315
+ }
316
+
214
317
  /**
215
318
  * Retrieve driver scheduling settings for the current company.
216
319
  *
@@ -44,10 +44,11 @@ class OrderFilter extends Filter
44
44
  // eager load main relationships to reduce N+1 overhead
45
45
  $this->builder->with([
46
46
  'payload.entities',
47
- 'payload.waypoints',
48
47
  'payload.pickup',
49
48
  'payload.dropoff',
50
49
  'payload.return',
50
+ 'payload.firstWaypointMarker.place',
51
+ 'payload.lastWaypointMarker.place',
51
52
  'trackingNumber',
52
53
  'trackingStatuses',
53
54
  'driverAssigned' => function ($query) {
@@ -19,6 +19,8 @@ class Payload extends FleetbaseResource
19
19
  public function toArray($request): array
20
20
  {
21
21
  $isInternal = Http::isInternalRequest();
22
+ $pickup = $this->index_pickup_place;
23
+ $dropoff = $this->index_dropoff_place;
22
24
 
23
25
  return [
24
26
  'id' => $this->when($isInternal, $this->id, $this->public_id),
@@ -30,24 +32,24 @@ class Payload extends FleetbaseResource
30
32
  'return_uuid' => $this->when($isInternal, $this->return_uuid),
31
33
 
32
34
  // Minimal pickup - only what's displayed in the table
33
- 'pickup' => $this->whenLoaded('pickup', function () {
34
- return new Place($this->pickup);
35
+ 'pickup' => $this->when($pickup, function () use ($pickup) {
36
+ return new Place($pickup);
35
37
  }),
36
38
 
37
39
  // Minimal dropoff - only what's displayed in the table
38
- 'dropoff' => $this->whenLoaded('dropoff', function () {
39
- return new Place($this->dropoff);
40
+ 'dropoff' => $this->when($dropoff, function () use ($dropoff) {
41
+ return new Place($dropoff);
40
42
  }),
41
43
 
42
44
  // Entity count instead of full entities
43
45
  'entities_count' => $this->whenLoaded('entities', function () {
44
46
  return $this->entities->count();
45
- }),
47
+ }, $this->entities()->count()),
46
48
 
47
- // Waypoint count instead of full waypoints
49
+ // Waypoint count for lightweight route-shape summaries.
48
50
  'waypoints_count' => $this->whenLoaded('waypoints', function () {
49
51
  return $this->waypoints->count();
50
- }),
52
+ }, $this->waypoints()->count()),
51
53
 
52
54
  'type' => $this->type,
53
55
  'created_at' => $this->created_at,