@fleetbase/fleetops-engine 0.6.49 → 0.6.50

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 (142) hide show
  1. package/addon/components/customer/create-order-form.hbs +2 -2
  2. package/addon/components/customer/form.hbs +2 -2
  3. package/addon/components/customer/order-form.hbs +2 -2
  4. package/addon/components/customer/orders.hbs +2 -2
  5. package/addon/components/display-place.hbs +2 -2
  6. package/addon/components/map/google-live-map.js +6 -2
  7. package/addon/components/map/leaflet-live-map.hbs +48 -14
  8. package/addon/components/map/leaflet-live-map.js +184 -20
  9. package/addon/components/modals/order-change-destination.hbs +46 -0
  10. package/addon/components/modals/order-change-destination.js +106 -0
  11. package/addon/components/modals/stop-activity.hbs +28 -0
  12. package/addon/components/modals/stop-activity.js +52 -0
  13. package/addon/components/modals/update-order-activity.hbs +45 -3
  14. package/addon/components/modals/update-order-activity.js +143 -5
  15. package/addon/components/orchestrator/resource-panel.hbs +0 -10
  16. package/addon/components/orchestrator/resource-panel.js +2 -4
  17. package/addon/components/order/activity-list.hbs +2 -2
  18. package/addon/components/order/activity-timeline.hbs +31 -29
  19. package/addon/components/order/details/activity.hbs +3 -3
  20. package/addon/components/order/details/activity.js +11 -0
  21. package/addon/components/order/details/documents.js +2 -0
  22. package/addon/components/order/details/purchase-rate.hbs +2 -2
  23. package/addon/components/order/details/route.hbs +11 -3
  24. package/addon/components/order/details/route.js +91 -14
  25. package/addon/components/order/details/tracking.hbs +101 -72
  26. package/addon/components/order/details/tracking.js +149 -2
  27. package/addon/components/order/form/details.hbs +3 -3
  28. package/addon/components/order/form/documents.js +11 -4
  29. package/addon/components/order/form/route.hbs +108 -104
  30. package/addon/components/order/form/route.js +37 -1
  31. package/addon/components/order/form/service-rate.hbs +2 -2
  32. package/addon/components/order-config-manager/activity-flow.hbs +4 -1
  33. package/addon/components/order-config-manager/activity-flow.js +178 -12
  34. package/addon/components/place/address.js +1 -1
  35. package/addon/components/route-list.hbs +17 -17
  36. package/addon/components/route-list.js +42 -10
  37. package/addon/components/service-rate/details.hbs +41 -0
  38. package/addon/components/service-rate/form.hbs +118 -0
  39. package/addon/components/service-rate/form.js +55 -0
  40. package/addon/components/vehicle/form.js +1 -1
  41. package/addon/components/vehicle/pill.hbs +1 -1
  42. package/addon/controllers/management/vehicles/index/new.js +1 -1
  43. package/addon/controllers/operations/orders/index/details.js +4 -4
  44. package/addon/controllers/operations/orders/index.js +14 -0
  45. package/addon/helpers/place-address-html.js +2 -2
  46. package/addon/routes/operations/service-rates/index.js +1 -1
  47. package/addon/services/geofence.js +163 -19
  48. package/addon/services/leaflet-contextmenu-manager.js +27 -0
  49. package/addon/services/vehicle-actions.js +1 -1
  50. package/addon/styles/fleetops-engine.css +91 -1
  51. package/addon/utils/fleet-ops-options.js +2 -1
  52. package/addon/utils/normalize-order-config-flow.js +41 -0
  53. package/addon/utils/place-address-html.js +3 -2
  54. package/addon/utils/route-visualization.js +54 -7
  55. package/app/components/modals/order-change-destination.js +1 -0
  56. package/app/components/modals/stop-activity.js +1 -0
  57. package/composer.json +1 -1
  58. package/extension.json +1 -1
  59. package/package.json +3 -3
  60. package/server/migrations/2026_05_21_000001_add_geographic_rules_to_service_rate_fees_table.php +37 -0
  61. package/server/migrations/2026_05_25_000001_add_endpoint_tracking_refs_to_payloads_table.php +50 -0
  62. package/server/migrations/2026_05_27_000001_default_driver_vehicle_statuses_to_available.php +48 -0
  63. package/server/resources/views/labels/default.php +57 -49
  64. package/server/resources/views/labels/waypoint-label.php +27 -20
  65. package/server/seeders/Testing/ConnectivitySeeder.php +1 -0
  66. package/server/seeders/Testing/FleetSeeder.php +22 -22
  67. package/server/seeders/Testing/OrdersSeeder.php +6 -22
  68. package/server/seeders/Testing/VroomOrchestrationSeeder.php +21 -21
  69. package/server/src/Console/Commands/AuditCustomerUserConflicts.php +75 -0
  70. package/server/src/Exceptions/CustomerUserConflictException.php +13 -0
  71. package/server/src/Http/Controllers/Api/v1/ContactController.php +17 -2
  72. package/server/src/Http/Controllers/Api/v1/CustomerController.php +876 -0
  73. package/server/src/Http/Controllers/Api/v1/DriverController.php +1 -1
  74. package/server/src/Http/Controllers/Api/v1/OrderConfigController.php +54 -0
  75. package/server/src/Http/Controllers/Api/v1/OrderController.php +98 -70
  76. package/server/src/Http/Controllers/Api/v1/ServiceAreaController.php +2 -2
  77. package/server/src/Http/Controllers/Api/v1/VehicleController.php +20 -3
  78. package/server/src/Http/Controllers/Internal/v1/ContactController.php +19 -0
  79. package/server/src/Http/Controllers/Internal/v1/LiveController.php +101 -66
  80. package/server/src/Http/Controllers/Internal/v1/OrderController.php +429 -26
  81. package/server/src/Http/Controllers/Internal/v1/ServiceQuoteController.php +1 -1
  82. package/server/src/Http/Controllers/Internal/v1/SettingController.php +2 -2
  83. package/server/src/Http/Controllers/Internal/v1/VehicleController.php +54 -1
  84. package/server/src/Http/Middleware/AuthenticateCustomerToken.php +34 -0
  85. package/server/src/Http/Requests/CreateCustomerOrderRequest.php +63 -0
  86. package/server/src/Http/Requests/CreateCustomerRequest.php +48 -0
  87. package/server/src/Http/Requests/CreateDriverRequest.php +1 -1
  88. package/server/src/Http/Requests/CreateVehicleRequest.php +25 -1
  89. package/server/src/Http/Requests/Internal/CreateDriverRequest.php +1 -1
  90. package/server/src/Http/Requests/VerifyCreateCustomerRequest.php +29 -0
  91. package/server/src/Http/Resources/Internal/v1/OrderConfig.php +41 -0
  92. package/server/src/Http/Resources/v1/Customer.php +79 -0
  93. package/server/src/Http/Resources/v1/OrderConfig.php +74 -0
  94. package/server/src/Http/Resources/v1/Payload.php +23 -20
  95. package/server/src/Http/Resources/v1/ServiceArea.php +33 -19
  96. package/server/src/Http/Resources/v1/ServiceRateFee.php +7 -0
  97. package/server/src/Listeners/HandleOrderDispatched.php +2 -2
  98. package/server/src/Models/Contact.php +92 -15
  99. package/server/src/Models/Customer.php +64 -0
  100. package/server/src/Models/Driver.php +12 -3
  101. package/server/src/Models/Entity.php +2 -2
  102. package/server/src/Models/Order.php +29 -4
  103. package/server/src/Models/Payload.php +17 -1
  104. package/server/src/Models/ServiceRate.php +273 -1
  105. package/server/src/Models/ServiceRateFee.php +39 -9
  106. package/server/src/Models/TrackingNumber.php +8 -2
  107. package/server/src/Models/Vehicle.php +18 -2
  108. package/server/src/Models/Waypoint.php +2 -2
  109. package/server/src/Notifications/LateDeparture.php +2 -2
  110. package/server/src/Notifications/ProlongedStoppage.php +2 -2
  111. package/server/src/Notifications/RouteDeviation.php +2 -2
  112. package/server/src/Observers/ContactObserver.php +2 -0
  113. package/server/src/Observers/ServiceRateObserver.php +6 -6
  114. package/server/src/Orchestration/Engines/VroomOrchestrationEngine.php +1 -1
  115. package/server/src/Providers/FleetOpsServiceProvider.php +1 -0
  116. package/server/src/Support/Analytics/AbstractAnalytics.php +6 -6
  117. package/server/src/Support/Analytics/FuelEfficiency.php +2 -2
  118. package/server/src/Support/Analytics/OrdersByStatus.php +1 -1
  119. package/server/src/Support/Analytics/RevenueTrend.php +0 -1
  120. package/server/src/Support/Analytics/TopDrivers.php +1 -1
  121. package/server/src/Support/CustomerAuth.php +98 -0
  122. package/server/src/Support/LabelPdf.php +52 -0
  123. package/server/src/Support/Metrics/AbstractMetric.php +4 -4
  124. package/server/src/Support/ResolvesOrderServiceStops.php +512 -0
  125. package/server/src/Tracking/TrackingContextBuilder.php +41 -38
  126. package/server/src/Tracking/TrackingIntelligenceService.php +109 -9
  127. package/server/src/Tracking/TrackingStop.php +13 -11
  128. package/server/src/routes.php +43 -9
  129. package/server/tests/CapacityAllocationEngineTest.php +10 -10
  130. package/server/tests/CustomerContactIdentitySafetyTest.php +54 -0
  131. package/server/tests/CustomerEndpointTest.php +199 -0
  132. package/server/tests/DriverVehicleStatusDefaultTest.php +39 -0
  133. package/server/tests/LabelPdfRenderingTest.php +23 -0
  134. package/server/tests/LiveControllerViewportTest.php +60 -0
  135. package/server/tests/MetricsRegistryTest.php +4 -8
  136. package/server/tests/OrchestrationPayloadBuilderTest.php +2 -2
  137. package/server/tests/OrderConfigEndpointTest.php +86 -0
  138. package/server/tests/OrderFileAttachmentTest.php +20 -0
  139. package/server/tests/ServiceAreaZoneApiShapeTest.php +34 -0
  140. package/server/tests/VehicleDriverAssignmentTest.php +18 -0
  141. package/translations/en-us.yaml +1 -0
  142. package/server/migrations/2026_05_13_000001_repair_customer_contact_user_types.php +0 -86
@@ -9,6 +9,7 @@ import Service, { inject as service } from '@ember/service';
9
9
  import { action } from '@ember/object';
10
10
  import { debug } from '@ember/debug';
11
11
  import { dasherize } from '@ember/string';
12
+ import { later } from '@ember/runloop';
12
13
  import toMultiPolygon from '../utils/to-multi-polygon';
13
14
 
14
15
  export default class GeofenceService extends Service {
@@ -173,9 +174,11 @@ export default class GeofenceService extends Service {
173
174
  },
174
175
  {
175
176
  callback: (serviceArea) => {
176
- this.#resetDraftSession({ hideControl: true, removeDraft: true });
177
- this.#showServiceArea(serviceArea);
177
+ this.#finishDraftSave((draftLayer) => {
178
+ this.#reloadThenFocusServiceArea(serviceArea, { draftLayer });
179
+ });
178
180
  },
181
+ refresh: false,
179
182
  }
180
183
  );
181
184
  return;
@@ -184,7 +187,7 @@ export default class GeofenceService extends Service {
184
187
  if (this._draftSession.mode === 'zone') {
185
188
  const serviceArea = this._draftSession.serviceArea;
186
189
  this.zoneActions.modal.create(
187
- { border: geoJson, service_area_uuid: serviceArea.id },
190
+ { border: geoJson, service_area: serviceArea },
188
191
  {
189
192
  decline: (modal) => {
190
193
  this.#resetDraftSession({ hideControl: true, removeDraft: true });
@@ -193,10 +196,11 @@ export default class GeofenceService extends Service {
193
196
  },
194
197
  {
195
198
  callback: (zone) => {
196
- serviceArea?.zones?.pushObject?.(zone);
197
- this.#resetDraftSession({ hideControl: true, removeDraft: true });
198
- this.#showZone(zone);
199
+ this.#finishDraftSave((draftLayer) => {
200
+ this.#reloadThenFocusZone(serviceArea, zone, { draftLayer });
201
+ });
199
202
  },
203
+ refresh: false,
200
204
  }
201
205
  );
202
206
  return;
@@ -246,19 +250,19 @@ export default class GeofenceService extends Service {
246
250
  } else {
247
251
  record.setProperties({
248
252
  border: geoJson,
249
- service_area_uuid: selectedServiceArea.id,
250
- serviceArea: selectedServiceArea,
253
+ service_area: selectedServiceArea,
251
254
  });
252
255
  }
253
256
 
254
257
  return record.save().then((savedRecord) => {
255
- this.#resetDraftSession({ hideControl: true, removeDraft: true });
256
-
257
258
  if (selectedLayerType === 'Service Area') {
258
- this.#showServiceArea(savedRecord);
259
+ this.#finishDraftSave((draftLayer) => {
260
+ this.#reloadThenFocusServiceArea(savedRecord, { draftLayer });
261
+ });
259
262
  } else {
260
- selectedServiceArea?.zones?.pushObject?.(savedRecord);
261
- this.#showZone(savedRecord);
263
+ this.#finishDraftSave((draftLayer) => {
264
+ this.#reloadThenFocusZone(selectedServiceArea, savedRecord, { draftLayer });
265
+ });
262
266
  }
263
267
 
264
268
  return savedRecord;
@@ -289,28 +293,168 @@ export default class GeofenceService extends Service {
289
293
  this._draftSession = null;
290
294
  }
291
295
 
296
+ #finishDraftSave(callback) {
297
+ const draftLayer = this._draftSession?.draftLayer ?? null;
298
+ callback?.(draftLayer);
299
+ this.#resetDraftSession({ hideControl: true, removeDraft: false });
300
+ }
301
+
292
302
  async #editPolygonLayer(originalLayer, { focusBounds = null } = {}) {
293
303
  return this.mapManager.editPolygon(originalLayer, { focusBounds });
294
304
  }
295
305
 
296
306
  #showZone(zone, { pin = false } = {}) {
297
- const layer = this.mapManager.getOverlay(zone.id) ?? zone?.leafletLayer ?? null;
307
+ const overlay = this.mapManager.getOverlay(zone.id);
308
+ const layer = overlay ?? zone?.leafletLayer ?? null;
298
309
  this.#setLayerPinned(layer, pin);
299
- this.mapManager.showPolygon(zone.id);
310
+ if (overlay) {
311
+ this.mapManager.showPolygon(zone.id);
312
+ } else if (layer) {
313
+ this.mapManager.showLayer?.(layer);
314
+ }
315
+ }
316
+
317
+ #includeServiceArea(serviceArea) {
318
+ if (!serviceArea?.id) return;
319
+
320
+ const currentServiceAreas = this.serviceAreaActions.serviceAreas;
321
+ if (!currentServiceAreas) {
322
+ this.serviceAreaActions.serviceAreas = [serviceArea];
323
+ return;
324
+ }
325
+
326
+ const serviceAreas = Array.from(currentServiceAreas);
327
+ const exists = serviceAreas.some((existingServiceArea) => existingServiceArea?.id === serviceArea.id);
328
+ if (exists) return;
329
+
330
+ this.serviceAreaActions.serviceAreas = [...serviceAreas, serviceArea];
331
+ }
332
+
333
+ #includeZone(serviceArea, zone) {
334
+ if (!serviceArea?.id || !zone?.id) return;
335
+
336
+ const zones = serviceArea.zones;
337
+ if (!zones) {
338
+ serviceArea.set?.('zones', [zone]);
339
+ this.serviceAreaActions.serviceAreas = Array.from(this.serviceAreaActions.serviceAreas ?? []);
340
+ return;
341
+ }
342
+
343
+ const exists = Array.from(zones).some((existingZone) => existingZone?.id === zone.id);
344
+ if (exists) return;
345
+
346
+ if (typeof zones.pushObject === 'function') {
347
+ zones.pushObject(zone);
348
+ } else if (typeof zones.addObject === 'function') {
349
+ zones.addObject(zone);
350
+ } else if (typeof zones.push === 'function') {
351
+ zones.push(zone);
352
+ }
353
+
354
+ this.serviceAreaActions.serviceAreas = Array.from(this.serviceAreaActions.serviceAreas ?? []);
355
+ }
356
+
357
+ #reloadThenFocusServiceArea(serviceArea, options = {}) {
358
+ this.#reloadServiceAreas()
359
+ .then((serviceAreas) => {
360
+ const canonicalServiceArea = this.#findResource(serviceAreas, serviceArea) ?? serviceArea;
361
+ this.#focusServiceAreaAfterCreate(canonicalServiceArea, options);
362
+ })
363
+ .catch(() => {
364
+ this.#includeServiceArea(serviceArea);
365
+ this.#focusServiceAreaAfterCreate(serviceArea, options);
366
+ });
367
+ }
368
+
369
+ #reloadThenFocusZone(serviceArea, zone, options = {}) {
370
+ this.#reloadServiceAreas()
371
+ .then((serviceAreas) => {
372
+ const canonicalServiceArea = this.#findResource(serviceAreas, serviceArea) ?? serviceArea;
373
+ const canonicalZone = this.#findResource(canonicalServiceArea?.zones, zone) ?? zone;
374
+ this.#focusZoneAfterCreate(canonicalZone, options);
375
+ })
376
+ .catch(() => {
377
+ this.#includeZone(serviceArea, zone);
378
+ this.#focusZoneAfterCreate(zone, options);
379
+ });
380
+ }
381
+
382
+ #reloadServiceAreas() {
383
+ return Promise.resolve(this.serviceAreaActions.loadAll.perform()).then(() => this.serviceAreaActions.serviceAreas ?? []);
384
+ }
385
+
386
+ #findResource(resources, resource) {
387
+ if (!resource || !resources) return null;
388
+
389
+ const resourceId = resource.id ?? resource.public_id;
390
+ const publicId = resource.public_id ?? resource.id;
391
+
392
+ return (
393
+ Array.from(resources).find((candidate) => {
394
+ return candidate?.id === resourceId || candidate?.id === publicId || candidate?.public_id === publicId || candidate?.public_id === resourceId;
395
+ }) ?? null
396
+ );
397
+ }
398
+
399
+ #focusServiceAreaAfterCreate(serviceArea, options = {}) {
400
+ this.#focusWhenReady([serviceArea], () => this.focusServiceArea(serviceArea), options);
401
+ }
402
+
403
+ #focusZoneAfterCreate(zone, options = {}) {
404
+ this.#focusWhenReady([zone], () => this.focusZone(zone), options);
405
+ }
406
+
407
+ #focusWhenReady(models, focus, { attempts = 60, delay = 100, draftLayer = null } = {}) {
408
+ const overlays = models
409
+ .map((model) => {
410
+ const overlay = this.mapManager.getOverlay(model?.id);
411
+ if (overlay) {
412
+ return overlay;
413
+ }
414
+
415
+ return model?.leafletLayer === draftLayer ? null : model?.leafletLayer;
416
+ })
417
+ .filter(Boolean);
418
+ const allRegistered = overlays.length === models.length;
419
+
420
+ if (allRegistered || attempts <= 0) {
421
+ focus();
422
+ if (draftLayer && !draftLayer.__removedAfterCreate && !overlays.includes(draftLayer)) {
423
+ draftLayer.__removedAfterCreate = true;
424
+ this.mapManager.removeLayer(draftLayer);
425
+ }
426
+ return;
427
+ }
428
+
429
+ later(this, () => this.#focusWhenReady(models, focus, { attempts: attempts - 1, delay, draftLayer }), delay);
300
430
  }
301
431
 
302
432
  #showServiceArea(serviceArea, { pin = false } = {}) {
303
433
  [serviceArea, ...(serviceArea.zones ?? [])].forEach((model) => {
304
- const layer = this.mapManager.getOverlay(model.id) ?? model?.leafletLayer ?? null;
434
+ const overlay = this.mapManager.getOverlay(model.id);
435
+ const layer = overlay ?? model?.leafletLayer ?? null;
305
436
  this.#setLayerPinned(layer, pin);
306
- this.mapManager.showPolygon(model.id);
437
+ if (overlay) {
438
+ this.mapManager.showPolygon(model.id);
439
+ } else if (layer) {
440
+ this.mapManager.showLayer?.(layer);
441
+ }
307
442
  });
308
443
  }
309
444
 
310
445
  #hideServiceArea(serviceArea) {
311
446
  [serviceArea, ...(serviceArea.zones ?? [])].forEach((model) => {
312
- this.#setLayerPinned(model?.leafletLayer ?? this.mapManager.getOverlay(model.id), false);
313
- this.mapManager.hidePolygon(model.id);
447
+ const overlay = this.mapManager.getOverlay(model.id);
448
+ const layer = model?.leafletLayer;
449
+
450
+ this.#setLayerPinned(layer ?? overlay, false);
451
+ if (overlay) {
452
+ this.mapManager.hidePolygon(model.id);
453
+ }
454
+
455
+ if (layer && layer !== overlay) {
456
+ this.mapManager.hideLayer?.(layer);
457
+ }
314
458
  });
315
459
  }
316
460
 
@@ -85,6 +85,33 @@ export default class LeafletContextmenuManagerService extends Service {
85
85
  return null;
86
86
  }
87
87
 
88
+ /**
89
+ * Remove a registered context menu and detach it from its layer when possible.
90
+ *
91
+ * @param {string} registryName - The public context menu registry name.
92
+ * @returns {Object|null} The removed registry, or null when none existed.
93
+ */
94
+ removeContextMenu(registryName) {
95
+ const internalRegistryName = this.createInternalRegistryName(registryName);
96
+ const registry = this.contextMenuRegistry[internalRegistryName];
97
+
98
+ if (!registry) {
99
+ return null;
100
+ }
101
+
102
+ try {
103
+ registry.contextmenuApi?.removeAllItems?.();
104
+ registry.layer?.unbindContextMenu?.();
105
+ } catch (error) {
106
+ // Context menus are disposable UI state; stale layer cleanup should not block record deletion.
107
+ }
108
+
109
+ delete this.contextMenuRegistry[internalRegistryName];
110
+ this.contextMenuRegistry = { ...this.contextMenuRegistry };
111
+
112
+ return registry;
113
+ }
114
+
88
115
  /**
89
116
  * Creates an internal registry name by camelizing the provided registry name and appending "ConextmenuRegistry" to it.
90
117
  *
@@ -62,7 +62,7 @@ export default class VehicleActionsService extends ResourceActionService {
62
62
 
63
63
  constructor() {
64
64
  super(...arguments);
65
- this.initialize('vehicle', { status: 'active' });
65
+ this.initialize('vehicle', { status: 'available' });
66
66
  }
67
67
 
68
68
  transition = {
@@ -345,6 +345,51 @@ body.fleetbase-console .next-content-overlay > .next-content-overlay-panel-conta
345
345
  gap: 0.5rem;
346
346
  }
347
347
 
348
+ .fleetops-order-form-waypoint {
349
+ position: relative;
350
+ }
351
+
352
+ .fleetops-order-form-waypoint--required {
353
+ border-color: #f97316 !important;
354
+ padding-top: 1.25rem;
355
+ box-shadow:
356
+ 0 1px 3px 0 rgb(249 115 22 / 15%),
357
+ 0 0 0 1px rgb(249 115 22 / 10%);
358
+ }
359
+
360
+ .fleetops-order-form-waypoint__required-tab {
361
+ position: absolute;
362
+ top: -1px;
363
+ right: -1px;
364
+ border-top-right-radius: 0.375rem;
365
+ border-bottom-left-radius: 0.375rem;
366
+ background-color: #f97316;
367
+ color: #fff7ed;
368
+ padding: 0.15rem 0.5rem;
369
+ font-size: 0.62rem;
370
+ font-weight: 700;
371
+ line-height: 1.2;
372
+ text-transform: uppercase;
373
+ letter-spacing: 0.02em;
374
+ }
375
+
376
+ .fleetops-order-form-waypoint__marker {
377
+ display: flex;
378
+ flex-direction: row;
379
+ align-items: center;
380
+ gap: 0.6rem;
381
+ min-width: 3.25rem;
382
+ padding-top: 0.15rem;
383
+ }
384
+
385
+ .fleetops-order-form-waypoint__badge {
386
+ min-width: 1.65rem;
387
+ width: 1.65rem;
388
+ height: 1.65rem;
389
+ font-size: 0.75rem;
390
+ flex-shrink: 0;
391
+ }
392
+
348
393
  .fleetops-route-editor-empty-state {
349
394
  border: 1px dashed rgb(75 85 99 / 75%);
350
395
  border-radius: 0.5rem;
@@ -464,7 +509,52 @@ body[data-theme='light'] .flow-activity-status-pill text {
464
509
  .activity-flow-graph-container {
465
510
  position: relative;
466
511
  width: 100%;
467
- height: 100%;
512
+ height: calc(100vh - 16rem);
513
+ min-height: 24rem;
514
+ overflow: auto hidden;
515
+ scrollbar-width: none;
516
+ }
517
+
518
+ .activity-flow-graph-container > .joint-js-paper {
519
+ min-width: 100%;
520
+ max-width: none;
521
+ overflow: visible;
522
+ }
523
+
524
+ .activity-flow-graph-container::-webkit-scrollbar {
525
+ display: none;
526
+ }
527
+
528
+ .activity-flow-horizontal-scrollbar {
529
+ position: fixed;
530
+ bottom: 0;
531
+ z-index: 100;
532
+ height: 12px;
533
+ overflow: auto hidden;
534
+ background-color: transparent;
535
+ scrollbar-color: #6b7280 transparent;
536
+ scrollbar-width: thin;
537
+ }
538
+
539
+ .activity-flow-horizontal-scrollbar.is-hidden {
540
+ display: none;
541
+ }
542
+
543
+ .activity-flow-horizontal-scrollbar-content {
544
+ height: 1px;
545
+ }
546
+
547
+ .activity-flow-horizontal-scrollbar::-webkit-scrollbar {
548
+ height: 8px;
549
+ }
550
+
551
+ .activity-flow-horizontal-scrollbar::-webkit-scrollbar-thumb {
552
+ background-color: #6b7280;
553
+ border-radius: 9999px;
554
+ }
555
+
556
+ .activity-flow-horizontal-scrollbar::-webkit-scrollbar-track {
557
+ background-color: transparent;
468
558
  }
469
559
 
470
560
  .activity-flow-zoom-controls {
@@ -15,7 +15,7 @@ export const driverTypes = [
15
15
  ];
16
16
 
17
17
  export const driverStatuses = [
18
- { label: 'Active', value: 'active', description: 'Driver is available for assignments' },
18
+ { label: 'Available', value: 'available', description: 'Driver is available for assignments' },
19
19
  { label: 'Inactive', value: 'inactive', description: 'Driver is not currently active' },
20
20
  { label: 'On Duty', value: 'on_duty', description: 'Driver is working and available' },
21
21
  { label: 'Off Duty', value: 'off_duty', description: 'Driver is not working' },
@@ -382,6 +382,7 @@ export const podOptions = [
382
382
  export const serviceRateCalculationMethods = [
383
383
  { label: 'Per Meter', value: 'per_meter', description: 'Calculate cost based on the number of meters traveled.' },
384
384
  { label: 'Fixed Rate', value: 'fixed_meter', description: 'Charge a fixed rate per meter regardless of distance.' },
385
+ { label: 'Multi-zone Distance', value: 'multi_zone_distance', description: 'Calculate cost by splitting route distance across priced service areas and zones.' },
385
386
  { label: 'Parcel Rate', value: 'parcel', description: 'Charge a dynamic rate based on parcel/payload weight and dimensions.' },
386
387
  { label: 'Per Drop-off', value: 'per_drop', description: 'Cost applied per drop-off point.' },
387
388
  { label: 'Algorithm', value: 'algo', description: 'Cost calculated dynamically using algorithmic logic.' },
@@ -0,0 +1,41 @@
1
+ import { isArray } from '@ember/array';
2
+
3
+ export function getOrderConfigFlowRootCode(flow = {}) {
4
+ if (flow.created) {
5
+ return 'created';
6
+ }
7
+
8
+ return Object.keys(flow)[0];
9
+ }
10
+
11
+ export default function normalizeOrderConfigFlow(flow = {}) {
12
+ if (isArray(flow)) {
13
+ return flow.reduce((normalized, activity, index) => {
14
+ if (!activity || typeof activity !== 'object') {
15
+ return normalized;
16
+ }
17
+
18
+ const code = activity.code ?? activity.key;
19
+ if (!code) {
20
+ return normalized;
21
+ }
22
+
23
+ const nextActivity = flow.slice(index + 1).find((candidate) => candidate && typeof candidate === 'object' && (candidate.code ?? candidate.key));
24
+
25
+ normalized[code] = {
26
+ ...activity,
27
+ code,
28
+ key: activity.key ?? code,
29
+ activities: isArray(activity.activities) ? activity.activities : nextActivity ? [nextActivity.code ?? nextActivity.key] : [],
30
+ };
31
+
32
+ return normalized;
33
+ }, {});
34
+ }
35
+
36
+ if (flow && typeof flow === 'object') {
37
+ return flow;
38
+ }
39
+
40
+ return {};
41
+ }
@@ -11,17 +11,18 @@ function line(content, className = '') {
11
11
  return `<div${classAttribute}>${escapeHtml(content)}</div>`;
12
12
  }
13
13
 
14
- export default function placeAddressHtml(place) {
14
+ export default function placeAddressHtml(place, options = {}) {
15
15
  if (!place) {
16
16
  return htmlSafe('');
17
17
  }
18
18
 
19
+ const { showTitle = true } = options;
19
20
  const name = place.name === place.street1 ? null : place.name;
20
21
  const cityStatePostalCode = [place.city, place.province, place.postal_code].filter((value) => !isBlank(value)).join(', ');
21
22
  const lines = [];
22
23
 
23
24
  if (name) {
24
- lines.push(line(name, 'font-semibold'));
25
+ if (showTitle) lines.push(line(name, 'font-semibold'));
25
26
  lines.push(line(place.street1));
26
27
  } else if (place.street1) {
27
28
  lines.push(line(place.street1, 'font-semibold'));
@@ -8,6 +8,18 @@ export function waypointToPlace(waypoint) {
8
8
  return waypoint.place ?? waypoint;
9
9
  }
10
10
 
11
+ export function endpointTrackingNumberUuid(payload, role) {
12
+ if (!payload || !role) {
13
+ return null;
14
+ }
15
+
16
+ return payload[`${role}_tracking_number_uuid`] ?? null;
17
+ }
18
+
19
+ export function waypointTrackingNumberUuid(waypoint) {
20
+ return waypoint?.tracking_number_uuid ?? waypoint?.tracking_number?.uuid ?? waypoint?.trackingNumber?.uuid ?? null;
21
+ }
22
+
11
23
  export function buildPlaceAddressTooltip(title, place) {
12
24
  if (!place) {
13
25
  return title;
@@ -38,30 +50,65 @@ export function buildRoutePointsFromPayload(payload) {
38
50
  const waypoints = payload.waypoints ?? [];
39
51
 
40
52
  if (hasValidCoordinates(payload.pickup)) {
41
- points.push({ role: 'pickup', place: payload.pickup });
53
+ points.push({ role: 'pickup', place: payload.pickup, trackingNumberUuid: endpointTrackingNumberUuid(payload, 'pickup') });
42
54
  }
43
55
 
44
56
  waypoints.forEach((waypoint, index) => {
45
57
  const place = waypointToPlace(waypoint);
46
58
 
47
59
  if (hasValidCoordinates(place)) {
48
- points.push({ role: 'waypoint', place, stopNumber: index + 1 });
60
+ points.push({ role: 'waypoint', place, stopNumber: index + 1, trackingNumberUuid: waypointTrackingNumberUuid(waypoint) });
49
61
  }
50
62
  });
51
63
 
52
64
  if (hasValidCoordinates(payload.dropoff)) {
53
- points.push({ role: 'dropoff', place: payload.dropoff });
65
+ points.push({ role: 'dropoff', place: payload.dropoff, trackingNumberUuid: endpointTrackingNumberUuid(payload, 'dropoff') });
66
+ }
67
+
68
+ if (hasValidCoordinates(payload.return)) {
69
+ points.push({ role: 'return', place: payload.return, trackingNumberUuid: endpointTrackingNumberUuid(payload, 'return') });
54
70
  }
55
71
 
56
72
  return points;
57
73
  }
58
74
 
75
+ export function buildServiceStopsFromPayload(payload) {
76
+ if (!payload) {
77
+ return [];
78
+ }
79
+
80
+ const stops = [];
81
+ const waypoints = payload.waypoints ?? [];
82
+
83
+ if (payload.pickup) {
84
+ stops.push({ role: 'pickup', place: payload.pickup, trackingNumberUuid: endpointTrackingNumberUuid(payload, 'pickup') });
85
+ }
86
+
87
+ waypoints.forEach((waypoint, index) => {
88
+ const place = waypointToPlace(waypoint);
89
+
90
+ if (place) {
91
+ stops.push({ role: 'waypoint', place, stopNumber: index + 1, trackingNumberUuid: waypointTrackingNumberUuid(waypoint) });
92
+ }
93
+ });
94
+
95
+ if (payload.dropoff) {
96
+ stops.push({ role: 'dropoff', place: payload.dropoff, trackingNumberUuid: endpointTrackingNumberUuid(payload, 'dropoff') });
97
+ }
98
+
99
+ if (payload.return) {
100
+ stops.push({ role: 'return', place: payload.return, trackingNumberUuid: endpointTrackingNumberUuid(payload, 'return') });
101
+ }
102
+
103
+ return stops;
104
+ }
105
+
59
106
  export function describeRoutePoint(routePoint, routeColor) {
60
107
  const role = routePoint?.role;
61
108
  const stopNumber = routePoint?.stopNumber ?? 1;
62
- const label = role === 'pickup' ? 'P' : role === 'dropoff' ? 'D' : String(stopNumber);
63
- const markerColor = role === 'pickup' ? '#22C55E' : role === 'dropoff' ? '#EF4444' : routeColor;
64
- const title = role === 'pickup' ? 'Pickup' : role === 'dropoff' ? 'Dropoff' : `Stop ${stopNumber}`;
109
+ const label = role === 'pickup' ? 'P' : role === 'dropoff' ? 'D' : role === 'return' ? 'R' : String(stopNumber);
110
+ const markerColor = role === 'pickup' ? '#22C55E' : role === 'dropoff' ? '#EF4444' : role === 'return' ? '#F97316' : routeColor;
111
+ const title = role === 'pickup' ? 'Pickup' : role === 'dropoff' ? 'Dropoff' : role === 'return' ? 'Return' : `Stop ${stopNumber}`;
65
112
 
66
113
  return {
67
114
  label,
@@ -74,7 +121,7 @@ export function buildRoutePointMarkerPresentation(routePoint, routeColor, option
74
121
  const role = routePoint?.role;
75
122
  const place = routePoint?.place;
76
123
  const { label, markerColor, title } = describeRoutePoint(routePoint, routeColor);
77
- const zIndexOffset = role === 'pickup' ? 2200 : role === 'dropoff' ? 2100 : 1800;
124
+ const zIndexOffset = role === 'pickup' ? 2200 : role === 'dropoff' ? 2100 : role === 'return' ? 2000 : 1800;
78
125
 
79
126
  return {
80
127
  waypointLabel: label,
@@ -0,0 +1 @@
1
+ export { default } from '@fleetbase/fleetops-engine/components/modals/order-change-destination';
@@ -0,0 +1 @@
1
+ export { default } from '@fleetbase/fleetops-engine/components/modals/stop-activity';
package/composer.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fleetbase/fleetops-api",
3
- "version": "0.6.49",
3
+ "version": "0.6.50",
4
4
  "description": "Fleet & Transport Management Extension for Fleetbase",
5
5
  "keywords": [
6
6
  "fleetbase-extension",
package/extension.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "Fleet-Ops",
3
- "version": "0.6.49",
3
+ "version": "0.6.50",
4
4
  "description": "Fleet & Transport Management Extension for Fleetbase",
5
5
  "repository": "https://github.com/fleetbase/fleetops",
6
6
  "license": "AGPL-3.0-or-later",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fleetbase/fleetops-engine",
3
- "version": "0.6.49",
3
+ "version": "0.6.50",
4
4
  "description": "Fleet & Transport Management Extension for Fleetbase",
5
5
  "fleetbase": {
6
6
  "route": "fleet-ops"
@@ -43,8 +43,8 @@
43
43
  "dependencies": {
44
44
  "@babel/core": "^7.23.2",
45
45
  "@fleetbase/ember-core": "^0.3.19",
46
- "@fleetbase/ember-ui": "^0.3.30",
47
- "@fleetbase/fleetops-data": "^0.1.33",
46
+ "@fleetbase/ember-ui": "^0.3.32",
47
+ "@fleetbase/fleetops-data": "^0.1.34",
48
48
  "@fleetbase/leaflet-routing-machine": "^3.2.17",
49
49
  "@fortawesome/ember-fontawesome": "^2.0.0",
50
50
  "@fortawesome/fontawesome-svg-core": "6.4.0",
@@ -0,0 +1,37 @@
1
+ <?php
2
+
3
+ use Illuminate\Database\Migrations\Migration;
4
+ use Illuminate\Database\Schema\Blueprint;
5
+ use Illuminate\Support\Facades\Schema;
6
+
7
+ return new class extends Migration {
8
+ /**
9
+ * Run the migrations.
10
+ *
11
+ * @return void
12
+ */
13
+ public function up()
14
+ {
15
+ Schema::table('service_rate_fees', function (Blueprint $table) {
16
+ $table->uuid('service_area_uuid')->nullable()->index()->after('service_rate_uuid');
17
+ $table->uuid('zone_uuid')->nullable()->index()->after('service_area_uuid');
18
+ $table->string('label')->nullable()->after('zone_uuid');
19
+ $table->integer('priority')->default(0)->after('label');
20
+ $table->boolean('is_fallback')->default(false)->after('priority');
21
+ });
22
+ }
23
+
24
+ /**
25
+ * Reverse the migrations.
26
+ *
27
+ * @return void
28
+ */
29
+ public function down()
30
+ {
31
+ Schema::table('service_rate_fees', function (Blueprint $table) {
32
+ $table->dropIndex(['service_area_uuid']);
33
+ $table->dropIndex(['zone_uuid']);
34
+ $table->dropColumn(['service_area_uuid', 'zone_uuid', 'label', 'priority', 'is_fallback']);
35
+ });
36
+ }
37
+ };