@fleetbase/fleetops-engine 0.6.48 → 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 (266) 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/driver/details.hbs +5 -1
  7. package/addon/components/driver/form.hbs +11 -1
  8. package/addon/components/fleet-listing-panel.hbs +2 -2
  9. package/addon/components/fleet-listing-panel.js +10 -0
  10. package/addon/components/map/google-live-map.js +18 -75
  11. package/addon/components/map/leaflet-live-map.hbs +187 -37
  12. package/addon/components/map/leaflet-live-map.js +184 -20
  13. package/addon/components/modals/order-change-destination.hbs +46 -0
  14. package/addon/components/modals/order-change-destination.js +106 -0
  15. package/addon/components/modals/stop-activity.hbs +28 -0
  16. package/addon/components/modals/stop-activity.js +52 -0
  17. package/addon/components/modals/update-order-activity.hbs +45 -3
  18. package/addon/components/modals/update-order-activity.js +143 -5
  19. package/addon/components/orchestrator/phase-builder.hbs +21 -0
  20. package/addon/components/orchestrator/phase-builder.js +13 -0
  21. package/addon/components/orchestrator/resource-panel.hbs +0 -10
  22. package/addon/components/orchestrator/resource-panel.js +2 -4
  23. package/addon/components/orchestrator-workbench.js +1 -0
  24. package/addon/components/order/activity-list.hbs +2 -2
  25. package/addon/components/order/activity-timeline.hbs +31 -29
  26. package/addon/components/order/details/activity.hbs +3 -3
  27. package/addon/components/order/details/activity.js +11 -0
  28. package/addon/components/order/details/documents.js +2 -0
  29. package/addon/components/order/details/purchase-rate.hbs +2 -2
  30. package/addon/components/order/details/route.hbs +11 -3
  31. package/addon/components/order/details/route.js +91 -14
  32. package/addon/components/order/details/tracking.hbs +101 -72
  33. package/addon/components/order/details/tracking.js +149 -2
  34. package/addon/components/order/form/details.hbs +3 -3
  35. package/addon/components/order/form/documents.js +11 -4
  36. package/addon/components/order/form/route.hbs +108 -104
  37. package/addon/components/order/form/route.js +37 -1
  38. package/addon/components/order/form/service-rate.hbs +2 -2
  39. package/addon/components/order/form.hbs +2 -2
  40. package/addon/components/order-config-manager/activity-flow.hbs +4 -1
  41. package/addon/components/order-config-manager/activity-flow.js +178 -12
  42. package/addon/components/place/address.js +1 -1
  43. package/addon/components/route-list.hbs +17 -17
  44. package/addon/components/route-list.js +42 -10
  45. package/addon/components/service-rate/details.hbs +41 -0
  46. package/addon/components/service-rate/form.hbs +118 -0
  47. package/addon/components/service-rate/form.js +55 -0
  48. package/addon/components/vehicle/form.js +1 -1
  49. package/addon/components/vehicle/pill.hbs +1 -1
  50. package/addon/components/widget/fleet-ops-key-metrics.hbs +19 -17
  51. package/addon/components/widget/fleet-ops-key-metrics.js +26 -0
  52. package/addon/components/widget/fuel-efficiency.hbs +26 -0
  53. package/addon/components/widget/fuel-efficiency.js +43 -0
  54. package/addon/components/widget/geofence-violations.hbs +44 -0
  55. package/addon/components/widget/geofence-violations.js +55 -0
  56. package/addon/components/widget/issues-insights.hbs +43 -0
  57. package/addon/components/widget/issues-insights.js +62 -0
  58. package/addon/components/widget/kpi-active-orders.hbs +10 -0
  59. package/addon/components/widget/kpi-active-orders.js +5 -0
  60. package/addon/components/widget/kpi-aov.hbs +10 -0
  61. package/addon/components/widget/kpi-aov.js +5 -0
  62. package/addon/components/widget/kpi-distance.hbs +10 -0
  63. package/addon/components/widget/kpi-distance.js +5 -0
  64. package/addon/components/widget/kpi-drivers-online.hbs +10 -0
  65. package/addon/components/widget/kpi-drivers-online.js +5 -0
  66. package/addon/components/widget/kpi-earnings.hbs +10 -0
  67. package/addon/components/widget/kpi-earnings.js +5 -0
  68. package/addon/components/widget/kpi-open-issues.hbs +11 -0
  69. package/addon/components/widget/kpi-open-issues.js +5 -0
  70. package/addon/components/widget/kpi-tile.hbs +59 -0
  71. package/addon/components/widget/kpi-tile.js +168 -0
  72. package/addon/components/widget/live-fleet.hbs +122 -0
  73. package/addon/components/widget/live-fleet.js +365 -0
  74. package/addon/components/widget/maintenance-overview.hbs +45 -0
  75. package/addon/components/widget/maintenance-overview.js +40 -0
  76. package/addon/components/widget/on-time-delivery.hbs +27 -0
  77. package/addon/components/widget/on-time-delivery.js +73 -0
  78. package/addon/components/widget/operations-pulse.hbs +51 -0
  79. package/addon/components/widget/operations-pulse.js +173 -0
  80. package/addon/components/widget/orders-by-status.hbs +26 -0
  81. package/addon/components/widget/orders-by-status.js +54 -0
  82. package/addon/components/widget/revenue-trend.hbs +34 -0
  83. package/addon/components/widget/revenue-trend.js +67 -0
  84. package/addon/components/widget/top-drivers.hbs +44 -0
  85. package/addon/components/widget/top-drivers.js +54 -0
  86. package/addon/controllers/management/vehicles/index/new.js +1 -1
  87. package/addon/controllers/operations/orders/index/details.js +4 -4
  88. package/addon/controllers/operations/orders/index.js +14 -0
  89. package/addon/controllers/settings/routing.js +26 -0
  90. package/addon/extension.js +172 -3
  91. package/addon/helpers/place-address-html.js +2 -2
  92. package/addon/routes/operations/service-rates/index.js +1 -1
  93. package/addon/services/geofence.js +163 -19
  94. package/addon/services/leaflet-contextmenu-manager.js +27 -0
  95. package/addon/services/map-adapter/google.js +13 -2
  96. package/addon/services/vehicle-actions.js +1 -1
  97. package/addon/styles/fleetops-engine.css +205 -88
  98. package/addon/templates/maintenance/equipment/index.hbs +1 -1
  99. package/addon/templates/maintenance/parts/index.hbs +1 -1
  100. package/addon/templates/maintenance/schedules/index.hbs +1 -1
  101. package/addon/templates/management/vehicles/index.hbs +1 -1
  102. package/addon/templates/settings/avatars.hbs +1 -1
  103. package/addon/templates/settings/custom-fields.hbs +1 -1
  104. package/addon/templates/settings/map.hbs +1 -1
  105. package/addon/templates/settings/navigator-app.hbs +1 -1
  106. package/addon/templates/settings/notifications.hbs +1 -1
  107. package/addon/templates/settings/orchestrator.hbs +1 -1
  108. package/addon/templates/settings/payments/index.hbs +1 -1
  109. package/addon/templates/settings/payments/onboard.hbs +1 -1
  110. package/addon/templates/settings/routing.hbs +22 -2
  111. package/addon/templates/settings/scheduling.hbs +1 -1
  112. package/addon/utils/fleet-ops-options.js +2 -1
  113. package/addon/utils/live-map-card-content.js +171 -0
  114. package/addon/utils/normalize-order-config-flow.js +41 -0
  115. package/addon/utils/place-address-html.js +3 -2
  116. package/addon/utils/route-visualization.js +54 -7
  117. package/app/components/modals/order-change-destination.js +1 -0
  118. package/app/components/modals/stop-activity.js +1 -0
  119. package/app/components/widget/fuel-efficiency.js +1 -0
  120. package/app/components/widget/geofence-violations.js +1 -0
  121. package/app/components/widget/issues-insights.js +1 -0
  122. package/app/components/widget/kpi-active-orders.js +1 -0
  123. package/app/components/widget/kpi-aov.js +1 -0
  124. package/app/components/widget/kpi-distance.js +1 -0
  125. package/app/components/widget/kpi-drivers-online.js +1 -0
  126. package/app/components/widget/kpi-earnings.js +1 -0
  127. package/app/components/widget/kpi-open-issues.js +1 -0
  128. package/app/components/widget/kpi-tile.js +1 -0
  129. package/app/components/widget/live-fleet.js +1 -0
  130. package/app/components/widget/maintenance-overview.js +1 -0
  131. package/app/components/widget/on-time-delivery.js +1 -0
  132. package/app/components/widget/operations-pulse.js +1 -0
  133. package/app/components/widget/orders-by-status.js +1 -0
  134. package/app/components/widget/revenue-trend.js +1 -0
  135. package/app/components/widget/top-drivers.js +1 -0
  136. package/app/utils/live-map-card-content.js +1 -0
  137. package/composer.json +1 -1
  138. package/extension.json +1 -1
  139. package/package.json +3 -3
  140. package/pnpm-workspace.yaml +1 -0
  141. package/server/config/telematics.php +111 -1
  142. package/server/migrations/2026_05_18_000001_add_license_expiry_to_drivers_table.php +25 -0
  143. package/server/migrations/2026_05_21_000001_add_geographic_rules_to_service_rate_fees_table.php +37 -0
  144. package/server/migrations/2026_05_25_000001_add_endpoint_tracking_refs_to_payloads_table.php +50 -0
  145. package/server/migrations/2026_05_27_000001_default_driver_vehicle_statuses_to_available.php +48 -0
  146. package/server/resources/views/labels/default.php +57 -49
  147. package/server/resources/views/labels/waypoint-label.php +27 -20
  148. package/server/seeders/Testing/Concerns/SeedsTestingData.php +183 -0
  149. package/server/seeders/Testing/ConnectivitySeeder.php +213 -0
  150. package/server/seeders/Testing/FleetSeeder.php +341 -0
  151. package/server/seeders/Testing/MaintenanceSeeder.php +238 -0
  152. package/server/seeders/Testing/NetworkSeeder.php +250 -0
  153. package/server/seeders/Testing/OrdersSeeder.php +198 -0
  154. package/server/seeders/Testing/TestingSeeder.php +40 -0
  155. package/server/seeders/{VroomOrchestrationSeeder.php → Testing/VroomOrchestrationSeeder.php} +29 -22
  156. package/server/src/Console/Commands/AuditCustomerUserConflicts.php +75 -0
  157. package/server/src/Console/Commands/ProcessOperationalAlerts.php +279 -0
  158. package/server/src/Exceptions/CustomerUserConflictException.php +13 -0
  159. package/server/src/Http/Controllers/Api/v1/ContactController.php +17 -2
  160. package/server/src/Http/Controllers/Api/v1/CustomerController.php +876 -0
  161. package/server/src/Http/Controllers/Api/v1/DriverController.php +1 -1
  162. package/server/src/Http/Controllers/Api/v1/OrderConfigController.php +54 -0
  163. package/server/src/Http/Controllers/Api/v1/OrderController.php +98 -70
  164. package/server/src/Http/Controllers/Api/v1/ServiceAreaController.php +2 -2
  165. package/server/src/Http/Controllers/Api/v1/VehicleController.php +20 -3
  166. package/server/src/Http/Controllers/Internal/v1/AnalyticsController.php +119 -0
  167. package/server/src/Http/Controllers/Internal/v1/ContactController.php +19 -0
  168. package/server/src/Http/Controllers/Internal/v1/LiveController.php +101 -66
  169. package/server/src/Http/Controllers/Internal/v1/MetricsController.php +82 -3
  170. package/server/src/Http/Controllers/Internal/v1/OrderController.php +441 -32
  171. package/server/src/Http/Controllers/Internal/v1/ServiceQuoteController.php +1 -1
  172. package/server/src/Http/Controllers/Internal/v1/SettingController.php +46 -1
  173. package/server/src/Http/Controllers/Internal/v1/VehicleController.php +54 -1
  174. package/server/src/Http/Middleware/AuthenticateCustomerToken.php +34 -0
  175. package/server/src/Http/Requests/CreateCustomerOrderRequest.php +63 -0
  176. package/server/src/Http/Requests/CreateCustomerRequest.php +48 -0
  177. package/server/src/Http/Requests/CreateDriverRequest.php +1 -1
  178. package/server/src/Http/Requests/CreateVehicleRequest.php +25 -1
  179. package/server/src/Http/Requests/Internal/CreateDriverRequest.php +1 -1
  180. package/server/src/Http/Requests/VerifyCreateCustomerRequest.php +29 -0
  181. package/server/src/Http/Resources/Internal/v1/OrderConfig.php +41 -0
  182. package/server/src/Http/Resources/v1/Customer.php +79 -0
  183. package/server/src/Http/Resources/v1/Driver.php +2 -0
  184. package/server/src/Http/Resources/v1/Fleet.php +4 -0
  185. package/server/src/Http/Resources/v1/Index/Driver.php +48 -0
  186. package/server/src/Http/Resources/v1/Index/Vehicle.php +74 -1
  187. package/server/src/Http/Resources/v1/OrderConfig.php +74 -0
  188. package/server/src/Http/Resources/v1/Payload.php +23 -20
  189. package/server/src/Http/Resources/v1/ServiceArea.php +33 -19
  190. package/server/src/Http/Resources/v1/ServiceRateFee.php +7 -0
  191. package/server/src/Listeners/HandleOrderDispatched.php +2 -2
  192. package/server/src/Models/Contact.php +92 -15
  193. package/server/src/Models/Customer.php +64 -0
  194. package/server/src/Models/Driver.php +14 -3
  195. package/server/src/Models/Entity.php +2 -2
  196. package/server/src/Models/Order.php +29 -4
  197. package/server/src/Models/OrderConfig.php +14 -11
  198. package/server/src/Models/Payload.php +17 -1
  199. package/server/src/Models/ServiceRate.php +273 -1
  200. package/server/src/Models/ServiceRateFee.php +39 -9
  201. package/server/src/Models/TrackingNumber.php +8 -2
  202. package/server/src/Models/Vehicle.php +18 -2
  203. package/server/src/Models/Waypoint.php +2 -2
  204. package/server/src/Notifications/LateDeparture.php +48 -0
  205. package/server/src/Notifications/ProlongedStoppage.php +48 -0
  206. package/server/src/Notifications/RouteDeviation.php +48 -0
  207. package/server/src/Observers/ContactObserver.php +2 -0
  208. package/server/src/Observers/ServiceRateObserver.php +6 -6
  209. package/server/src/Orchestration/Engines/VroomOrchestrationEngine.php +29 -2
  210. package/server/src/Providers/FleetOpsServiceProvider.php +6 -0
  211. package/server/src/Providers/NotificationServiceProvider.php +3 -0
  212. package/server/src/Support/Analytics/AbstractAnalytics.php +77 -0
  213. package/server/src/Support/Analytics/FuelEfficiency.php +83 -0
  214. package/server/src/Support/Analytics/GeofenceViolations.php +63 -0
  215. package/server/src/Support/Analytics/IssuesInsights.php +67 -0
  216. package/server/src/Support/Analytics/LiveFleet.php +127 -0
  217. package/server/src/Support/Analytics/MaintenanceOverview.php +70 -0
  218. package/server/src/Support/Analytics/OnTimeDelivery.php +75 -0
  219. package/server/src/Support/Analytics/OperationsPulse.php +86 -0
  220. package/server/src/Support/Analytics/OrdersByStatus.php +64 -0
  221. package/server/src/Support/Analytics/RevenueTrend.php +78 -0
  222. package/server/src/Support/Analytics/TopDrivers.php +92 -0
  223. package/server/src/Support/CustomerAuth.php +98 -0
  224. package/server/src/Support/LabelPdf.php +52 -0
  225. package/server/src/Support/Metrics/AbstractMetric.php +170 -0
  226. package/server/src/Support/Metrics/ActiveLiveOrdersMetric.php +28 -0
  227. package/server/src/Support/Metrics/AvgOrderValueMetric.php +53 -0
  228. package/server/src/Support/Metrics/DriversOnlineMetric.php +30 -0
  229. package/server/src/Support/Metrics/EarningsMetric.php +30 -0
  230. package/server/src/Support/Metrics/FuelCostsMetric.php +30 -0
  231. package/server/src/Support/Metrics/MoneyMetric.php +30 -0
  232. package/server/src/Support/Metrics/OpenIssuesMetric.php +35 -0
  233. package/server/src/Support/Metrics/OrdersCanceledMetric.php +35 -0
  234. package/server/src/Support/Metrics/OrdersCompletedMetric.php +35 -0
  235. package/server/src/Support/Metrics/OrdersInProgressMetric.php +51 -0
  236. package/server/src/Support/Metrics/OrdersScheduledMetric.php +37 -0
  237. package/server/src/Support/Metrics/Registry.php +46 -0
  238. package/server/src/Support/Metrics/ResolvedIssuesMetric.php +35 -0
  239. package/server/src/Support/Metrics/TotalCustomersMetric.php +29 -0
  240. package/server/src/Support/Metrics/TotalDistanceTraveledMetric.php +35 -0
  241. package/server/src/Support/Metrics/TotalDriversMetric.php +28 -0
  242. package/server/src/Support/Metrics/TotalTimeTraveledMetric.php +35 -0
  243. package/server/src/Support/Metrics.php +46 -249
  244. package/server/src/Support/ResolvesOrderServiceStops.php +512 -0
  245. package/server/src/Support/Telematics/Providers/AfaqyProvider.php +290 -0
  246. package/server/src/Support/Telematics/Providers/SafeeProvider.php +333 -0
  247. package/server/src/Tracking/TrackingContextBuilder.php +41 -38
  248. package/server/src/Tracking/TrackingIntelligenceService.php +109 -9
  249. package/server/src/Tracking/TrackingStop.php +13 -11
  250. package/server/src/routes.php +50 -0
  251. package/server/tests/AnalyticsRoutesTest.php +41 -0
  252. package/server/tests/CapacityAllocationEngineTest.php +10 -10
  253. package/server/tests/CustomerContactIdentitySafetyTest.php +54 -0
  254. package/server/tests/CustomerEndpointTest.php +199 -0
  255. package/server/tests/DriverVehicleStatusDefaultTest.php +39 -0
  256. package/server/tests/LabelPdfRenderingTest.php +23 -0
  257. package/server/tests/LiveControllerViewportTest.php +60 -0
  258. package/server/tests/MetricsRegistryTest.php +84 -0
  259. package/server/tests/OrchestrationPayloadBuilderTest.php +2 -2
  260. package/server/tests/OrderConfigEndpointTest.php +86 -0
  261. package/server/tests/OrderFileAttachmentTest.php +20 -0
  262. package/server/tests/ServiceAreaZoneApiShapeTest.php +34 -0
  263. package/server/tests/VehicleDriverAssignmentTest.php +18 -0
  264. package/server/tests/VroomOrchestrationEngineTest.php +91 -0
  265. package/translations/en-us.yaml +38 -0
  266. package/server/migrations/2026_05_13_000001_repair_customer_contact_user_types.php +0 -86
@@ -8,11 +8,14 @@ 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
14
 
15
15
  export default class MapLeafletLiveMapComponent extends Component {
16
+ liveViewportReloadDebounceMs = 300;
17
+ liveViewportResourceLimit = 500;
18
+
16
19
  @service mapManager;
17
20
  @service mapSettings;
18
21
  @service leafletMapManager;
@@ -98,6 +101,11 @@ export default class MapLeafletLiveMapComponent extends Component {
98
101
  this.universe.off('fleet-ops.geofence.exited', this._geofenceExitedHandler);
99
102
  this._geofenceExitedHandler = null;
100
103
  }
104
+ if (this._viewportReloadHandler && this.map && typeof this.map.off === 'function') {
105
+ this.map.off('moveend', this._viewportReloadHandler);
106
+ this.map.off('zoomend', this._viewportReloadHandler);
107
+ this._viewportReloadHandler = null;
108
+ }
101
109
  }
102
110
 
103
111
  @action didLoad({ target: map }) {
@@ -106,8 +114,9 @@ export default class MapLeafletLiveMapComponent extends Component {
106
114
  this.trigger('onLoad', ...arguments);
107
115
  this.load.perform();
108
116
  // 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());
117
+ this._viewportReloadHandler = () => this.reloadResourcesInViewport.perform();
118
+ map.on('moveend', this._viewportReloadHandler);
119
+ map.on('zoomend', this._viewportReloadHandler);
111
120
  }
112
121
 
113
122
  /**
@@ -211,13 +220,27 @@ export default class MapLeafletLiveMapComponent extends Component {
211
220
  this.#createZoneContextMenu(zone, layer);
212
221
  }
213
222
 
223
+ @action rebuildMapContextMenu() {
224
+ if (!this.map) {
225
+ return;
226
+ }
227
+
228
+ this.#createMapContextMenu(this.map);
229
+ this.#refreshResourceContextMenus();
230
+ this.#pruneResourceContextMenus();
231
+ }
232
+
233
+ @action syncServiceAreaContextMenus() {
234
+ this.rebuildMapContextMenu();
235
+ }
236
+
214
237
  /** load resources and wait for stuff here and trigger map ready **/
215
238
  @task *load() {
216
239
  try {
217
240
  // Get initial map bounds for spatial filtering (provider-agnostic)
218
241
  const mapBounds = this.mapManager.getBounds?.() ?? (this.map ? this.map.getBounds() : null);
219
242
  const bounds = this.#serializeBoundsForRequest(mapBounds);
220
- const params = bounds ? { bounds } : {};
243
+ const params = this.#liveViewportRequestParams(bounds);
221
244
 
222
245
  const data = yield all([
223
246
  this.loadResource.perform('routes'),
@@ -242,12 +265,19 @@ export default class MapLeafletLiveMapComponent extends Component {
242
265
  if (this.isViewportReloadSuspended) {
243
266
  return;
244
267
  }
268
+
269
+ yield timeout(this.liveViewportReloadDebounceMs);
270
+
271
+ if (this.isViewportReloadSuspended) {
272
+ return;
273
+ }
274
+
245
275
  // Get current map bounds (provider-agnostic)
246
276
  const rawBounds = this.mapManager.getBounds?.() ?? this.map?.getBounds();
247
277
  if (!rawBounds) return;
248
278
  const bounds = this.#serializeBoundsForRequest(rawBounds);
249
279
  if (!bounds) return;
250
- const params = { bounds };
280
+ const params = this.#liveViewportRequestParams(bounds);
251
281
 
252
282
  // Reload spatially-filtered resources (drivers, vehicles, places)
253
283
  // Orders, routes, and service-areas are not spatially filtered
@@ -445,6 +475,10 @@ export default class MapLeafletLiveMapComponent extends Component {
445
475
  return null;
446
476
  }
447
477
 
478
+ #liveViewportRequestParams(bounds) {
479
+ return bounds ? { bounds, limit: this.liveViewportResourceLimit } : { limit: this.liveViewportResourceLimit };
480
+ }
481
+
448
482
  #setResourceLayer(model, layer, options = {}) {
449
483
  const { hidden = false } = options;
450
484
  const type = getModelName(model);
@@ -462,6 +496,7 @@ export default class MapLeafletLiveMapComponent extends Component {
462
496
  }
463
497
 
464
498
  #createMapContextMenu(map) {
499
+ const serviceAreas = Array.from(this.serviceAreaActions.serviceAreas ?? []);
465
500
  const items = [
466
501
  {
467
502
  text: this.intl.t('live-map.show-coordinates'),
@@ -494,10 +529,10 @@ export default class MapLeafletLiveMapComponent extends Component {
494
529
  callback: () => this.geofence.createServiceArea(),
495
530
  index: 5,
496
531
  },
497
- this.serviceAreaActions.serviceAreas.length ? { separator: true } : null,
498
- ...this.serviceAreaActions.serviceAreas.map((serviceArea, i) => {
532
+ serviceAreas.length ? { separator: true } : null,
533
+ ...serviceAreas.map((serviceArea, i) => {
499
534
  return {
500
- text: this.intl.t('live-map.focus-service', { serviceName: serviceArea.name }),
535
+ text: this.intl.t('live-map.focus-service', { serviceName: this.#resourceDisplayName(serviceArea) }),
501
536
  callback: () => this.geofence.focusServiceArea(serviceArea),
502
537
  index: 6 + i,
503
538
  };
@@ -511,71 +546,200 @@ export default class MapLeafletLiveMapComponent extends Component {
511
546
  }
512
547
 
513
548
  #createZoneContextMenu(zone, layer) {
549
+ const zoneName = this.#resourceDisplayName(zone);
514
550
  let items = [
515
551
  {
516
552
  separator: true,
517
553
  },
518
554
  {
519
- text: this.intl.t('live-map.edit-zone', { zoneName: zone.name }),
555
+ text: this.intl.t('live-map.edit-zone', { zoneName }),
520
556
  callback: () => this.zoneActions.modal.edit(zone),
521
557
  },
522
558
  {
523
- text: this.intl.t('live-map.delete-zone', { zoneName: zone.name }),
524
- callback: () => this.zoneActions.delete(zone),
559
+ text: this.intl.t('live-map.delete-zone', { zoneName }),
560
+ callback: () => this.#deleteZone(zone),
525
561
  },
526
562
  ];
527
563
 
528
564
  if (!this.mapManager.isGoogleMaps) {
529
565
  items.splice(2, 0, {
530
- text: this.intl.t('live-map.edit-boundaries', { resource: zone.name }),
566
+ text: this.intl.t('live-map.edit-boundaries', { resource: zoneName }),
531
567
  callback: () => this.geofence.editZone(zone),
532
568
  });
533
569
  }
534
570
 
535
571
  // create contextmenu registry
536
- const contextmenuRegistry = this.leafletContextmenuManager.createContextMenu(`zone:${zone.public_id}`, layer, items, { zone });
572
+ const contextmenuRegistry = this.leafletContextmenuManager.createContextMenu(this.#resourceContextKey('zone', zone), layer, items, { zone });
537
573
  this.universe.trigger('fleet-ops:contextmenu:zone:created', contextmenuRegistry, this.leafletContextmenuManager);
538
574
 
539
575
  return contextmenuRegistry;
540
576
  }
541
577
 
542
578
  #createServiceAreaContextMenu(serviceArea, layer) {
579
+ const serviceName = this.#resourceDisplayName(serviceArea);
543
580
  let items = [
544
581
  {
545
582
  separator: true,
546
583
  },
547
584
  {
548
- text: this.intl.t('live-map.blur-service', { serviceName: serviceArea.name }),
585
+ text: this.intl.t('live-map.blur-service', { serviceName }),
549
586
  callback: () => this.geofence.blurServiceArea(serviceArea),
550
587
  },
551
588
  {
552
- text: this.intl.t('live-map.create-zone', { serviceName: serviceArea.name }),
589
+ text: this.intl.t('live-map.create-zone', { serviceName }),
553
590
  callback: () => this.geofence.createZone(serviceArea),
554
591
  },
555
592
  {
556
- text: this.intl.t('live-map.edit-service', { serviceName: serviceArea.name }),
593
+ text: this.intl.t('live-map.edit-service', { serviceName }),
557
594
  callback: () => this.serviceAreaActions.modal.edit(serviceArea),
558
595
  },
559
596
  {
560
- text: this.intl.t('live-map.delete-service', { serviceName: serviceArea.name }),
561
- callback: () => this.serviceAreaActions.delete(serviceArea),
597
+ text: this.intl.t('live-map.delete-service', { serviceName }),
598
+ callback: () => this.#deleteServiceArea(serviceArea),
562
599
  },
563
600
  ];
564
601
 
565
602
  if (!this.mapManager.isGoogleMaps) {
566
603
  items.splice(4, 0, {
567
- text: this.intl.t('live-map.edit-boundaries', { resource: serviceArea.name }),
604
+ text: this.intl.t('live-map.edit-boundaries', { resource: serviceName }),
568
605
  callback: () => this.geofence.editServiceArea(serviceArea),
569
606
  });
570
607
  }
571
608
 
572
609
  // create contextmenu registry
573
- const contextmenuRegistry = this.leafletContextmenuManager.createContextMenu(`service-area:${serviceArea.public_id}`, layer, items, { serviceArea });
610
+ const contextmenuRegistry = this.leafletContextmenuManager.createContextMenu(this.#resourceContextKey('service-area', serviceArea), layer, items, { serviceArea });
574
611
  this.universe.trigger('fleet-ops:contextmenu:service-area:created', contextmenuRegistry, this.leafletContextmenuManager);
575
612
 
576
613
  return contextmenuRegistry;
577
614
  }
578
615
 
616
+ #resourceContextKey(type, resource) {
617
+ return `${type}:${resource?.public_id ?? resource?.id}`;
618
+ }
619
+
620
+ #resourceDisplayName(resource) {
621
+ return resource?.get?.('name') || resource?.name || resource?.display_name || resource?.public_id || resource?.id || '';
622
+ }
623
+
624
+ #deleteServiceArea(serviceArea) {
625
+ const zones = Array.from(serviceArea?.zones ?? []);
626
+
627
+ return this.serviceAreaActions.delete(
628
+ serviceArea,
629
+ {},
630
+ {
631
+ callback: () => {
632
+ this.#removeServiceAreaFromMapState(serviceArea, zones);
633
+ this.rebuildMapContextMenu();
634
+ },
635
+ }
636
+ );
637
+ }
638
+
639
+ #deleteZone(zone) {
640
+ return this.zoneActions.delete(
641
+ zone,
642
+ {},
643
+ {
644
+ callback: () => {
645
+ this.#removeZoneFromMapState(zone);
646
+ this.rebuildMapContextMenu();
647
+ },
648
+ }
649
+ );
650
+ }
651
+
652
+ #removeServiceAreaFromMapState(serviceArea, zones = []) {
653
+ const serviceAreaId = serviceArea?.id;
654
+ const serviceAreaKey = this.#resourceContextKey('service-area', serviceArea);
655
+
656
+ zones.forEach((zone) => this.#removeZoneFromMapState(zone));
657
+ this.#removeOverlay(serviceArea);
658
+ this.leafletContextmenuManager.removeContextMenu(serviceAreaKey);
659
+
660
+ this.serviceAreaActions.serviceAreas = Array.from(this.serviceAreaActions.serviceAreas ?? []).filter((candidate) => candidate?.id !== serviceAreaId);
661
+ }
662
+
663
+ #removeZoneFromMapState(zone) {
664
+ const zoneId = zone?.id;
665
+ const zoneKey = this.#resourceContextKey('zone', zone);
666
+
667
+ this.#removeOverlay(zone);
668
+ this.leafletContextmenuManager.removeContextMenu(zoneKey);
669
+
670
+ for (const serviceArea of Array.from(this.serviceAreaActions.serviceAreas ?? [])) {
671
+ const zones = serviceArea?.zones;
672
+ if (!zones) {
673
+ continue;
674
+ }
675
+
676
+ const currentZones = Array.from(zones);
677
+ const nextZones = currentZones.filter((candidate) => candidate?.id !== zoneId);
678
+ if (nextZones.length !== currentZones.length) {
679
+ if (typeof zones.removeObject === 'function') {
680
+ zones.removeObject(zone);
681
+ } else {
682
+ serviceArea.set?.('zones', nextZones);
683
+ }
684
+ break;
685
+ }
686
+ }
687
+
688
+ this.serviceAreaActions.serviceAreas = Array.from(this.serviceAreaActions.serviceAreas ?? []);
689
+ }
690
+
691
+ #removeOverlay(resource) {
692
+ if (!resource?.id) {
693
+ return;
694
+ }
695
+
696
+ this.mapManager.removeOverlay?.(resource.id);
697
+
698
+ const layer = resource?.leafletLayer;
699
+ if (layer) {
700
+ this.mapManager.removeLayer?.(layer);
701
+ }
702
+ }
703
+
704
+ #refreshResourceContextMenus() {
705
+ for (const serviceArea of Array.from(this.serviceAreaActions.serviceAreas ?? [])) {
706
+ const serviceAreaLayer = this.mapManager.getOverlay(serviceArea?.id) ?? serviceArea?.leafletLayer;
707
+ if (serviceAreaLayer) {
708
+ this.#createServiceAreaContextMenu(serviceArea, serviceAreaLayer);
709
+ }
710
+
711
+ for (const zone of Array.from(serviceArea?.zones ?? [])) {
712
+ const zoneLayer = this.mapManager.getOverlay(zone?.id) ?? zone?.leafletLayer;
713
+ if (zoneLayer) {
714
+ this.#createZoneContextMenu(zone, zoneLayer);
715
+ }
716
+ }
717
+ }
718
+ }
719
+
720
+ #pruneResourceContextMenus() {
721
+ const activeServiceAreaKeys = new Set();
722
+ const activeZoneKeys = new Set();
723
+
724
+ for (const serviceArea of Array.from(this.serviceAreaActions.serviceAreas ?? [])) {
725
+ activeServiceAreaKeys.add(this.#resourceContextKey('service-area', serviceArea));
726
+
727
+ for (const zone of Array.from(serviceArea?.zones ?? [])) {
728
+ activeZoneKeys.add(this.#resourceContextKey('zone', zone));
729
+ }
730
+ }
731
+
732
+ for (const registry of Object.values(this.leafletContextmenuManager.contextMenuRegistry ?? {})) {
733
+ if (registry?.serviceArea && !activeServiceAreaKeys.has(this.#resourceContextKey('service-area', registry.serviceArea))) {
734
+ this.leafletContextmenuManager.removeContextMenu(this.#resourceContextKey('service-area', registry.serviceArea));
735
+ }
736
+
737
+ if (registry?.zone && !activeZoneKeys.has(this.#resourceContextKey('zone', registry.zone))) {
738
+ this.leafletContextmenuManager.removeContextMenu(this.#resourceContextKey('zone', registry.zone));
739
+ }
740
+ }
741
+ }
742
+
579
743
  #createDriverContextMenu(driver, layer) {
580
744
  let items = [
581
745
  {
@@ -0,0 +1,46 @@
1
+ <Modal::Default @modalIsOpened={{@modalIsOpened}} @options={{@options}} @confirm={{perform this.confirmSelection}} @decline={{@onDecline}}>
2
+ <div class="modal-body-container">
3
+ <div class="space-y-3">
4
+ {{#each this.waypointOptions as |option|}}
5
+ <button
6
+ type="button"
7
+ class="w-full rounded-md border px-4 py-3 text-left shadow-sm transition {{if option.isCurrent 'border-blue-500 bg-blue-50 dark:border-blue-600 dark:bg-blue-900' (if option.isSelected 'border-green-500 bg-green-50 dark:border-green-700 dark:bg-green-900' 'border-gray-200 bg-white hover:border-blue-300 hover:bg-blue-50 dark:border-gray-700 dark:bg-gray-800 dark:hover:border-blue-700 dark:hover:bg-gray-900')}}"
8
+ disabled={{this.confirmSelection.isRunning}}
9
+ {{on "click" (fn this.selectOption option)}}
10
+ >
11
+ <div class="flex items-start gap-3">
12
+ <div
13
+ class="flex h-7 w-7 flex-shrink-0 items-center justify-center rounded-full text-xs font-bold {{if option.isCurrent 'bg-blue-600 text-white' 'bg-gray-200 text-gray-800 dark:bg-gray-700 dark:text-gray-100'}}"
14
+ >
15
+ {{option.label}}
16
+ </div>
17
+ <div class="min-w-0 flex-1">
18
+ <div class="flex items-center justify-between gap-2">
19
+ <div class="truncate text-sm font-semibold dark:text-gray-100">{{option.title}} - {{or option.waypoint.name option.waypoint.id}}</div>
20
+ {{#if option.isCurrent}}
21
+ <Badge @status="info" class="uppercase">Current</Badge>
22
+ {{else if option.isSelected}}
23
+ <Badge @status="success" class="uppercase">Selected</Badge>
24
+ {{else if option.waypoint.complete}}
25
+ <Badge @status="completed" class="uppercase">Complete</Badge>
26
+ {{else if option.waypoint.status_code}}
27
+ <Badge @status={{dasherize option.waypoint.status_code}} class="uppercase">{{option.waypoint.status_code}}</Badge>
28
+ {{else if option.waypoint.status}}
29
+ <Badge @status={{dasherize option.waypoint.status}} class="uppercase">{{option.waypoint.status}}</Badge>
30
+ {{/if}}
31
+ </div>
32
+ <div class="mt-1 truncate text-xs text-gray-600 dark:text-gray-300">{{or option.waypoint.address option.waypoint.street1}}</div>
33
+ {{#if option.tracking}}
34
+ <div class="mt-1 text-xs font-mono text-gray-500 dark:text-gray-400">{{option.tracking}}</div>
35
+ {{/if}}
36
+ </div>
37
+ </div>
38
+ </button>
39
+ {{else}}
40
+ <div class="rounded-md border border-dashed border-gray-300 p-6 text-center text-sm text-gray-500 dark:border-gray-700 dark:text-gray-400">
41
+ No waypoint destinations are available.
42
+ </div>
43
+ {{/each}}
44
+ </div>
45
+ </div>
46
+ </Modal::Default>
@@ -0,0 +1,106 @@
1
+ import Component from '@glimmer/component';
2
+ import { tracked } from '@glimmer/tracking';
3
+ import { inject as service } from '@ember/service';
4
+ import { action } from '@ember/object';
5
+ import { task } from 'ember-concurrency';
6
+ import { buildServiceStopsFromPayload, describeRoutePoint } from '../../utils/route-visualization';
7
+ import { colorForId } from '../../utils/route-colors';
8
+
9
+ export default class ModalsOrderChangeDestinationComponent extends Component {
10
+ @service fetch;
11
+ @service notifications;
12
+ @tracked selectedOption;
13
+
14
+ get order() {
15
+ return this.args.options?.order;
16
+ }
17
+
18
+ get waypoints() {
19
+ const waypoints = this.order?.payload?.waypoints;
20
+ if (typeof waypoints?.toArray === 'function') {
21
+ return waypoints.toArray();
22
+ }
23
+
24
+ return Array.isArray(waypoints) ? waypoints : Array.from(waypoints ?? []);
25
+ }
26
+
27
+ get routeColor() {
28
+ return colorForId(this.order?.public_id ?? this.order?.id ?? 'order-route');
29
+ }
30
+
31
+ get serviceStops() {
32
+ return buildServiceStopsFromPayload(this.order?.payload);
33
+ }
34
+
35
+ get currentWaypointId() {
36
+ const activeStop = this.order?.tracker_data?.active_stop;
37
+ const payload = this.order?.payload;
38
+
39
+ return activeStop?.uuid ?? activeStop?.public_id ?? payload?.current_waypoint_uuid ?? payload?.current_waypoint;
40
+ }
41
+
42
+ get waypointOptions() {
43
+ return this.serviceStops.map((stop, index) => ({
44
+ waypoint: stop.place,
45
+ stop,
46
+ index,
47
+ id: this.waypointId(stop.place),
48
+ isCurrent: this.isCurrentWaypoint(stop.place),
49
+ isSelected: this.isSelectedStop(stop),
50
+ label: describeRoutePoint(stop, this.routeColor).label,
51
+ title: describeRoutePoint(stop, this.routeColor).title,
52
+ tracking: this.trackingForStop(stop),
53
+ }));
54
+ }
55
+
56
+ waypointId(waypoint) {
57
+ return waypoint?.id ?? waypoint?.public_id ?? waypoint?.uuid;
58
+ }
59
+
60
+ isCurrentWaypoint(waypoint) {
61
+ return [waypoint?.id, waypoint?.uuid, waypoint?.public_id, waypoint?.waypoint_public_id].includes(this.currentWaypointId);
62
+ }
63
+
64
+ isSelectedStop(stop) {
65
+ const selectedPlace = this.selectedOption?.waypoint;
66
+
67
+ return [selectedPlace?.id, selectedPlace?.uuid, selectedPlace?.public_id, selectedPlace?.waypoint_public_id].includes(this.waypointId(stop?.place));
68
+ }
69
+
70
+ trackingForStop(stop) {
71
+ return stop?.place?.tracking ?? stop?.place?.tracking_number ?? stop?.trackingNumberUuid ?? this.order?.tracking ?? this.order?.tracking_number;
72
+ }
73
+
74
+ @action selectOption(option) {
75
+ if (!option?.isCurrent) {
76
+ this.selectedOption = option;
77
+ }
78
+ }
79
+
80
+ @task *confirmSelection(modal) {
81
+ const option = this.selectedOption;
82
+ const waypoint = option?.waypoint;
83
+ const waypointId = this.waypointId(waypoint);
84
+ if (!waypointId) {
85
+ modal.stopLoading();
86
+ return this.notifications.warning('Select a destination first.');
87
+ }
88
+
89
+ modal.startLoading();
90
+
91
+ try {
92
+ const order = yield this.fetch.patch(`orders/set-destination/${this.order.id}/${waypointId}`);
93
+ this.notifications.success(`Current destination changed to ${waypoint.name ?? waypoint.address ?? waypointId}.`);
94
+
95
+ if (typeof this.args.options?.onChange === 'function') {
96
+ yield this.args.options.onChange(order, waypoint);
97
+ }
98
+
99
+ modal.done();
100
+ } catch (error) {
101
+ this.notifications.serverError(error);
102
+ } finally {
103
+ modal.stopLoading();
104
+ }
105
+ }
106
+ }
@@ -0,0 +1,28 @@
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-gray-200 bg-gray-50 px-4 py-3 dark:border-gray-700 dark:bg-gray-800">
4
+ <div class="text-xs font-semibold uppercase text-gray-500 dark:text-gray-400">Stop</div>
5
+ <div class="mt-1 text-sm font-semibold text-gray-900 dark:text-gray-100">{{this.stopName}}</div>
6
+ <div class="mt-1 text-xs text-gray-500 dark:text-gray-400">{{or this.place.address this.place.street1}}</div>
7
+ </div>
8
+
9
+ {{#if this.loadActivity.isRunning}}
10
+ <div class="flex items-center justify-center p-8 text-sm text-gray-500 dark:text-gray-400">
11
+ <FaIcon @icon="spinner" @spin={{true}} class="mr-2" />
12
+ Loading activity...
13
+ </div>
14
+ {{else if this.trackingNumberUuid}}
15
+ <Order::ActivityList @activity={{this.activity}} @resource={{hash status=this.stop.status}} />
16
+ {{else}}
17
+ <div class="min-h-24 pb-4">
18
+ <div class="flex items-center justify-center rounded-xl border border-dashed border-gray-300 bg-gray-50 p-6 dark:border-gray-600 dark:bg-gray-800">
19
+ <div class="text-center">
20
+ <FaIcon @icon="layer-group" @size="2x" class="mb-2 text-gray-400" />
21
+ <div class="text-sm font-medium text-gray-600 dark:text-gray-400">No stop activity</div>
22
+ <div class="mt-1 text-xs text-gray-500 dark:text-gray-500">This stop does not have a separate activity stream.</div>
23
+ </div>
24
+ </div>
25
+ </div>
26
+ {{/if}}
27
+ </div>
28
+ </Modal::Default>
@@ -0,0 +1,52 @@
1
+ import Component from '@glimmer/component';
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 ModalsStopActivityComponent extends Component {
7
+ @service store;
8
+ @service notifications;
9
+ @tracked activity = [];
10
+
11
+ constructor() {
12
+ super(...arguments);
13
+ this.loadActivity.perform();
14
+ }
15
+
16
+ get stop() {
17
+ return this.args.options?.stop;
18
+ }
19
+
20
+ get place() {
21
+ return this.stop?.place ?? this.stop;
22
+ }
23
+
24
+ get order() {
25
+ return this.args.options?.order;
26
+ }
27
+
28
+ get stopName() {
29
+ return this.place?.name ?? this.place?.address ?? this.place?.id;
30
+ }
31
+
32
+ get trackingNumberUuid() {
33
+ return this.stop?.trackingNumberUuid ?? this.stop?.tracking_number_uuid ?? this.stop?.tracking_number?.uuid ?? this.place?.tracking_number_uuid ?? this.place?.tracking_number?.uuid;
34
+ }
35
+
36
+ @task *loadActivity() {
37
+ if (!this.trackingNumberUuid) {
38
+ this.activity = [];
39
+ return [];
40
+ }
41
+
42
+ try {
43
+ const activity = yield this.store.query('tracking-status', {
44
+ tracking_number_uuid: this.trackingNumberUuid,
45
+ });
46
+ this.activity = activity.toArray();
47
+ return activity;
48
+ } catch (error) {
49
+ this.notifications.serverError(error);
50
+ }
51
+ }
52
+ }
@@ -16,6 +16,23 @@
16
16
  </InfoBlock>
17
17
  {{/if}}
18
18
 
19
+ {{#if this.currentWaypoint}}
20
+ <div class="my-4 rounded-md border border-blue-200 bg-blue-50 px-4 py-3 text-blue-900 dark:border-blue-700 dark:bg-blue-900 dark:text-blue-50">
21
+ <div class="text-xs font-semibold uppercase">Updating activity for</div>
22
+ <div class="mt-1 flex items-start justify-between gap-3">
23
+ <div class="min-w-0">
24
+ <div class="truncate text-sm font-semibold">{{or this.currentWaypoint.name this.currentWaypoint.id}}</div>
25
+ <div class="truncate text-xs">{{or (place-address-html this.currentWaypoint showTitle=false) this.currentWaypoint.address this.currentWaypoint.street1}}</div>
26
+ </div>
27
+ {{#if this.currentWaypoint.status_code}}
28
+ <Badge @status={{dasherize this.currentWaypoint.status_code}} class="uppercase">{{this.currentWaypoint.status_code}}</Badge>
29
+ {{else if this.currentWaypoint.status}}
30
+ <Badge @status={{dasherize this.currentWaypoint.status}} class="uppercase">{{this.currentWaypoint.status}}</Badge>
31
+ {{/if}}
32
+ </div>
33
+ </div>
34
+ {{/if}}
35
+
19
36
  <fieldset class="my-5">
20
37
  <legend class="sr-only">
21
38
  {{t "modals.order-new-activity.activity-options"}}
@@ -36,14 +53,18 @@
36
53
  </div>
37
54
  <label for={{concat "activity_" index}} class="ml-3 flex flex-col cursor-pointer w-full">
38
55
  <span class="block text-sm font-semibold">
39
- {{activity.status}}
56
+ {{or activity._resolved_status activity.status}}
40
57
  </span>
41
58
  <span class="block text-sm">
42
- {{activity.details}}
59
+ {{or activity._resolved_details activity.details}}
43
60
 
44
61
  {{#if (eq activity.code "dispatched")}}
45
62
  <InfoBlock @type="warning" @size="xs" @icon="exclamation-triangle" @text={{t "modals.order-new-activity.dispatch"}} class="mt-1" />
46
63
  {{/if}}
64
+
65
+ {{#if activity.require_pod}}
66
+ <InfoBlock @type="warning" @size="xs" @icon="camera" @text="Requires photo proof of delivery" class="mt-1" />
67
+ {{/if}}
47
68
  </span>
48
69
  </label>
49
70
  <div>
@@ -88,5 +109,26 @@
88
109
  </div>
89
110
  </div>
90
111
  </fieldset>
112
+
113
+ {{#if this.selectedActivityRequiresPod}}
114
+ <div class="mb-5 rounded-md border border-yellow-300 bg-yellow-50 p-4 dark:border-yellow-700 dark:bg-yellow-900">
115
+ <div class="mb-2 text-sm font-semibold text-yellow-900 dark:text-yellow-50">Photo proof of delivery required</div>
116
+ <p class="mb-3 text-xs text-yellow-800 dark:text-yellow-100">Upload a delivery photo before submitting this activity update.</p>
117
+ <FileUpload @name="photos[]" @accept={{join "," this.acceptedProofFileTypes}} @multiple={{false}} @onFileAdded={{this.setProofFile}}>
118
+ <a tabindex={{0}} class="btn btn-magic cursor-pointer">
119
+ <FaIcon @icon="camera" class="mr-1" />
120
+ Select photo
121
+ </a>
122
+ </FileUpload>
123
+ {{#if this.proofFiles}}
124
+ {{#each this.proofFiles as |file|}}
125
+ <div class="mt-3 flex items-center justify-between rounded-md border border-yellow-400 bg-white px-3 py-2 text-xs text-yellow-900 dark:bg-yellow-950 dark:text-yellow-50">
126
+ <span class="truncate">{{file.name}}</span>
127
+ <span class="font-semibold">{{round file.progress}}%</span>
128
+ </div>
129
+ {{/each}}
130
+ {{/if}}
131
+ </div>
132
+ {{/if}}
91
133
  </div>
92
- </Modal::Default>
134
+ </Modal::Default>