@fleetbase/fleetops-engine 0.6.49 → 0.6.51

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 (271) hide show
  1. package/README.md +185 -67
  2. package/addon/components/contact/panel-header.hbs +11 -48
  3. package/addon/components/customer/create-order-form.hbs +2 -2
  4. package/addon/components/customer/details.hbs +1 -2
  5. package/addon/components/customer/form.hbs +62 -50
  6. package/addon/components/customer/form.js +27 -1
  7. package/addon/components/customer/order-form.hbs +2 -2
  8. package/addon/components/customer/orders.hbs +50 -2
  9. package/addon/components/customer/orders.js +67 -5
  10. package/addon/components/display-place.hbs +2 -2
  11. package/addon/components/driver/form.hbs +1 -1
  12. package/addon/components/home/getting-started-guidance.hbs +113 -0
  13. package/addon/components/home/getting-started-guidance.js +107 -0
  14. package/addon/components/issue/details/comments.hbs +3 -0
  15. package/addon/components/issue/details/comments.js +3 -0
  16. package/addon/components/issue/details/correspondence.hbs +9 -0
  17. package/addon/components/issue/details/correspondence.js +83 -0
  18. package/addon/components/issue/details/documents.hbs +69 -0
  19. package/addon/components/issue/details/documents.js +85 -0
  20. package/addon/components/issue/details.hbs +220 -59
  21. package/addon/components/issue/details.js +245 -0
  22. package/addon/components/issue/form.hbs +19 -1
  23. package/addon/components/issue/panel-header.hbs +43 -0
  24. package/addon/components/issue/panel-header.js +22 -0
  25. package/addon/components/issue/timeline.hbs +40 -0
  26. package/addon/components/issue/timeline.js +45 -0
  27. package/addon/components/leaflet-tracking-marker.js +3 -0
  28. package/addon/components/map/drawer/position-listing.js +3 -0
  29. package/addon/components/map/google-live-map.js +6 -2
  30. package/addon/components/map/leaflet-live-map.hbs +48 -14
  31. package/addon/components/map/leaflet-live-map.js +201 -20
  32. package/addon/components/map/order-list-overlay.hbs +4 -4
  33. package/addon/components/modals/convert-customer-to-vendor.hbs +30 -0
  34. package/addon/components/modals/convert-customer-to-vendor.js +55 -0
  35. package/addon/components/modals/issue-assign.hbs +20 -0
  36. package/addon/components/modals/issue-assign.js +49 -0
  37. package/addon/components/modals/issue-close.hbs +30 -0
  38. package/addon/components/modals/issue-close.js +73 -0
  39. package/addon/components/modals/issue-status.hbs +20 -0
  40. package/addon/components/modals/issue-status.js +43 -0
  41. package/addon/components/modals/order-change-destination.hbs +46 -0
  42. package/addon/components/modals/order-change-destination.js +106 -0
  43. package/addon/components/modals/reset-customer-credentials.js +12 -6
  44. package/addon/components/modals/stop-activity.hbs +28 -0
  45. package/addon/components/modals/stop-activity.js +52 -0
  46. package/addon/components/modals/update-order-activity.hbs +78 -2
  47. package/addon/components/modals/update-order-activity.js +197 -5
  48. package/addon/components/orchestrator/resource-panel.hbs +0 -10
  49. package/addon/components/orchestrator/resource-panel.js +2 -4
  50. package/addon/components/order/activity-list.hbs +2 -2
  51. package/addon/components/order/activity-timeline.hbs +31 -29
  52. package/addon/components/order/details/activity.hbs +3 -3
  53. package/addon/components/order/details/activity.js +13 -2
  54. package/addon/components/order/details/documents.js +2 -0
  55. package/addon/components/order/details/proof.hbs +40 -38
  56. package/addon/components/order/details/proof.js +31 -4
  57. package/addon/components/order/details/purchase-rate.hbs +2 -2
  58. package/addon/components/order/details/route.hbs +11 -3
  59. package/addon/components/order/details/route.js +91 -14
  60. package/addon/components/order/details/tracking.hbs +101 -72
  61. package/addon/components/order/details/tracking.js +149 -2
  62. package/addon/components/order/details.hbs +5 -5
  63. package/addon/components/order/details.js +19 -1
  64. package/addon/components/order/form/details.hbs +3 -3
  65. package/addon/components/order/form/documents.js +11 -4
  66. package/addon/components/order/form/route.hbs +108 -104
  67. package/addon/components/order/form/route.js +37 -1
  68. package/addon/components/order/form/service-rate.hbs +2 -2
  69. package/addon/components/order/header.hbs +1 -1
  70. package/addon/components/order-config-manager/activity-flow.hbs +4 -1
  71. package/addon/components/order-config-manager/activity-flow.js +178 -12
  72. package/addon/components/place/address.js +1 -1
  73. package/addon/components/route-list.hbs +17 -29
  74. package/addon/components/route-list.js +42 -10
  75. package/addon/components/service-rate/details.hbs +42 -1
  76. package/addon/components/service-rate/form.hbs +119 -1
  77. package/addon/components/service-rate/form.js +59 -0
  78. package/addon/components/vehicle/form.js +1 -1
  79. package/addon/components/vehicle/pill.hbs +1 -1
  80. package/addon/components/vendor/personnel-manager.hbs +166 -0
  81. package/addon/components/vendor/personnel-manager.js +143 -0
  82. package/addon/components/widget/live-fleet.hbs +1 -0
  83. package/addon/components/widget/live-fleet.js +2 -7
  84. package/addon/components/widget/revenue-trend.hbs +1 -1
  85. package/addon/components/widget/revenue-trend.js +75 -2
  86. package/addon/controllers/management/contacts/customers/details.js +31 -13
  87. package/addon/controllers/management/contacts/customers.js +16 -5
  88. package/addon/controllers/management/contacts/index/details.js +31 -13
  89. package/addon/controllers/management/contacts/index.js +7 -0
  90. package/addon/controllers/management/issues/index/details.js +52 -6
  91. package/addon/controllers/management/vehicles/index/new.js +1 -1
  92. package/addon/controllers/management/vendors/index/details.js +4 -0
  93. package/addon/controllers/operations/orders/index/details.js +15 -7
  94. package/addon/controllers/operations/orders/index.js +15 -1
  95. package/addon/extension.js +19 -12
  96. package/addon/helpers/format-duration.js +1 -38
  97. package/addon/helpers/place-address-html.js +2 -2
  98. package/addon/initializers/load-leaflet-assets.js +5 -6
  99. package/addon/initializers/patch-ember-leaflet-tooltip-layer.js +51 -0
  100. package/addon/routes/management/issues/index/details.js +1 -1
  101. package/addon/routes/operations/service-rates/index.js +1 -1
  102. package/addon/routes.js +1 -0
  103. package/addon/services/contact-actions.js +231 -1
  104. package/addon/services/geofence.js +163 -19
  105. package/addon/services/getting-started.js +44 -0
  106. package/addon/services/issue-actions.js +71 -0
  107. package/addon/services/leaflet-contextmenu-manager.js +27 -0
  108. package/addon/services/leaflet-map-manager.js +2 -0
  109. package/addon/services/map-adapter/leaflet.js +19 -0
  110. package/addon/services/service-areas.js +3 -0
  111. package/addon/services/vehicle-actions.js +1 -1
  112. package/addon/styles/fleetops-engine.css +1127 -1
  113. package/addon/templates/management/contacts/customers/details.hbs +1 -1
  114. package/addon/templates/management/contacts/index/details.hbs +1 -1
  115. package/addon/templates/management/contacts.hbs +2 -2
  116. package/addon/templates/management/issues/index/details/index.hbs +3 -1
  117. package/addon/templates/management/issues/index/details.hbs +3 -2
  118. package/addon/templates/management/vendors/index/details/personnel.hbs +1 -2
  119. package/addon/templates/operations/orders/index/details/index.hbs +2 -2
  120. package/addon/templates/operations/orders/index.hbs +1 -1
  121. package/addon/templates/operations/service-rates/index/edit.hbs +1 -0
  122. package/addon/templates/operations/service-rates/index/new.hbs +1 -0
  123. package/addon/templates/settings/payments/onboard.hbs +8 -8
  124. package/addon/utils/fleet-ops-options.js +2 -1
  125. package/addon/utils/leaflet-draw-namespace-guard.js +22 -0
  126. package/addon/utils/leaflet-plugin-loader.js +219 -0
  127. package/addon/utils/normalize-order-config-flow.js +41 -0
  128. package/addon/utils/place-address-html.js +3 -2
  129. package/addon/utils/route-visualization.js +54 -7
  130. package/addon/utils/setup-customer-portal.js +4 -64
  131. package/app/components/home/getting-started-guidance.js +1 -0
  132. package/app/components/issue/details/comments.js +1 -0
  133. package/app/components/issue/details/correspondence.js +1 -0
  134. package/app/components/issue/details/documents.js +1 -0
  135. package/app/components/issue/panel-header.js +1 -0
  136. package/app/components/modals/convert-customer-to-vendor.js +1 -0
  137. package/app/components/modals/issue-assign.js +1 -0
  138. package/app/components/modals/issue-close.js +1 -0
  139. package/app/components/modals/issue-status.js +1 -0
  140. package/app/components/modals/order-change-destination.js +1 -0
  141. package/app/components/modals/stop-activity.js +1 -0
  142. package/app/components/vendor/personnel-manager.js +1 -0
  143. package/app/helpers/format-duration.js +1 -1
  144. package/app/services/getting-started.js +1 -0
  145. package/app/utils/leaflet-draw-namespace-guard.js +1 -0
  146. package/app/utils/leaflet-plugin-loader.js +1 -0
  147. package/composer.json +1 -1
  148. package/extension.json +1 -1
  149. package/package.json +5 -5
  150. package/server/migrations/2026_05_21_000001_add_geographic_rules_to_service_rate_fees_table.php +37 -0
  151. package/server/migrations/2026_05_25_000001_add_endpoint_tracking_refs_to_payloads_table.php +50 -0
  152. package/server/migrations/2026_05_27_000001_default_driver_vehicle_statuses_to_available.php +48 -0
  153. package/server/migrations/2026_05_31_000001_add_portal_fields_to_vendor_personnels_table.php +30 -0
  154. package/server/migrations/2026_06_03_000000_ensure_tags_column_on_issues_table.php +31 -0
  155. package/server/migrations/2026_06_03_000001_add_order_uuid_to_issues_table.php +25 -0
  156. package/server/migrations/2026_06_04_000001_backfill_order_transaction_subjects.php +39 -0
  157. package/server/migrations/2026_06_04_000002_backfill_order_transaction_directions.php +24 -0
  158. package/server/resources/views/labels/default.php +57 -49
  159. package/server/resources/views/labels/waypoint-label.php +27 -20
  160. package/server/resources/views/mail/customer-credentials.blade.php +16 -13
  161. package/server/seeders/Testing/ConnectivitySeeder.php +1 -0
  162. package/server/seeders/Testing/FleetSeeder.php +22 -22
  163. package/server/seeders/Testing/OrdersSeeder.php +6 -22
  164. package/server/seeders/Testing/VroomOrchestrationSeeder.php +21 -21
  165. package/server/src/Console/Commands/AuditCustomerUserConflicts.php +75 -0
  166. package/server/src/Exceptions/CustomerUserConflictException.php +13 -0
  167. package/server/src/Expansions/UserFilterExpansion.php +28 -0
  168. package/server/src/Http/Controllers/Api/v1/ContactController.php +17 -2
  169. package/server/src/Http/Controllers/Api/v1/CustomerController.php +876 -0
  170. package/server/src/Http/Controllers/Api/v1/DriverController.php +1 -1
  171. package/server/src/Http/Controllers/Api/v1/IssueController.php +2 -0
  172. package/server/src/Http/Controllers/Api/v1/OrderConfigController.php +54 -0
  173. package/server/src/Http/Controllers/Api/v1/OrderController.php +109 -80
  174. package/server/src/Http/Controllers/Api/v1/PlaceController.php +10 -58
  175. package/server/src/Http/Controllers/Api/v1/ServiceAreaController.php +2 -2
  176. package/server/src/Http/Controllers/Api/v1/VehicleController.php +20 -3
  177. package/server/src/Http/Controllers/Internal/v1/ContactController.php +154 -0
  178. package/server/src/Http/Controllers/Internal/v1/CustomerController.php +116 -6
  179. package/server/src/Http/Controllers/Internal/v1/GettingStartedController.php +17 -0
  180. package/server/src/Http/Controllers/Internal/v1/IssueController.php +251 -0
  181. package/server/src/Http/Controllers/Internal/v1/LiveController.php +101 -66
  182. package/server/src/Http/Controllers/Internal/v1/OrderController.php +530 -52
  183. package/server/src/Http/Controllers/Internal/v1/PlaceController.php +10 -68
  184. package/server/src/Http/Controllers/Internal/v1/ServiceQuoteController.php +1 -1
  185. package/server/src/Http/Controllers/Internal/v1/SettingController.php +2 -2
  186. package/server/src/Http/Controllers/Internal/v1/VehicleController.php +54 -1
  187. package/server/src/Http/Controllers/Internal/v1/VendorController.php +89 -0
  188. package/server/src/Http/Middleware/AuthenticateCustomerToken.php +34 -0
  189. package/server/src/Http/Requests/CreateCustomerOrderRequest.php +63 -0
  190. package/server/src/Http/Requests/CreateCustomerRequest.php +48 -0
  191. package/server/src/Http/Requests/CreateDriverRequest.php +1 -1
  192. package/server/src/Http/Requests/CreateIssueRequest.php +4 -0
  193. package/server/src/Http/Requests/CreateVehicleRequest.php +25 -1
  194. package/server/src/Http/Requests/Internal/CreateDriverRequest.php +1 -1
  195. package/server/src/Http/Requests/UpdateIssueRequest.php +4 -0
  196. package/server/src/Http/Requests/VerifyCreateCustomerRequest.php +29 -0
  197. package/server/src/Http/Resources/Internal/v1/OrderConfig.php +41 -0
  198. package/server/src/Http/Resources/v1/Customer.php +79 -0
  199. package/server/src/Http/Resources/v1/Index/Order.php +12 -9
  200. package/server/src/Http/Resources/v1/Issue.php +25 -0
  201. package/server/src/Http/Resources/v1/Order.php +20 -18
  202. package/server/src/Http/Resources/v1/OrderConfig.php +74 -0
  203. package/server/src/Http/Resources/v1/Payload.php +23 -20
  204. package/server/src/Http/Resources/v1/ServiceArea.php +33 -19
  205. package/server/src/Http/Resources/v1/ServiceRateFee.php +7 -0
  206. package/server/src/Listeners/HandleOrderDispatched.php +2 -2
  207. package/server/src/Mail/CustomerCredentialsMail.php +5 -6
  208. package/server/src/Models/Contact.php +122 -66
  209. package/server/src/Models/Customer.php +64 -0
  210. package/server/src/Models/Driver.php +12 -3
  211. package/server/src/Models/Entity.php +2 -2
  212. package/server/src/Models/Issue.php +47 -6
  213. package/server/src/Models/Order.php +62 -10
  214. package/server/src/Models/Payload.php +17 -1
  215. package/server/src/Models/ServiceArea.php +1 -1
  216. package/server/src/Models/ServiceRate.php +273 -1
  217. package/server/src/Models/ServiceRateFee.php +39 -9
  218. package/server/src/Models/TrackingNumber.php +8 -2
  219. package/server/src/Models/Vehicle.php +18 -2
  220. package/server/src/Models/VendorPersonnel.php +10 -1
  221. package/server/src/Models/Waypoint.php +2 -2
  222. package/server/src/Models/Zone.php +1 -1
  223. package/server/src/Notifications/LateDeparture.php +2 -2
  224. package/server/src/Notifications/ProlongedStoppage.php +2 -2
  225. package/server/src/Notifications/RouteDeviation.php +2 -2
  226. package/server/src/Observers/ContactObserver.php +2 -0
  227. package/server/src/Observers/PurchaseRateObserver.php +20 -0
  228. package/server/src/Observers/ServiceRateObserver.php +6 -6
  229. package/server/src/Observers/ZoneObserver.php +56 -0
  230. package/server/src/Orchestration/Engines/VroomOrchestrationEngine.php +1 -1
  231. package/server/src/Providers/FleetOpsServiceProvider.php +2 -0
  232. package/server/src/Support/Analytics/AbstractAnalytics.php +6 -6
  233. package/server/src/Support/Analytics/FuelEfficiency.php +2 -2
  234. package/server/src/Support/Analytics/OrdersByStatus.php +1 -1
  235. package/server/src/Support/Analytics/RevenueTrend.php +0 -1
  236. package/server/src/Support/Analytics/TopDrivers.php +10 -2
  237. package/server/src/Support/CustomerAuth.php +98 -0
  238. package/server/src/Support/GettingStarted.php +144 -0
  239. package/server/src/Support/LabelPdf.php +52 -0
  240. package/server/src/Support/Metrics/AbstractMetric.php +4 -4
  241. package/server/src/Support/PlaceSearch.php +282 -0
  242. package/server/src/Support/ResolvesOrderServiceStops.php +522 -0
  243. package/server/src/Tracking/TrackingContextBuilder.php +41 -38
  244. package/server/src/Tracking/TrackingIntelligenceService.php +109 -9
  245. package/server/src/Tracking/TrackingStop.php +13 -11
  246. package/server/src/routes.php +76 -10
  247. package/server/tests/AnalyticsRoutesTest.php +8 -0
  248. package/server/tests/CapacityAllocationEngineTest.php +10 -10
  249. package/server/tests/CustomerContactIdentitySafetyTest.php +95 -0
  250. package/server/tests/CustomerEndpointTest.php +199 -0
  251. package/server/tests/DriverVehicleStatusDefaultTest.php +39 -0
  252. package/server/tests/GettingStartedStatusTest.php +21 -0
  253. package/server/tests/LabelPdfRenderingTest.php +23 -0
  254. package/server/tests/LiveControllerViewportTest.php +60 -0
  255. package/server/tests/MetricsRegistryTest.php +4 -8
  256. package/server/tests/OrchestrationPayloadBuilderTest.php +2 -2
  257. package/server/tests/OrderActivityFlowRegressionTest.php +116 -0
  258. package/server/tests/OrderConfigEndpointTest.php +86 -0
  259. package/server/tests/OrderFileAttachmentTest.php +20 -0
  260. package/server/tests/ServiceAreaZoneApiShapeTest.php +78 -0
  261. package/server/tests/VehicleDriverAssignmentTest.php +18 -0
  262. package/translations/ar-ae.yml +9 -0
  263. package/translations/bg-bg.yaml +9 -0
  264. package/translations/en-us.yaml +12 -0
  265. package/translations/es-pa.yaml +9 -0
  266. package/translations/fr-fr.yaml +9 -0
  267. package/translations/mn-mn.yaml +9 -0
  268. package/translations/pt-br.yaml +9 -0
  269. package/translations/ru-ru.yaml +9 -0
  270. package/translations/vi-vn.yaml +9 -0
  271. package/server/migrations/2026_05_13_000001_repair_customer_contact_user_types.php +0 -86
@@ -8,11 +8,15 @@ import { guidFor } from '@ember/object/internals';
8
8
  import { camelize, capitalize, dasherize } from '@ember/string';
9
9
  import { singularize, pluralize } from 'ember-inflector';
10
10
  import { all } from 'rsvp';
11
- import { task } from 'ember-concurrency';
11
+ import { task, timeout } from 'ember-concurrency';
12
12
  import { next } from '@ember/runloop';
13
13
  import getModelName from '@fleetbase/ember-core/utils/get-model-name';
14
+ import ensureLeafletPluginsReady, { hasLeafletPluginsReady } from '../../utils/leaflet-plugin-loader';
14
15
 
15
16
  export default class MapLeafletLiveMapComponent extends Component {
17
+ liveViewportReloadDebounceMs = 300;
18
+ liveViewportResourceLimit = 500;
19
+
16
20
  @service mapManager;
17
21
  @service mapSettings;
18
22
  @service leafletMapManager;
@@ -50,6 +54,7 @@ export default class MapLeafletLiveMapComponent extends Component {
50
54
  @tracked drivers = [];
51
55
  @tracked vehicles = [];
52
56
  @tracked places = [];
57
+ @tracked leafletPluginsReady = hasLeafletPluginsReady();
53
58
  _viewportReloadLocks = new Set();
54
59
 
55
60
  constructor() {
@@ -76,6 +81,10 @@ export default class MapLeafletLiveMapComponent extends Component {
76
81
  this._geofenceExitedHandler = this.#handleGeofenceExited.bind(this);
77
82
  this.universe.on('fleet-ops.geofence.entered', this._geofenceEnteredHandler);
78
83
  this.universe.on('fleet-ops.geofence.exited', this._geofenceExitedHandler);
84
+
85
+ if (!this.shouldUseGoogleMaps && !this.leafletPluginsReady) {
86
+ this.prepareLeafletPlugins();
87
+ }
79
88
  }
80
89
 
81
90
  willDestroy() {
@@ -98,6 +107,11 @@ export default class MapLeafletLiveMapComponent extends Component {
98
107
  this.universe.off('fleet-ops.geofence.exited', this._geofenceExitedHandler);
99
108
  this._geofenceExitedHandler = null;
100
109
  }
110
+ if (this._viewportReloadHandler && this.map && typeof this.map.off === 'function') {
111
+ this.map.off('moveend', this._viewportReloadHandler);
112
+ this.map.off('zoomend', this._viewportReloadHandler);
113
+ this._viewportReloadHandler = null;
114
+ }
101
115
  }
102
116
 
103
117
  @action didLoad({ target: map }) {
@@ -106,8 +120,9 @@ export default class MapLeafletLiveMapComponent extends Component {
106
120
  this.trigger('onLoad', ...arguments);
107
121
  this.load.perform();
108
122
  // Listen for map move/zoom events to trigger viewport-based resource reload
109
- map.on('moveend', () => this.reloadResourcesInViewport.perform());
110
- map.on('zoomend', () => this.reloadResourcesInViewport.perform());
123
+ this._viewportReloadHandler = () => this.reloadResourcesInViewport.perform();
124
+ map.on('moveend', this._viewportReloadHandler);
125
+ map.on('zoomend', this._viewportReloadHandler);
111
126
  }
112
127
 
113
128
  /**
@@ -211,13 +226,27 @@ export default class MapLeafletLiveMapComponent extends Component {
211
226
  this.#createZoneContextMenu(zone, layer);
212
227
  }
213
228
 
229
+ @action rebuildMapContextMenu() {
230
+ if (!this.map) {
231
+ return;
232
+ }
233
+
234
+ this.#createMapContextMenu(this.map);
235
+ this.#refreshResourceContextMenus();
236
+ this.#pruneResourceContextMenus();
237
+ }
238
+
239
+ @action syncServiceAreaContextMenus() {
240
+ this.rebuildMapContextMenu();
241
+ }
242
+
214
243
  /** load resources and wait for stuff here and trigger map ready **/
215
244
  @task *load() {
216
245
  try {
217
246
  // Get initial map bounds for spatial filtering (provider-agnostic)
218
247
  const mapBounds = this.mapManager.getBounds?.() ?? (this.map ? this.map.getBounds() : null);
219
248
  const bounds = this.#serializeBoundsForRequest(mapBounds);
220
- const params = bounds ? { bounds } : {};
249
+ const params = this.#liveViewportRequestParams(bounds);
221
250
 
222
251
  const data = yield all([
223
252
  this.loadResource.perform('routes'),
@@ -242,12 +271,19 @@ export default class MapLeafletLiveMapComponent extends Component {
242
271
  if (this.isViewportReloadSuspended) {
243
272
  return;
244
273
  }
274
+
275
+ yield timeout(this.liveViewportReloadDebounceMs);
276
+
277
+ if (this.isViewportReloadSuspended) {
278
+ return;
279
+ }
280
+
245
281
  // Get current map bounds (provider-agnostic)
246
282
  const rawBounds = this.mapManager.getBounds?.() ?? this.map?.getBounds();
247
283
  if (!rawBounds) return;
248
284
  const bounds = this.#serializeBoundsForRequest(rawBounds);
249
285
  if (!bounds) return;
250
- const params = { bounds };
286
+ const params = this.#liveViewportRequestParams(bounds);
251
287
 
252
288
  // Reload spatially-filtered resources (drivers, vehicles, places)
253
289
  // Orders, routes, and service-areas are not spatially filtered
@@ -299,6 +335,17 @@ export default class MapLeafletLiveMapComponent extends Component {
299
335
  return this.mapSettings.isGoogleMaps;
300
336
  }
301
337
 
338
+ async prepareLeafletPlugins() {
339
+ try {
340
+ await ensureLeafletPluginsReady();
341
+ if (!this.isDestroying && !this.isDestroyed) {
342
+ this.leafletPluginsReady = true;
343
+ }
344
+ } catch (error) {
345
+ debug('Failed to prepare Leaflet plugins: ' + error.message);
346
+ }
347
+ }
348
+
302
349
  /**
303
350
  * Get valid zoom level for map initialization
304
351
  * @returns {number} Valid zoom level between 1-20
@@ -445,6 +492,10 @@ export default class MapLeafletLiveMapComponent extends Component {
445
492
  return null;
446
493
  }
447
494
 
495
+ #liveViewportRequestParams(bounds) {
496
+ return bounds ? { bounds, limit: this.liveViewportResourceLimit } : { limit: this.liveViewportResourceLimit };
497
+ }
498
+
448
499
  #setResourceLayer(model, layer, options = {}) {
449
500
  const { hidden = false } = options;
450
501
  const type = getModelName(model);
@@ -462,6 +513,7 @@ export default class MapLeafletLiveMapComponent extends Component {
462
513
  }
463
514
 
464
515
  #createMapContextMenu(map) {
516
+ const serviceAreas = Array.from(this.serviceAreaActions.serviceAreas ?? []);
465
517
  const items = [
466
518
  {
467
519
  text: this.intl.t('live-map.show-coordinates'),
@@ -494,10 +546,10 @@ export default class MapLeafletLiveMapComponent extends Component {
494
546
  callback: () => this.geofence.createServiceArea(),
495
547
  index: 5,
496
548
  },
497
- this.serviceAreaActions.serviceAreas.length ? { separator: true } : null,
498
- ...this.serviceAreaActions.serviceAreas.map((serviceArea, i) => {
549
+ serviceAreas.length ? { separator: true } : null,
550
+ ...serviceAreas.map((serviceArea, i) => {
499
551
  return {
500
- text: this.intl.t('live-map.focus-service', { serviceName: serviceArea.name }),
552
+ text: this.intl.t('live-map.focus-service', { serviceName: this.#resourceDisplayName(serviceArea) }),
501
553
  callback: () => this.geofence.focusServiceArea(serviceArea),
502
554
  index: 6 + i,
503
555
  };
@@ -511,71 +563,200 @@ export default class MapLeafletLiveMapComponent extends Component {
511
563
  }
512
564
 
513
565
  #createZoneContextMenu(zone, layer) {
566
+ const zoneName = this.#resourceDisplayName(zone);
514
567
  let items = [
515
568
  {
516
569
  separator: true,
517
570
  },
518
571
  {
519
- text: this.intl.t('live-map.edit-zone', { zoneName: zone.name }),
572
+ text: this.intl.t('live-map.edit-zone', { zoneName }),
520
573
  callback: () => this.zoneActions.modal.edit(zone),
521
574
  },
522
575
  {
523
- text: this.intl.t('live-map.delete-zone', { zoneName: zone.name }),
524
- callback: () => this.zoneActions.delete(zone),
576
+ text: this.intl.t('live-map.delete-zone', { zoneName }),
577
+ callback: () => this.#deleteZone(zone),
525
578
  },
526
579
  ];
527
580
 
528
581
  if (!this.mapManager.isGoogleMaps) {
529
582
  items.splice(2, 0, {
530
- text: this.intl.t('live-map.edit-boundaries', { resource: zone.name }),
583
+ text: this.intl.t('live-map.edit-boundaries', { resource: zoneName }),
531
584
  callback: () => this.geofence.editZone(zone),
532
585
  });
533
586
  }
534
587
 
535
588
  // create contextmenu registry
536
- const contextmenuRegistry = this.leafletContextmenuManager.createContextMenu(`zone:${zone.public_id}`, layer, items, { zone });
589
+ const contextmenuRegistry = this.leafletContextmenuManager.createContextMenu(this.#resourceContextKey('zone', zone), layer, items, { zone });
537
590
  this.universe.trigger('fleet-ops:contextmenu:zone:created', contextmenuRegistry, this.leafletContextmenuManager);
538
591
 
539
592
  return contextmenuRegistry;
540
593
  }
541
594
 
542
595
  #createServiceAreaContextMenu(serviceArea, layer) {
596
+ const serviceName = this.#resourceDisplayName(serviceArea);
543
597
  let items = [
544
598
  {
545
599
  separator: true,
546
600
  },
547
601
  {
548
- text: this.intl.t('live-map.blur-service', { serviceName: serviceArea.name }),
602
+ text: this.intl.t('live-map.blur-service', { serviceName }),
549
603
  callback: () => this.geofence.blurServiceArea(serviceArea),
550
604
  },
551
605
  {
552
- text: this.intl.t('live-map.create-zone', { serviceName: serviceArea.name }),
606
+ text: this.intl.t('live-map.create-zone', { serviceName }),
553
607
  callback: () => this.geofence.createZone(serviceArea),
554
608
  },
555
609
  {
556
- text: this.intl.t('live-map.edit-service', { serviceName: serviceArea.name }),
610
+ text: this.intl.t('live-map.edit-service', { serviceName }),
557
611
  callback: () => this.serviceAreaActions.modal.edit(serviceArea),
558
612
  },
559
613
  {
560
- text: this.intl.t('live-map.delete-service', { serviceName: serviceArea.name }),
561
- callback: () => this.serviceAreaActions.delete(serviceArea),
614
+ text: this.intl.t('live-map.delete-service', { serviceName }),
615
+ callback: () => this.#deleteServiceArea(serviceArea),
562
616
  },
563
617
  ];
564
618
 
565
619
  if (!this.mapManager.isGoogleMaps) {
566
620
  items.splice(4, 0, {
567
- text: this.intl.t('live-map.edit-boundaries', { resource: serviceArea.name }),
621
+ text: this.intl.t('live-map.edit-boundaries', { resource: serviceName }),
568
622
  callback: () => this.geofence.editServiceArea(serviceArea),
569
623
  });
570
624
  }
571
625
 
572
626
  // create contextmenu registry
573
- const contextmenuRegistry = this.leafletContextmenuManager.createContextMenu(`service-area:${serviceArea.public_id}`, layer, items, { serviceArea });
627
+ const contextmenuRegistry = this.leafletContextmenuManager.createContextMenu(this.#resourceContextKey('service-area', serviceArea), layer, items, { serviceArea });
574
628
  this.universe.trigger('fleet-ops:contextmenu:service-area:created', contextmenuRegistry, this.leafletContextmenuManager);
575
629
 
576
630
  return contextmenuRegistry;
577
631
  }
578
632
 
633
+ #resourceContextKey(type, resource) {
634
+ return `${type}:${resource?.public_id ?? resource?.id}`;
635
+ }
636
+
637
+ #resourceDisplayName(resource) {
638
+ return resource?.get?.('name') || resource?.name || resource?.display_name || resource?.public_id || resource?.id || '';
639
+ }
640
+
641
+ #deleteServiceArea(serviceArea) {
642
+ const zones = Array.from(serviceArea?.zones ?? []);
643
+
644
+ return this.serviceAreaActions.delete(
645
+ serviceArea,
646
+ {},
647
+ {
648
+ callback: () => {
649
+ this.#removeServiceAreaFromMapState(serviceArea, zones);
650
+ this.rebuildMapContextMenu();
651
+ },
652
+ }
653
+ );
654
+ }
655
+
656
+ #deleteZone(zone) {
657
+ return this.zoneActions.delete(
658
+ zone,
659
+ {},
660
+ {
661
+ callback: () => {
662
+ this.#removeZoneFromMapState(zone);
663
+ this.rebuildMapContextMenu();
664
+ },
665
+ }
666
+ );
667
+ }
668
+
669
+ #removeServiceAreaFromMapState(serviceArea, zones = []) {
670
+ const serviceAreaId = serviceArea?.id;
671
+ const serviceAreaKey = this.#resourceContextKey('service-area', serviceArea);
672
+
673
+ zones.forEach((zone) => this.#removeZoneFromMapState(zone));
674
+ this.#removeOverlay(serviceArea);
675
+ this.leafletContextmenuManager.removeContextMenu(serviceAreaKey);
676
+
677
+ this.serviceAreaActions.serviceAreas = Array.from(this.serviceAreaActions.serviceAreas ?? []).filter((candidate) => candidate?.id !== serviceAreaId);
678
+ }
679
+
680
+ #removeZoneFromMapState(zone) {
681
+ const zoneId = zone?.id;
682
+ const zoneKey = this.#resourceContextKey('zone', zone);
683
+
684
+ this.#removeOverlay(zone);
685
+ this.leafletContextmenuManager.removeContextMenu(zoneKey);
686
+
687
+ for (const serviceArea of Array.from(this.serviceAreaActions.serviceAreas ?? [])) {
688
+ const zones = serviceArea?.zones;
689
+ if (!zones) {
690
+ continue;
691
+ }
692
+
693
+ const currentZones = Array.from(zones);
694
+ const nextZones = currentZones.filter((candidate) => candidate?.id !== zoneId);
695
+ if (nextZones.length !== currentZones.length) {
696
+ if (typeof zones.removeObject === 'function') {
697
+ zones.removeObject(zone);
698
+ } else {
699
+ serviceArea.set?.('zones', nextZones);
700
+ }
701
+ break;
702
+ }
703
+ }
704
+
705
+ this.serviceAreaActions.serviceAreas = Array.from(this.serviceAreaActions.serviceAreas ?? []);
706
+ }
707
+
708
+ #removeOverlay(resource) {
709
+ if (!resource?.id) {
710
+ return;
711
+ }
712
+
713
+ this.mapManager.removeOverlay?.(resource.id);
714
+
715
+ const layer = resource?.leafletLayer;
716
+ if (layer) {
717
+ this.mapManager.removeLayer?.(layer);
718
+ }
719
+ }
720
+
721
+ #refreshResourceContextMenus() {
722
+ for (const serviceArea of Array.from(this.serviceAreaActions.serviceAreas ?? [])) {
723
+ const serviceAreaLayer = this.mapManager.getOverlay(serviceArea?.id) ?? serviceArea?.leafletLayer;
724
+ if (serviceAreaLayer) {
725
+ this.#createServiceAreaContextMenu(serviceArea, serviceAreaLayer);
726
+ }
727
+
728
+ for (const zone of Array.from(serviceArea?.zones ?? [])) {
729
+ const zoneLayer = this.mapManager.getOverlay(zone?.id) ?? zone?.leafletLayer;
730
+ if (zoneLayer) {
731
+ this.#createZoneContextMenu(zone, zoneLayer);
732
+ }
733
+ }
734
+ }
735
+ }
736
+
737
+ #pruneResourceContextMenus() {
738
+ const activeServiceAreaKeys = new Set();
739
+ const activeZoneKeys = new Set();
740
+
741
+ for (const serviceArea of Array.from(this.serviceAreaActions.serviceAreas ?? [])) {
742
+ activeServiceAreaKeys.add(this.#resourceContextKey('service-area', serviceArea));
743
+
744
+ for (const zone of Array.from(serviceArea?.zones ?? [])) {
745
+ activeZoneKeys.add(this.#resourceContextKey('zone', zone));
746
+ }
747
+ }
748
+
749
+ for (const registry of Object.values(this.leafletContextmenuManager.contextMenuRegistry ?? {})) {
750
+ if (registry?.serviceArea && !activeServiceAreaKeys.has(this.#resourceContextKey('service-area', registry.serviceArea))) {
751
+ this.leafletContextmenuManager.removeContextMenu(this.#resourceContextKey('service-area', registry.serviceArea));
752
+ }
753
+
754
+ if (registry?.zone && !activeZoneKeys.has(this.#resourceContextKey('zone', registry.zone))) {
755
+ this.leafletContextmenuManager.removeContextMenu(this.#resourceContextKey('zone', registry.zone));
756
+ }
757
+ }
758
+ }
759
+
579
760
  #createDriverContextMenu(driver, layer) {
580
761
  let items = [
581
762
  {
@@ -44,10 +44,10 @@
44
44
  <div class="next-dd-menu-seperator"></div>
45
45
  <div class="px-1">
46
46
  <a href="javascript:;" class="next-dd-item" {{on "click" (dropdown-fn dd this.orderActions.bulkCancel this.overlay.selectedOrders)}}>{{t
47
- "fleet-ops.component.order-list-overlay.cancel"
47
+ "order-list-overlay.cancel"
48
48
  }}</a>
49
49
  <a href="javascript:;" class="next-dd-item text-danger" {{on "click" (dropdown-fn dd this.orderActions.bulkDelete this.overlay.selectedOrders)}}>{{t
50
- "fleet-ops.component.order-list-overlay.delete"
50
+ "order-list-overlay.delete"
51
51
  }}</a>
52
52
  </div>
53
53
  </div>
@@ -70,10 +70,10 @@
70
70
  <div class="next-dd-menu-seperator"></div>
71
71
  <div class="px-1">
72
72
  <a href="javascript:;" class="next-dd-item" {{on "click" (dropdown-fn dd (transition-to "operations.orders.index.new"))}}>{{t
73
- "fleet-ops.component.order-list-overlay.create-order"
73
+ "order-list-overlay.create-order"
74
74
  }}</a>
75
75
  <a href="javascript:;" class="next-dd-item" {{on "click" (dropdown-fn dd this.fleetActions.modal.create)}}>{{t
76
- "fleet-ops.component.order-list-overlay.create-fleet"
76
+ "order-list-overlay.create-fleet"
77
77
  }}</a>
78
78
  </div>
79
79
  </div>
@@ -0,0 +1,30 @@
1
+ <Modal::Default @modalIsOpened={{@modalIsOpened}} @options={{@options}} @confirm={{@onConfirm}} @decline={{@onDecline}}>
2
+ <div class="modal-body-container">
3
+ <div class="mb-4 rounded-md border border-yellow-200 bg-yellow-50 p-3 text-sm text-yellow-900 dark:border-yellow-700 dark:bg-yellow-900 dark:text-yellow-100">
4
+ <div class="mb-1 font-semibold">Create a company customer account</div>
5
+ <div>
6
+ This action will create a vendor, assign
7
+ {{this.customer.name}}
8
+ as admin personnel, and migrate any customer-owned portal history to the vendor account.
9
+ </div>
10
+ </div>
11
+
12
+ <InputGroup @name="Primary Personnel" @wrapperClass="mb-0i">
13
+ <div class="flex items-start rounded-md border border-gray-200 bg-gray-50 px-3 py-2 dark:border-gray-700 dark:bg-gray-800">
14
+ <Image src={{this.customer.photo_url}} @fallbackSrc={{config "defaultValues.contactImage"}} alt={{this.customer.name}} class="mr-2 h-6 w-6 rounded-md" />
15
+ <div class="min-w-0">
16
+ <div class="leading-4 text-sm truncate font-semibold dark:text-gray-100">{{this.customer.name}}</div>
17
+ <div class="truncate text-sm text-gray-500 dark:text-gray-400">{{n-a this.customer.email}}</div>
18
+ </div>
19
+ </div>
20
+ </InputGroup>
21
+
22
+ <div class="mt-4 grid grid-cols-1 gap-3 text-xs lg:grid-cols-2">
23
+ <InputGroup @name="Vendor Name" @value={{this.vendorName}} @placeholder="Company name" @wrapperClass="mb-0i" />
24
+ <InputGroup @name="Email" @type="email" @value={{this.vendorEmail}} @placeholder="billing@example.com" @wrapperClass="mb-0i" />
25
+ <InputGroup @name="Phone" @wrapperClass="mb-0i">
26
+ <PhoneInput @value={{this.vendorPhone}} @onInput={{fn (mut this.vendorPhone)}} class="form-input w-full" />
27
+ </InputGroup>
28
+ </div>
29
+ </div>
30
+ </Modal::Default>
@@ -0,0 +1,55 @@
1
+ import Component from '@glimmer/component';
2
+ import { inject as service } from '@ember/service';
3
+ import { tracked } from '@glimmer/tracking';
4
+
5
+ export default class ModalsConvertCustomerToVendorComponent extends Component {
6
+ @service fetch;
7
+ @service notifications;
8
+
9
+ @tracked customer;
10
+ @tracked vendorName;
11
+ @tracked vendorEmail;
12
+ @tracked vendorPhone;
13
+ @tracked options = {};
14
+
15
+ constructor(owner, { options }) {
16
+ super(...arguments);
17
+ this.customer = options.customer;
18
+ this.vendorName = this.customer?.name;
19
+ this.vendorEmail = this.customer?.email;
20
+ this.vendorPhone = this.customer?.phone;
21
+ this.options = options;
22
+ this.setupOptions();
23
+ }
24
+
25
+ setupOptions() {
26
+ this.options.title = 'Convert Contact to Vendor';
27
+ this.options.acceptButtonText = 'Create Vendor';
28
+ this.options.confirm = async (modal) => {
29
+ modal.startLoading();
30
+
31
+ try {
32
+ const response = await this.fetch.post(
33
+ `contacts/${this.customer.id}/convert-to-vendor`,
34
+ {
35
+ name: this.vendorName,
36
+ email: this.vendorEmail,
37
+ phone: this.vendorPhone,
38
+ },
39
+ { namespace: 'int/v1' }
40
+ );
41
+
42
+ this.notifications.success('Customer converted to vendor account.');
43
+
44
+ if (typeof this.options.onConverted === 'function') {
45
+ this.options.onConverted(response.vendor);
46
+ }
47
+
48
+ modal.done();
49
+ } catch (error) {
50
+ this.notifications.serverError(error);
51
+ modal.stopLoading();
52
+ }
53
+ };
54
+ }
55
+ }
@@ -0,0 +1,20 @@
1
+ <Modal::Default @modalIsOpened={{@modalIsOpened}} @options={{@options}} @confirm={{@onConfirm}} @decline={{@onDecline}}>
2
+ <div class="modal-body-container">
3
+ <InputGroup @name="Assignee" @helpText="Choose the operator responsible for following up on this issue.">
4
+ <ModelSelect
5
+ @modelName="user"
6
+ @selectedModel={{this.selectedAssignee}}
7
+ @placeholder="Select assignee"
8
+ @triggerClass="form-select form-input"
9
+ @infiniteScroll={{false}}
10
+ @renderInPlace={{true}}
11
+ @query={{hash is_not_customer=true}}
12
+ @allowClear={{true}}
13
+ @onChange={{fn (mut this.selectedAssignee)}}
14
+ as |model|
15
+ >
16
+ {{model.name}}
17
+ </ModelSelect>
18
+ </InputGroup>
19
+ </div>
20
+ </Modal::Default>
@@ -0,0 +1,49 @@
1
+ import Component from '@glimmer/component';
2
+ import { tracked } from '@glimmer/tracking';
3
+ import { inject as service } from '@ember/service';
4
+
5
+ export default class ModalsIssueAssignComponent extends Component {
6
+ @service notifications;
7
+
8
+ @tracked options = {};
9
+ @tracked issue;
10
+ @tracked selectedAssignee;
11
+
12
+ constructor(owner, { options }) {
13
+ super(...arguments);
14
+ this.options = options;
15
+ this.issue = options.issue;
16
+ this.selectedAssignee = this.issue?.assignee;
17
+ this.setupOptions();
18
+ }
19
+
20
+ setupOptions() {
21
+ this.options.title = 'Assign Issue';
22
+ this.options.acceptButtonText = 'Assign Issue';
23
+ this.options.acceptButtonIcon = 'user-check';
24
+ this.options.confirm = async (modal) => {
25
+ modal.startLoading();
26
+
27
+ try {
28
+ if (this.selectedAssignee?.type === 'customer') {
29
+ this.notifications.warning('Customers cannot be assigned to issues.');
30
+ modal.stopLoading();
31
+ return;
32
+ }
33
+
34
+ this.issue.assignee = this.selectedAssignee;
35
+ await this.issue.save();
36
+ this.notifications.success('Issue assignment updated.');
37
+
38
+ if (typeof this.options.onSaved === 'function') {
39
+ await this.options.onSaved(this.issue);
40
+ }
41
+
42
+ modal.done();
43
+ } catch (error) {
44
+ this.notifications.serverError(error);
45
+ modal.stopLoading();
46
+ }
47
+ };
48
+ }
49
+ }
@@ -0,0 +1,30 @@
1
+ <Modal::Default @modalIsOpened={{@modalIsOpened}} @options={{@options}} @confirm={{@onConfirm}} @decline={{@onDecline}}>
2
+ <div class="modal-body-container">
3
+ <div class="issue-close-modal-summary">
4
+ {{#if this.vehicleLabel}}
5
+ <div>
6
+ <span>Vehicle</span>
7
+ <strong>{{this.vehicleLabel}}</strong>
8
+ </div>
9
+ {{/if}}
10
+ {{#if this.orderLabel}}
11
+ <div>
12
+ <span>Order</span>
13
+ <strong>{{this.orderLabel}}</strong>
14
+ </div>
15
+ {{/if}}
16
+ <div>
17
+ <span>Summary</span>
18
+ <strong>{{n-a this.issue.title}}</strong>
19
+ </div>
20
+ <div>
21
+ <span>Description</span>
22
+ <strong>{{n-a this.issue.report "No report was provided."}}</strong>
23
+ </div>
24
+ </div>
25
+
26
+ <InputGroup @name="Close Note" @helpText="Describe why this issue is being closed.">
27
+ <Textarea @value={{this.note}} class="form-input w-full" rows={{5}} placeholder="Resolution note" />
28
+ </InputGroup>
29
+ </div>
30
+ </Modal::Default>
@@ -0,0 +1,73 @@
1
+ import Component from '@glimmer/component';
2
+ import { tracked } from '@glimmer/tracking';
3
+ import { inject as service } from '@ember/service';
4
+
5
+ export default class ModalsIssueCloseComponent extends Component {
6
+ @service currentUser;
7
+ @service notifications;
8
+
9
+ @tracked options = {};
10
+ @tracked issue;
11
+ @tracked note;
12
+
13
+ constructor(owner, { options }) {
14
+ super(...arguments);
15
+ this.options = options;
16
+ this.issue = options.issue;
17
+ this.setupOptions();
18
+ }
19
+
20
+ get orderLabel() {
21
+ return this.issue?.order?.tracking || this.issue?.order?.public_id;
22
+ }
23
+
24
+ get vehicleLabel() {
25
+ return this.issue?.vehicle?.displayName || this.issue?.vehicle_name;
26
+ }
27
+
28
+ setupOptions() {
29
+ this.options.title = `Close ${this.issue?.public_id || 'Issue'}`;
30
+ this.options.acceptButtonText = 'Close Issue';
31
+ this.options.acceptButtonIcon = 'circle-check';
32
+ this.options.acceptButtonScheme = 'success';
33
+ this.options.confirm = async (modal) => {
34
+ if (!this.note) {
35
+ this.notifications.warning('Add a close note before closing the issue.');
36
+ return;
37
+ }
38
+
39
+ modal.startLoading();
40
+
41
+ try {
42
+ const closedAt = new Date().toISOString();
43
+ const user = this.currentUser.user;
44
+ const meta = this.issue.meta ?? {};
45
+
46
+ this.issue.status = 'closed';
47
+ this.issue.resolved_at = new Date();
48
+ this.issue.meta = {
49
+ ...meta,
50
+ resolution: {
51
+ ...(meta.resolution ?? {}),
52
+ note: this.note,
53
+ closed_at: closedAt,
54
+ closed_by_uuid: user?.id,
55
+ closed_by_name: user?.name,
56
+ },
57
+ };
58
+
59
+ await this.issue.save();
60
+ this.notifications.success('Issue closed.');
61
+
62
+ if (typeof this.options.onSaved === 'function') {
63
+ await this.options.onSaved(this.issue);
64
+ }
65
+
66
+ modal.done();
67
+ } catch (error) {
68
+ this.notifications.serverError(error);
69
+ modal.stopLoading();
70
+ }
71
+ };
72
+ }
73
+ }