@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
@@ -27,7 +27,7 @@ export default class OperationsServiceRatesIndexRoute extends Route {
27
27
  model(params) {
28
28
  return this.store.query('service-rate', {
29
29
  ...params,
30
- with: ['parcelFees', 'rateFees', 'zone', 'serviceArea'],
30
+ with: ['parcelFees', 'rateFees.zone', 'rateFees.serviceArea', 'zone', 'serviceArea'],
31
31
  });
32
32
  }
33
33
  }
package/addon/routes.js CHANGED
@@ -53,6 +53,7 @@ export default buildRoutes(function () {
53
53
  this.route('new');
54
54
  this.route('details', { path: '/:public_id' }, function () {
55
55
  this.route('index', { path: '/' });
56
+ this.route('personnel');
56
57
  });
57
58
  this.route('edit', { path: '/edit/:public_id' });
58
59
  });
@@ -1,9 +1,15 @@
1
1
  import ResourceActionService from '@fleetbase/ember-core/services/resource-action';
2
2
  import { inject as service } from '@ember/service';
3
- import { action } from '@ember/object';
3
+ import { action, get } from '@ember/object';
4
+
5
+ const INTERNAL_NAMESPACE = 'int/v1';
4
6
 
5
7
  export default class ContactActionsService extends ResourceActionService {
8
+ @service fetch;
6
9
  @service placeActions;
10
+ @service notifications;
11
+ @service hostRouter;
12
+ @service('universe/extension-manager') extensionManager;
7
13
 
8
14
  constructor() {
9
15
  super(...arguments);
@@ -112,4 +118,228 @@ export default class ContactActionsService extends ResourceActionService {
112
118
  }
113
119
  );
114
120
  }
121
+
122
+ hasLinkedUser(contact) {
123
+ const user = contact?.user;
124
+ return Boolean(contact?.user_uuid || (user && (get(user, 'id') || get(user, 'uuid'))));
125
+ }
126
+
127
+ linkedUserStatus(contact) {
128
+ const user = contact?.user;
129
+ return (user ? (get(user, 'status') ?? get(user, 'session_status')) : null) ?? 'active';
130
+ }
131
+
132
+ hasInactiveLogin(contact) {
133
+ return ['inactive', 'disabled', 'suspended'].includes(this.linkedUserStatus(contact));
134
+ }
135
+
136
+ isCustomerPortalInstalled() {
137
+ return this.extensionManager.isInstalled('@fleetbase/customer-portal-engine');
138
+ }
139
+
140
+ accountActionButton(contact, options = {}) {
141
+ if (!this.hasLinkedUser(contact)) {
142
+ return null;
143
+ }
144
+
145
+ return {
146
+ icon: 'ellipsis-h',
147
+ iconPrefix: 'fas',
148
+ renderInPlace: true,
149
+ items: this.accountActionItems(contact, options),
150
+ };
151
+ }
152
+
153
+ accountActionItems(contact, options = {}) {
154
+ const actions = [
155
+ {
156
+ text: 'Reset Password',
157
+ icon: 'key',
158
+ fn: () => this.openResetPasswordModal(contact),
159
+ },
160
+ {
161
+ text: 'Send Credentials',
162
+ icon: 'paper-plane',
163
+ fn: () => this.confirmSendCredentials(contact),
164
+ },
165
+ this.hasInactiveLogin(contact)
166
+ ? {
167
+ text: 'Reactivate Login',
168
+ icon: 'unlock',
169
+ fn: () => this.confirmReactivatePortalLogin(contact),
170
+ }
171
+ : {
172
+ text: 'Deactivate Login',
173
+ icon: 'lock',
174
+ fn: () => this.confirmDeactivatePortalLogin(contact),
175
+ class: 'text-red-500 hover:text-red-600',
176
+ },
177
+ ];
178
+
179
+ if (this.isCustomerPortalInstalled()) {
180
+ actions.push(
181
+ {
182
+ separator: true,
183
+ },
184
+ {
185
+ text: 'Convert to Vendor',
186
+ icon: 'building',
187
+ fn: () => this.openConvertToVendorModal(contact, options),
188
+ }
189
+ );
190
+ }
191
+
192
+ return actions;
193
+ }
194
+
195
+ accountRowActionItems(options = {}) {
196
+ const hasCustomerPortal = this.isCustomerPortalInstalled();
197
+ const hasLinkedUser = (contact) => this.hasLinkedUser(contact);
198
+
199
+ const actions = [
200
+ {
201
+ label: 'Reset Password',
202
+ icon: 'key',
203
+ fn: (contact) => this.openResetPasswordModal(contact),
204
+ isVisible: hasLinkedUser,
205
+ },
206
+ {
207
+ label: 'Send Credentials',
208
+ icon: 'paper-plane',
209
+ fn: (contact) => this.confirmSendCredentials(contact),
210
+ isVisible: hasLinkedUser,
211
+ },
212
+ {
213
+ label: 'Deactivate Login',
214
+ icon: 'lock',
215
+ class: 'text-red-500 hover:text-red-600',
216
+ fn: (contact) => this.confirmDeactivatePortalLogin(contact),
217
+ isVisible: (contact) => hasLinkedUser(contact) && !this.hasInactiveLogin(contact),
218
+ },
219
+ {
220
+ label: 'Reactivate Login',
221
+ icon: 'unlock',
222
+ fn: (contact) => this.confirmReactivatePortalLogin(contact),
223
+ isVisible: (contact) => hasLinkedUser(contact) && this.hasInactiveLogin(contact),
224
+ },
225
+ ];
226
+
227
+ if (hasCustomerPortal) {
228
+ actions.push({
229
+ label: 'Convert to Vendor',
230
+ icon: 'building',
231
+ fn: (contact) => this.openConvertToVendorModal(contact, options),
232
+ isVisible: hasLinkedUser,
233
+ });
234
+ }
235
+
236
+ return actions;
237
+ }
238
+
239
+ openResetPasswordModal(contact) {
240
+ this.modalsManager.show('modals/reset-customer-credentials', {
241
+ customer: contact,
242
+ });
243
+ }
244
+
245
+ openConvertToVendorModal(contact, options = {}) {
246
+ this.modalsManager.show('modals/convert-customer-to-vendor', {
247
+ customer: contact,
248
+ onConverted: (vendor) => {
249
+ if (typeof options.onConverted === 'function') {
250
+ options.onConverted(vendor);
251
+ return;
252
+ }
253
+
254
+ if (vendor?.id) {
255
+ this.hostRouter.transitionTo('console.fleet-ops.management.vendors.index.details', vendor.id);
256
+ }
257
+ },
258
+ });
259
+ }
260
+
261
+ confirmSendCredentials(contact) {
262
+ this.confirmLoginAction(contact, {
263
+ title: 'Send Portal Credentials',
264
+ body: 'Generate a new temporary password and email this contact their portal credentials.',
265
+ acceptButtonText: 'Send Credentials',
266
+ endpoint: 'customers/send-credentials',
267
+ successMessage: 'Portal credentials sent.',
268
+ });
269
+ }
270
+
271
+ confirmDeactivatePortalLogin(contact) {
272
+ this.confirmLoginAction(contact, {
273
+ title: 'Deactivate Login',
274
+ body: 'Deactivate portal access for this contact. Their profile and history will be preserved.',
275
+ acceptButtonText: 'Deactivate Login',
276
+ acceptButtonScheme: 'danger',
277
+ endpoint: 'customers/deactivate-portal-login',
278
+ successMessage: 'Portal login deactivated.',
279
+ });
280
+ }
281
+
282
+ confirmReactivatePortalLogin(contact) {
283
+ this.confirmLoginAction(contact, {
284
+ title: 'Reactivate Login',
285
+ body: 'Reactivate portal access for this contact.',
286
+ acceptButtonText: 'Reactivate Login',
287
+ endpoint: 'customers/reactivate-portal-login',
288
+ successMessage: 'Portal login reactivated.',
289
+ });
290
+ }
291
+
292
+ confirmLoginAction(contact, { title, body, acceptButtonText, acceptButtonScheme, endpoint, successMessage }) {
293
+ this.modalsManager.confirm({
294
+ title,
295
+ body,
296
+ acceptButtonText,
297
+ acceptButtonScheme,
298
+ confirm: async (modal) => {
299
+ modal.startLoading();
300
+
301
+ try {
302
+ const response = await this.fetch.post(endpoint, { customer: contact?.id }, { namespace: INTERNAL_NAMESPACE });
303
+ await this.updateContactFromResponse(contact, response);
304
+ this.notifications.success(successMessage);
305
+ modal.done();
306
+ } catch (error) {
307
+ this.notifications.serverError(error);
308
+ modal.stopLoading();
309
+ }
310
+ },
311
+ });
312
+ }
313
+
314
+ async updateContactFromResponse(contact, response) {
315
+ const payload = response.customer ?? response.contact;
316
+
317
+ if (!payload || typeof contact?.setProperties !== 'function') {
318
+ return;
319
+ }
320
+
321
+ contact.setProperties(
322
+ this.withoutIdentityFields({
323
+ user_uuid: payload.user_uuid,
324
+ })
325
+ );
326
+
327
+ if (payload.user) {
328
+ const user = await contact.user;
329
+
330
+ if (typeof user?.setProperties === 'function') {
331
+ user.setProperties(this.withoutIdentityFields(payload.user));
332
+ }
333
+ }
334
+ }
335
+
336
+ withoutIdentityFields(payload = {}) {
337
+ const attributes = { ...payload };
338
+
339
+ delete attributes.id;
340
+ delete attributes.uuid;
341
+ delete attributes.public_id;
342
+
343
+ return attributes;
344
+ }
115
345
  }
@@ -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
 
@@ -0,0 +1,44 @@
1
+ import Service from '@ember/service';
2
+ import { tracked } from '@glimmer/tracking';
3
+ import { inject as service } from '@ember/service';
4
+ import { task } from 'ember-concurrency';
5
+
6
+ export default class GettingStartedService extends Service {
7
+ @service fetch;
8
+
9
+ @tracked data = null;
10
+ @tracked error = null;
11
+
12
+ get isCompleted() {
13
+ return this.data?.is_completed === true;
14
+ }
15
+
16
+ get steps() {
17
+ return this.data?.steps ?? [];
18
+ }
19
+
20
+ get recommendations() {
21
+ return this.data?.recommendations ?? [];
22
+ }
23
+
24
+ get progress() {
25
+ return this.data?.progress ?? { completed: 0, total: 0, percent: 0 };
26
+ }
27
+
28
+ get nextStepKey() {
29
+ return this.data?.next_step;
30
+ }
31
+
32
+ get nextStep() {
33
+ return this.steps.find((step) => step.key === this.nextStepKey) ?? this.steps.find((step) => !step.completed);
34
+ }
35
+
36
+ @task({ drop: true }) *load() {
37
+ try {
38
+ this.data = yield this.fetch.get('fleet-ops/getting-started/status');
39
+ this.error = null;
40
+ } catch (error) {
41
+ this.error = error?.message ?? 'Failed to load getting started status';
42
+ }
43
+ }
44
+ }
@@ -2,6 +2,7 @@ import ResourceActionService from '@fleetbase/ember-core/services/resource-actio
2
2
  import { inject as service } from '@ember/service';
3
3
  import { action } from '@ember/object';
4
4
  import { format } from 'date-fns';
5
+ import { issueStatuses } from '../utils/fleet-ops-options';
5
6
 
6
7
  export default class IssueActionsService extends ResourceActionService {
7
8
  @service driverActions;
@@ -92,6 +93,76 @@ export default class IssueActionsService extends ResourceActionService {
92
93
  },
93
94
  };
94
95
 
96
+ get closedStatuses() {
97
+ return ['closed', 'resolved', 'completed'];
98
+ }
99
+
100
+ openStatusModal(issue, options = {}) {
101
+ return this.modalsManager.show('modals/issue-status', {
102
+ issue,
103
+ statusOptions: issueStatuses,
104
+ onSaved: options.onSaved,
105
+ });
106
+ }
107
+
108
+ openAssignModal(issue, options = {}) {
109
+ return this.modalsManager.show('modals/issue-assign', {
110
+ issue,
111
+ onSaved: options.onSaved,
112
+ });
113
+ }
114
+
115
+ openCloseIssueModal(issue, options = {}) {
116
+ return this.modalsManager.show('modals/issue-close', {
117
+ issue,
118
+ onSaved: options.onSaved,
119
+ });
120
+ }
121
+
122
+ confirmReopenIssue(issue, options = {}) {
123
+ this.modalsManager.confirm({
124
+ title: `Re-open ${issue.public_id || 'issue'}?`,
125
+ body: 'This will move the issue back into an active re-opened state and clear the active resolution date.',
126
+ acceptButtonText: 'Re-open Issue',
127
+ acceptButtonIcon: 'rotate-left',
128
+ confirm: async (modal) => {
129
+ modal.startLoading();
130
+
131
+ try {
132
+ const meta = issue.meta ?? {};
133
+ const reopenHistory = Array.isArray(meta.reopen_history) ? meta.reopen_history : [];
134
+ const user = this.currentUser.user;
135
+
136
+ issue.status = 're_opened';
137
+ issue.resolved_at = null;
138
+ issue.meta = {
139
+ ...meta,
140
+ reopen_history: [
141
+ ...reopenHistory,
142
+ {
143
+ reopened_at: new Date().toISOString(),
144
+ reopened_by_uuid: user?.id,
145
+ reopened_by_name: user?.name,
146
+ },
147
+ ],
148
+ };
149
+
150
+ await issue.save();
151
+ this.notifications.success('Issue re-opened.');
152
+
153
+ if (typeof options.onSaved === 'function') {
154
+ await options.onSaved(issue);
155
+ }
156
+
157
+ modal.done();
158
+ } catch (error) {
159
+ this.notifications.serverError(error);
160
+ modal.stopLoading();
161
+ }
162
+ },
163
+ });
164
+ }
165
+
95
166
  @action async viewDriver(issue) {
96
167
  const driver = await issue.loadDriver();
97
168
  if (driver) {
@@ -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
  *
@@ -7,6 +7,7 @@ import { renderCompleted, waitForInsertedAndSized } from '@fleetbase/ember-ui/ut
7
7
  import { Control as RoutingControl } from '@fleetbase/leaflet-routing-machine';
8
8
  import { getLayerById, findLayer, flyToLayer } from '../utils/leaflet';
9
9
  import isUuid from '@fleetbase/ember-core/utils/is-uuid';
10
+ import ensureLeafletPluginsReady from '../utils/leaflet-plugin-loader';
10
11
 
11
12
  export default class LeafletMapManagerService extends Service {
12
13
  @service leafletRoutingControl;
@@ -92,6 +93,7 @@ export default class LeafletMapManagerService extends Service {
92
93
  }
93
94
 
94
95
  async ensureInteractive({ timeoutMs = 8000 } = {}) {
96
+ await ensureLeafletPluginsReady({ timeoutMs });
95
97
  await this.whenMapLoaded({ timeoutMs });
96
98
 
97
99
  // always ask Leaflet for the live container