@fleetbase/fleetops-engine 0.6.45 → 0.6.46

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 (144) hide show
  1. package/addon/components/admin/map-settings.hbs +26 -0
  2. package/addon/components/admin/map-settings.js +46 -0
  3. package/addon/components/admin/routing-settings.hbs +11 -0
  4. package/addon/components/cell/order-route-type.hbs +46 -0
  5. package/addon/components/cell/order-route-type.js +90 -0
  6. package/addon/components/cell/place-address.hbs +1 -0
  7. package/addon/components/display-place.hbs +45 -56
  8. package/addon/components/display-place.js +21 -0
  9. package/addon/components/layout/fleet-ops-sidebar/driver-listing.js +5 -5
  10. package/addon/components/layout/fleet-ops-sidebar/fleet-listing.js +5 -5
  11. package/addon/components/layout/fleet-ops-sidebar.js +8 -0
  12. package/addon/components/maintenance-schedule/form.js +2 -1
  13. package/addon/components/map/drawer/driver-listing.js +5 -5
  14. package/addon/components/map/drawer/place-listing.js +5 -5
  15. package/addon/components/map/drawer/position-listing.js +102 -21
  16. package/addon/components/map/drawer/vehicle-listing.js +5 -5
  17. package/addon/components/map/google-live-map.hbs +9 -0
  18. package/addon/components/map/google-live-map.js +445 -0
  19. package/addon/components/map/leaflet-live-map.hbs +186 -142
  20. package/addon/components/map/leaflet-live-map.js +121 -41
  21. package/addon/components/map/toolbar/visibility-control-panel.js +2 -2
  22. package/addon/components/map/toolbar/zones-panel.hbs +9 -9
  23. package/addon/components/map/toolbar.hbs +3 -3
  24. package/addon/components/map/toolbar.js +9 -1
  25. package/addon/components/modals/map-layer-form.hbs +132 -63
  26. package/addon/components/modals/orchestrator-import.hbs +4 -11
  27. package/addon/components/modals/orchestrator-import.js +17 -1
  28. package/addon/components/modals/update-order-activity.hbs +1 -1
  29. package/addon/components/orchestrator/order-pool.js +3 -2
  30. package/addon/components/orchestrator-workbench.js +29 -41
  31. package/addon/components/order/details/detail.js +2 -2
  32. package/addon/components/order/details/payload.hbs +5 -5
  33. package/addon/components/order/details/payload.js +18 -1
  34. package/addon/components/order/details/route.hbs +2 -2
  35. package/addon/components/order/details/route.js +76 -3
  36. package/addon/components/order/form/payload.hbs +33 -0
  37. package/addon/components/order/form/payload.js +1 -0
  38. package/addon/components/order/form/route.hbs +8 -8
  39. package/addon/components/order/form/route.js +78 -18
  40. package/addon/components/order/form.js +2 -2
  41. package/addon/components/order/route-editor.hbs +209 -147
  42. package/addon/components/order/route-editor.js +143 -102
  43. package/addon/components/place/address.hbs +1 -0
  44. package/addon/components/place/address.js +12 -0
  45. package/addon/components/route-list.hbs +43 -55
  46. package/addon/components/route-list.js +47 -0
  47. package/addon/components/route-optimization-wizard-panel.js +3 -4
  48. package/addon/components/vehicle/card.hbs +4 -4
  49. package/addon/controllers/maintenance/schedules/index.js +2 -1
  50. package/addon/controllers/operations/orders/index/details.js +110 -37
  51. package/addon/controllers/operations/orders/index/new.js +1 -1
  52. package/addon/controllers/operations/orders/index.js +8 -1
  53. package/addon/controllers/settings/map.js +97 -0
  54. package/addon/controllers/settings/routing.js +47 -12
  55. package/addon/extension.js +11 -0
  56. package/addon/helpers/place-address-html.js +6 -0
  57. package/addon/instance-initializers/register-google-routes.js +17 -0
  58. package/addon/instance-initializers/register-osrm.js +5 -0
  59. package/addon/routes/application.js +6 -0
  60. package/addon/routes/operations/orchestrator.js +13 -0
  61. package/addon/routes/operations/orders/index/details.js +29 -22
  62. package/addon/routes/operations/orders/index/new.js +9 -0
  63. package/addon/routes/operations/orders/index.js +0 -1
  64. package/addon/routes/settings/map.js +3 -0
  65. package/addon/routes.js +1 -0
  66. package/addon/services/entity-actions.js +1 -1
  67. package/addon/services/geofence-event-bus.js +2 -1
  68. package/addon/services/geofence.js +204 -168
  69. package/addon/services/google-routes.js +300 -0
  70. package/addon/services/leaflet-draw-restriction.js +2 -1
  71. package/addon/services/leaflet-map-manager.js +17 -0
  72. package/addon/services/map-adapter/google.js +2205 -0
  73. package/addon/services/map-adapter/leaflet.js +1042 -0
  74. package/addon/services/map-adapter-interface.js +551 -0
  75. package/addon/services/map-manager.js +768 -0
  76. package/addon/services/map-settings.js +75 -0
  77. package/addon/services/movement-tracker.js +83 -32
  78. package/addon/services/order-actions.js +11 -1
  79. package/addon/services/osrm.js +79 -6
  80. package/addon/services/position-playback.js +135 -62
  81. package/addon/services/route-engine.js +83 -0
  82. package/addon/services/service-areas.js +8 -1
  83. package/addon/styles/fleetops-engine.css +555 -0
  84. package/addon/templates/management/vehicles/index.hbs +0 -1
  85. package/addon/templates/operations/orders/index/details.hbs +0 -1
  86. package/addon/templates/settings/map.hbs +53 -0
  87. package/addon/templates/settings/routing.hbs +15 -4
  88. package/addon/utils/fleet-ops-options.js +1 -0
  89. package/addon/utils/has-valid-coordinates.js +6 -0
  90. package/addon/utils/map-provider-options.js +6 -0
  91. package/addon/utils/order-route-editing.js +86 -0
  92. package/addon/utils/order-route-summary.js +20 -0
  93. package/addon/utils/place-address-html.js +41 -0
  94. package/addon/utils/route-colors.js +43 -12
  95. package/addon/utils/route-visualization.js +93 -0
  96. package/addon/utils/vendor-integration.js +2 -1
  97. package/app/components/admin/map-settings.js +1 -0
  98. package/app/components/admin/routing-settings.js +1 -0
  99. package/app/components/cell/order-route-type.js +1 -0
  100. package/app/components/cell/place-address.js +1 -0
  101. package/app/components/map/google-live-map.js +1 -0
  102. package/app/components/place/address.js +1 -0
  103. package/app/controllers/settings/map.js +1 -0
  104. package/app/helpers/place-address-html.js +1 -0
  105. package/app/routes/settings/map.js +1 -0
  106. package/app/services/map-adapter/google.js +1 -0
  107. package/app/services/map-adapter/leaflet.js +1 -0
  108. package/app/services/map-adapter-interface.js +1 -0
  109. package/app/services/map-manager.js +1 -0
  110. package/app/services/map-settings.js +1 -0
  111. package/app/templates/settings/map.js +1 -0
  112. package/app/utils/has-valid-coordinates.js +1 -0
  113. package/app/utils/order-route-editing.js +2 -0
  114. package/app/utils/order-route-summary.js +1 -0
  115. package/app/utils/place-address-html.js +1 -0
  116. package/app/utils/route-colors.js +1 -1
  117. package/app/utils/route-visualization.js +1 -0
  118. package/assets/images/drawing-controls-spritesheet.svg +156 -0
  119. package/composer.json +1 -1
  120. package/extension.json +1 -1
  121. package/package.json +3 -3
  122. package/server/src/Auth/Schemas/FleetOps.php +5 -0
  123. package/server/src/Http/Controllers/Api/v1/OrderController.php +31 -3
  124. package/server/src/Http/Controllers/Api/v1/PayloadController.php +14 -1
  125. package/server/src/Http/Controllers/Api/v1/ServiceAreaController.php +2 -0
  126. package/server/src/Http/Controllers/Api/v1/ServiceQuoteController.php +3 -2
  127. package/server/src/Http/Controllers/Api/v1/ZoneController.php +2 -0
  128. package/server/src/Http/Controllers/Internal/v1/LiveController.php +2 -1
  129. package/server/src/Http/Controllers/Internal/v1/OrchestrationController.php +7 -4
  130. package/server/src/Http/Controllers/Internal/v1/OrderController.php +61 -34
  131. package/server/src/Http/Controllers/Internal/v1/ServiceQuoteController.php +3 -2
  132. package/server/src/Http/Controllers/Internal/v1/SettingController.php +106 -3
  133. package/server/src/Http/Filter/OrderFilter.php +2 -1
  134. package/server/src/Http/Resources/v1/Index/Payload.php +9 -7
  135. package/server/src/Http/Resources/v1/Order.php +20 -1
  136. package/server/src/Models/Order.php +42 -1
  137. package/server/src/Models/OrderConfig.php +15 -0
  138. package/server/src/Models/Payload.php +131 -27
  139. package/server/src/Models/Place.php +182 -48
  140. package/server/src/Models/ServiceRate.php +4 -3
  141. package/server/src/Orchestration/Engines/VroomOrchestrationEngine.php +59 -23
  142. package/server/src/Support/Utils.php +36 -2
  143. package/server/src/routes.php +4 -0
  144. package/translations/en-us.yaml +1937 -1916
@@ -9,9 +9,12 @@ import { camelize, capitalize, dasherize } from '@ember/string';
9
9
  import { singularize, pluralize } from 'ember-inflector';
10
10
  import { all } from 'rsvp';
11
11
  import { task } from 'ember-concurrency';
12
+ import { next } from '@ember/runloop';
12
13
  import getModelName from '@fleetbase/ember-core/utils/get-model-name';
13
14
 
14
15
  export default class MapLeafletLiveMapComponent extends Component {
16
+ @service mapManager;
17
+ @service mapSettings;
15
18
  @service leafletMapManager;
16
19
  @service leafletLayerVisibilityManager;
17
20
  @service leafletContextmenuManager;
@@ -47,6 +50,7 @@ export default class MapLeafletLiveMapComponent extends Component {
47
50
  @tracked drivers = [];
48
51
  @tracked vehicles = [];
49
52
  @tracked places = [];
53
+ _viewportReloadLocks = new Set();
50
54
 
51
55
  constructor() {
52
56
  super(...arguments);
@@ -61,9 +65,9 @@ export default class MapLeafletLiveMapComponent extends Component {
61
65
  this.#updateCoordinatesFromLocation();
62
66
 
63
67
  if (this.currentUser.companyId) {
64
- this.geofenceEventBus.subscribe(this.currentUser.companyId);
68
+ next(this, () => this.geofenceEventBus.subscribe(this.currentUser.companyId));
65
69
  } else {
66
- this._currentUserLoadedHandler = () => this.geofenceEventBus.subscribe(this.currentUser.companyId);
70
+ this._currentUserLoadedHandler = () => next(this, () => this.geofenceEventBus.subscribe(this.currentUser.companyId));
67
71
  this.currentUser.on('user.loaded', this._currentUserLoadedHandler);
68
72
  }
69
73
 
@@ -101,12 +105,35 @@ export default class MapLeafletLiveMapComponent extends Component {
101
105
  this.#createMapContextMenu(map);
102
106
  this.trigger('onLoad', ...arguments);
103
107
  this.load.perform();
104
-
105
108
  // Listen for map move/zoom events to trigger viewport-based resource reload
106
109
  map.on('moveend', () => this.reloadResourcesInViewport.perform());
107
110
  map.on('zoomend', () => this.reloadResourcesInViewport.perform());
108
111
  }
109
112
 
113
+ /**
114
+ * Called when the Google Maps adapter is ready.
115
+ * Mirrors `didLoad` but receives the adapter instance instead of a raw Leaflet map.
116
+ *
117
+ * @param {{ target: * }} event
118
+ */
119
+ @action didLoadGoogleMap({ target: map }) {
120
+ this.#setMapFromAdapter(map);
121
+ this.trigger('onLoad', { target: map });
122
+ this.load.perform();
123
+ }
124
+
125
+ suspendViewportReload(lockId = 'default') {
126
+ this._viewportReloadLocks.add(lockId);
127
+ }
128
+
129
+ resumeViewportReload(lockId = 'default') {
130
+ this._viewportReloadLocks.delete(lockId);
131
+ }
132
+
133
+ get isViewportReloadSuspended() {
134
+ return this._viewportReloadLocks.size > 0;
135
+ }
136
+
110
137
  @action trigger(name, ...rest) {
111
138
  if (typeof this[name] === 'function') {
112
139
  this[name](...rest);
@@ -121,11 +148,13 @@ export default class MapLeafletLiveMapComponent extends Component {
121
148
 
122
149
  @action didCreateDrawControl(drawControl) {
123
150
  this.leafletMapManager.setDrawControl(drawControl);
151
+ this.mapManager.adapter?.setDrawControl?.(drawControl, this.leafletMapManager.drawControlFeatureGroup);
124
152
  this.trigger('onDrawControlCreated', ...arguments);
125
153
  }
126
154
 
127
155
  @action didCreateDrawControlFeatureGroup(featureGroup) {
128
156
  this.leafletMapManager.setDrawControlFeatureGroup(featureGroup);
157
+ this.mapManager.adapter?.setDrawControl?.(this.leafletMapManager.drawControl, featureGroup);
129
158
  this.trigger('onDrawFeatureGroupCreated', ...arguments);
130
159
  }
131
160
 
@@ -139,9 +168,7 @@ export default class MapLeafletLiveMapComponent extends Component {
139
168
  this.driverActions.panel.view(driver, {
140
169
  size: 'xs',
141
170
  onOpen: () => {
142
- this.map.once('moveend', () => {
143
- this.map.panBy([200, 0]);
144
- });
171
+ this.mapManager.once?.('moveend', () => this.mapManager.panBy?.(200, 0)) ?? this.map?.once?.('moveend', () => this.map?.panBy?.([200, 0]));
145
172
  },
146
173
  });
147
174
  }
@@ -156,9 +183,7 @@ export default class MapLeafletLiveMapComponent extends Component {
156
183
  this.vehicleActions.panel.view(vehicle, {
157
184
  size: 'xs',
158
185
  onOpen: () => {
159
- this.map.once('moveend', () => {
160
- this.map.panBy([200, 0]);
161
- });
186
+ this.mapManager.once?.('moveend', () => this.mapManager.panBy?.(200, 0)) ?? this.map?.once?.('moveend', () => this.map?.panBy?.([200, 0]));
162
187
  },
163
188
  });
164
189
  }
@@ -171,9 +196,7 @@ export default class MapLeafletLiveMapComponent extends Component {
171
196
  this.placeActions.panel.view(place, {
172
197
  size: 'xs',
173
198
  onOpen: () => {
174
- this.map.once('moveend', () => {
175
- this.map.panBy([200, 0]);
176
- });
199
+ this.mapManager.once?.('moveend', () => this.mapManager.panBy?.(200, 0)) ?? this.map?.once?.('moveend', () => this.map?.panBy?.([200, 0]));
177
200
  },
178
201
  });
179
202
  }
@@ -191,13 +214,10 @@ export default class MapLeafletLiveMapComponent extends Component {
191
214
  /** load resources and wait for stuff here and trigger map ready **/
192
215
  @task *load() {
193
216
  try {
194
- // Get initial map bounds for spatial filtering
195
- const bounds = this.map ? this.map.getBounds() : null;
196
- const params = bounds
197
- ? {
198
- bounds: [bounds.getSouth(), bounds.getWest(), bounds.getNorth(), bounds.getEast()],
199
- }
200
- : {};
217
+ // Get initial map bounds for spatial filtering (provider-agnostic)
218
+ const mapBounds = this.mapManager.getBounds?.() ?? (this.map ? this.map.getBounds() : null);
219
+ const bounds = this.#serializeBoundsForRequest(mapBounds);
220
+ const params = bounds ? { bounds } : {};
201
221
 
202
222
  const data = yield all([
203
223
  this.loadResource.perform('routes'),
@@ -216,15 +236,18 @@ export default class MapLeafletLiveMapComponent extends Component {
216
236
  }
217
237
 
218
238
  @task({ restartable: true }) *reloadResourcesInViewport() {
219
- if (!this.map) {
239
+ if (!this.map && !this.mapManager.isReady) {
220
240
  return;
221
241
  }
222
-
223
- // Get current map bounds
224
- const bounds = this.map.getBounds();
225
- const params = {
226
- bounds: [bounds.getSouth(), bounds.getWest(), bounds.getNorth(), bounds.getEast()],
227
- };
242
+ if (this.isViewportReloadSuspended) {
243
+ return;
244
+ }
245
+ // Get current map bounds (provider-agnostic)
246
+ const rawBounds = this.mapManager.getBounds?.() ?? this.map?.getBounds();
247
+ if (!rawBounds) return;
248
+ const bounds = this.#serializeBoundsForRequest(rawBounds);
249
+ if (!bounds) return;
250
+ const params = { bounds };
228
251
 
229
252
  // Reload spatially-filtered resources (drivers, vehicles, places)
230
253
  // Orders, routes, and service-areas are not spatially filtered
@@ -272,6 +295,10 @@ export default class MapLeafletLiveMapComponent extends Component {
272
295
  return this.ready === true;
273
296
  }
274
297
 
298
+ get shouldUseGoogleMaps() {
299
+ return this.mapSettings.isGoogleMaps;
300
+ }
301
+
275
302
  /**
276
303
  * Get valid zoom level for map initialization
277
304
  * @returns {number} Valid zoom level between 1-20
@@ -372,46 +399,93 @@ export default class MapLeafletLiveMapComponent extends Component {
372
399
  set(map, 'livemap', this);
373
400
  this.map = map;
374
401
  this.leafletMapManager.setMap(map);
402
+ this.mapManager.setLivemap(this);
403
+ // Also register with the provider-agnostic manager so all services can use it
404
+ this.mapManager.setActiveProvider('leaflet');
405
+ this.mapManager.setMapInstance(map);
406
+ this.universe.trigger('fleet-ops.live-map.loaded', map);
407
+ this.universe.set('component:fleet-ops:live-map', this);
408
+ }
409
+
410
+ /**
411
+ * Called when the Google Maps adapter initialises.
412
+ * Registers the adapter as the active provider so all services
413
+ * route through the provider-agnostic MapManagerService.
414
+ *
415
+ * @param {import('../services/map-manager').default} adapter
416
+ */
417
+ #setMapFromAdapter(map) {
418
+ this.map = map;
419
+ this.mapManager.setLivemap(this);
420
+ this.mapManager.setActiveProvider('google');
375
421
  this.universe.trigger('fleet-ops.live-map.loaded', map);
376
422
  this.universe.set('component:fleet-ops:live-map', this);
377
423
  }
378
424
 
425
+ #serializeBoundsForRequest(rawBounds) {
426
+ if (!rawBounds) return null;
427
+
428
+ if (isArray(rawBounds) && rawBounds.length === 2) {
429
+ const [[south, west], [north, east]] = rawBounds;
430
+ return [south, west, north, east].every(Number.isFinite) ? [south, west, north, east] : null;
431
+ }
432
+
433
+ if (typeof rawBounds.getSouth === 'function') {
434
+ const bounds = [rawBounds.getSouth(), rawBounds.getWest(), rawBounds.getNorth(), rawBounds.getEast()];
435
+ return bounds.every(Number.isFinite) ? bounds : null;
436
+ }
437
+
438
+ if (typeof rawBounds.getSouthWest === 'function') {
439
+ const sw = rawBounds.getSouthWest();
440
+ const ne = rawBounds.getNorthEast();
441
+ const bounds = [sw.lat(), sw.lng(), ne.lat(), ne.lng()];
442
+ return bounds.every(Number.isFinite) ? bounds : null;
443
+ }
444
+
445
+ return null;
446
+ }
447
+
379
448
  #setResourceLayer(model, layer, options = {}) {
380
449
  const { hidden = false } = options;
381
450
  const type = getModelName(model);
382
-
451
+ // Leaflet backward-compat: keep leafletLayer for existing code
383
452
  set(model, 'leafletLayer', layer);
384
453
  set(layer, 'record_id', model.id);
385
454
  set(layer, 'record_type', type);
386
-
387
455
  this.leafletLayerVisibilityManager.registerLayer(pluralize(type), layer, { id: model.id, hidden });
456
+
457
+ if (type === 'service-area' || type === 'zone') {
458
+ this.mapManager.registerPolygon?.(model.id, layer, { type, hidden });
459
+ } else {
460
+ this.mapManager.registerMarker?.(model.id, layer, { type, hidden });
461
+ }
388
462
  }
389
463
 
390
464
  #createMapContextMenu(map) {
391
465
  const items = [
392
466
  {
393
467
  text: this.intl.t('live-map.show-coordinates'),
394
- callback: this.leafletMapManager.showCoordinates,
468
+ callback: this.mapManager.showCoordinates?.bind(this.mapManager) ?? this.leafletMapManager.showCoordinates,
395
469
  index: 0,
396
470
  },
397
471
  {
398
472
  text: this.intl.t('live-map.center-map'),
399
- callback: this.leafletMapManager.centerMap,
473
+ callback: this.mapManager.centerMap?.bind(this.mapManager) ?? this.leafletMapManager.centerMap,
400
474
  index: 1,
401
475
  },
402
476
  {
403
477
  text: this.intl.t('live-map.zoom-in'),
404
- callback: this.leafletMapManager.zoomIn,
478
+ callback: this.mapManager.zoomIn?.bind(this.mapManager) ?? this.leafletMapManager.zoomIn,
405
479
  index: 2,
406
480
  },
407
481
  {
408
482
  text: this.intl.t('live-map.zoom-out'),
409
- callback: this.leafletMapManager.zoomOut,
483
+ callback: this.mapManager.zoomOut?.bind(this.mapManager) ?? this.leafletMapManager.zoomOut,
410
484
  index: 3,
411
485
  },
412
486
  {
413
487
  text: this.intl.t('live-map.toggle-draw-controls'),
414
- callback: this.leafletMapManager.toggleDrawControl,
488
+ callback: this.geofence.toggleDrawControl.bind(this.geofence),
415
489
  index: 4,
416
490
  },
417
491
  { separator: true },
@@ -445,16 +519,19 @@ export default class MapLeafletLiveMapComponent extends Component {
445
519
  text: this.intl.t('live-map.edit-zone', { zoneName: zone.name }),
446
520
  callback: () => this.zoneActions.modal.edit(zone),
447
521
  },
448
- {
449
- text: this.intl.t('live-map.edit-boundaries', { resource: zone.name }),
450
- callback: () => this.geofence.editZone(zone),
451
- },
452
522
  {
453
523
  text: this.intl.t('live-map.delete-zone', { zoneName: zone.name }),
454
524
  callback: () => this.zoneActions.delete(zone),
455
525
  },
456
526
  ];
457
527
 
528
+ if (!this.mapManager.isGoogleMaps) {
529
+ items.splice(2, 0, {
530
+ text: this.intl.t('live-map.edit-boundaries', { resource: zone.name }),
531
+ callback: () => this.geofence.editZone(zone),
532
+ });
533
+ }
534
+
458
535
  // create contextmenu registry
459
536
  const contextmenuRegistry = this.leafletContextmenuManager.createContextMenu(`zone:${zone.public_id}`, layer, items, { zone });
460
537
  this.universe.trigger('fleet-ops:contextmenu:zone:created', contextmenuRegistry, this.leafletContextmenuManager);
@@ -479,16 +556,19 @@ export default class MapLeafletLiveMapComponent extends Component {
479
556
  text: this.intl.t('live-map.edit-service', { serviceName: serviceArea.name }),
480
557
  callback: () => this.serviceAreaActions.modal.edit(serviceArea),
481
558
  },
482
- {
483
- text: this.intl.t('live-map.edit-boundaries', { resource: serviceArea.name }),
484
- callback: () => this.geofence.editServiceArea(serviceArea),
485
- },
486
559
  {
487
560
  text: this.intl.t('live-map.delete-service', { serviceName: serviceArea.name }),
488
561
  callback: () => this.serviceAreaActions.delete(serviceArea),
489
562
  },
490
563
  ];
491
564
 
565
+ if (!this.mapManager.isGoogleMaps) {
566
+ items.splice(4, 0, {
567
+ text: this.intl.t('live-map.edit-boundaries', { resource: serviceArea.name }),
568
+ callback: () => this.geofence.editServiceArea(serviceArea),
569
+ });
570
+ }
571
+
492
572
  // create contextmenu registry
493
573
  const contextmenuRegistry = this.leafletContextmenuManager.createContextMenu(`service-area:${serviceArea.public_id}`, layer, items, { serviceArea });
494
574
  this.universe.trigger('fleet-ops:contextmenu:service-area:created', contextmenuRegistry, this.leafletContextmenuManager);
@@ -6,13 +6,13 @@ import { calculateInPlacePosition } from 'ember-basic-dropdown/utils/calculate-p
6
6
 
7
7
  export default class MapToolbarVisibilityControlPanel extends Component {
8
8
  @service leafletLayerVisibilityManager;
9
- @service leafletMapManager;
9
+ @service mapManager;
10
10
  @tracked areVehiclesHidden = false;
11
11
  @tracked arePlacesHidden = false;
12
12
  @tracked areDriversHidden = false;
13
13
 
14
14
  get livemap() {
15
- return this.leafletMapManager?._livemap ?? {};
15
+ return this.mapManager.livemap ?? {};
16
16
  }
17
17
 
18
18
  get vehicles() {
@@ -6,9 +6,9 @@
6
6
  </div>
7
7
  <div class="next-dd-menu-seperator"></div>
8
8
  <div class="px-1">
9
- <a href="javascript:;" class="next-dd-item" {{on "click" (dropdown-fn @dd this.geofence.createServiceArea)}}>{{t "map.zones-panel.create-service"}}</a>
10
- <a href="javascript:;" class="next-dd-item" {{on "click" (dropdown-fn @dd this.geofence.showAllServiceAreas)}}>{{t "map.zones-panel.show"}}</a>
11
- <a href="javascript:;" class="next-dd-item" {{on "click" (dropdown-fn @dd this.geofence.hideAllServiceAreas)}}>{{t "map.zones-panel.hide"}}</a>
9
+ <a href="javascript:;" class="next-dd-item truncate w-full" {{on "click" (dropdown-fn @dd this.geofence.createServiceArea)}}>{{t "map.zones-panel.create-service"}}</a>
10
+ <a href="javascript:;" class="next-dd-item truncate w-full" {{on "click" (dropdown-fn @dd this.geofence.showAllServiceAreas)}}>{{t "map.zones-panel.show"}}</a>
11
+ <a href="javascript:;" class="next-dd-item truncate w-full" {{on "click" (dropdown-fn @dd this.geofence.hideAllServiceAreas)}}>{{t "map.zones-panel.hide"}}</a>
12
12
  {{#each this.serviceAreaActions.serviceAreas as |serviceArea|}}
13
13
  <BasicDropdownHover @horizontalPosition="right" @calculatePosition={{this.calculatePosition}} @renderInPlace={{true}} as |dd|>
14
14
  <dd.Trigger class="next-dd-item {{if dd.isOpen 'active'}}">
@@ -26,24 +26,24 @@
26
26
  <div class="next-dd-menu-seperator"></div>
27
27
  <div class="px-1">
28
28
  {{#if (is-model-leaflet-layer-hidden serviceArea)}}
29
- <a href="javascript:;" class="next-dd-item" {{on "click" (dropdown-fn dd this.geofence.focusServiceArea serviceArea)}}>
29
+ <a href="javascript:;" class="next-dd-item truncate w-full" {{on "click" (dropdown-fn dd this.geofence.focusServiceArea serviceArea)}}>
30
30
  <span>{{t "map.zones-panel.focus-resource" resource=serviceArea.name}}</span>
31
31
  </a>
32
32
  {{else}}
33
- <a href="javascript:;" class="next-dd-item" {{on "click" (dropdown-fn dd this.geofence.blurServiceArea serviceArea)}}>
33
+ <a href="javascript:;" class="next-dd-item truncate w-full" {{on "click" (dropdown-fn dd this.geofence.blurServiceArea serviceArea)}}>
34
34
  <span>{{t "map.zones-panel.blur-resource" resource=serviceArea.name}}</span>
35
35
  </a>
36
36
  {{/if}}
37
- <a href="javascript:;" class="next-dd-item" {{on "click" (dropdown-fn dd this.geofence.createZone serviceArea)}}>
37
+ <a href="javascript:;" class="next-dd-item truncate w-full" {{on "click" (dropdown-fn dd this.geofence.createZone serviceArea)}}>
38
38
  <span>{{t "map.zones-panel.create-zone-inside" serviceAreaName=serviceArea.name}}</span>
39
39
  </a>
40
- <a href="javascript:;" class="next-dd-item" {{on "click" (dropdown-fn dd this.serviceAreaActions.modal.edit serviceArea)}}>
40
+ <a href="javascript:;" class="next-dd-item truncate w-full" {{on "click" (dropdown-fn dd this.serviceAreaActions.modal.edit serviceArea)}}>
41
41
  <span>{{t "common.edit-resource-name" resourceName=serviceArea.name}}</span>
42
42
  </a>
43
- <a href="javascript:;" class="next-dd-item" {{on "click" (dropdown-fn dd this.geofence.editServiceArea serviceArea)}}>
43
+ <a href="javascript:;" class="next-dd-item truncate w-full" {{on "click" (dropdown-fn dd this.geofence.editServiceArea serviceArea)}}>
44
44
  <span>{{t "map.zones-panel.edit-boundaries" resource=serviceArea.name}}</span>
45
45
  </a>
46
- <a href="javascript:;" class="next-dd-item" {{on "click" (dropdown-fn dd this.serviceAreaActions.delete serviceArea)}}>
46
+ <a href="javascript:;" class="next-dd-item truncate w-full" {{on "click" (dropdown-fn dd this.serviceAreaActions.delete serviceArea)}}>
47
47
  <span>{{t "common.delete-resource-name" resourceName=serviceArea.name}}</span>
48
48
  </a>
49
49
  </div>
@@ -1,12 +1,12 @@
1
1
  <div class="next-map-container-toolbar-container {{if @horizontal 'toolbar-container-horizontal'}} {{if (eq @position 'bottom') 'position-bottom'}}" ...attributes>
2
2
  <div class="next-map-container-toolbar next-map-container-toolbar-zoom-control">
3
3
  <div id="map-toolbar-zoom-in-button-wrapper">
4
- <button type="button" id="map-toolbar-zoom-in-button" class="toolbar-button" {{on "click" this.leafletMapManager.zoomIn}}>
4
+ <button type="button" id="map-toolbar-zoom-in-button" class="toolbar-button" {{on "click" this.zoomIn}}>
5
5
  <FaIcon @icon="plus" @prefix="fas" class="text-gray-100" />
6
6
  </button>
7
7
  </div>
8
8
  <div id="map-toolbar-zoom-out-button-wrapper">
9
- <button type="button" class="toolbar-button" {{on "click" this.leafletMapManager.zoomOut}}>
9
+ <button type="button" class="toolbar-button" {{on "click" this.zoomOut}}>
10
10
  <FaIcon @icon="minus" id="map-toolbar-zoom-out-button" @prefix="fas" class="text-gray-100" />
11
11
  </button>
12
12
  </div>
@@ -87,4 +87,4 @@
87
87
  </Attach::Tooltip>
88
88
  </button>
89
89
  </div>
90
- </div>
90
+ </div>
@@ -6,7 +6,7 @@ import { debug } from '@ember/debug';
6
6
  import { task } from 'ember-concurrency';
7
7
 
8
8
  export default class MapToolbarComponent extends Component {
9
- @service leafletMapManager;
9
+ @service mapManager;
10
10
  @service orderListOverlay;
11
11
  @service mapDrawer;
12
12
  @service globalSearch;
@@ -30,6 +30,14 @@ export default class MapToolbarComponent extends Component {
30
30
  return { style };
31
31
  }
32
32
 
33
+ @action zoomIn() {
34
+ return this.mapManager.zoomIn?.();
35
+ }
36
+
37
+ @action zoomOut() {
38
+ return this.mapManager.zoomOut?.();
39
+ }
40
+
33
41
  @task *getActiveOrderCount() {
34
42
  try {
35
43
  const count = yield this.fetch.get('fleet-ops/metrics', { discover: ['orders_in_progress'] });
@@ -1,67 +1,136 @@
1
1
  <Modal::Default @modalIsOpened={{@modalIsOpened}} @options={{@options}} @confirm={{@onConfirm}} @decline={{@onDecline}}>
2
- <div class="modal-body-container">
3
- <div class="mb-4 border border-gray-700 shadow-sm rounded-md pt-2 px-4">
4
- <InputGroup @name={{t "modals.map-layer-form.layer-type"}} @helpText={{t "modals.map-layer-form.layer-type-text"}}>
5
- <Select
6
- @options={{@options.layerTypes}}
7
- @value={{@options.selectedLayerType}}
8
- @onSelect={{fn (mut @options.selectedLayerType)}}
9
- @placeholder={{t "modals.map-layer-form.select-layer-type"}}
10
- class="w-full"
2
+ <div>
3
+ <ContentPanel @title={{if (eq @options.selectedLayerType "Zone") "Zone Details" "Service Area Details"}} @open={{true}} @wrapperClass="bordered-top">
4
+ <div class="space-y-2">
5
+ <InputGroup @name={{t "modals.map-layer-form.layer-type"}} @helpText={{t "modals.map-layer-form.layer-type-text"}}>
6
+ <Select
7
+ @options={{@options.layerTypes}}
8
+ @value={{@options.selectedLayerType}}
9
+ @onSelect={{fn (mut @options.selectedLayerType)}}
10
+ @placeholder={{t "modals.map-layer-form.select-layer-type"}}
11
+ class="w-full"
12
+ />
13
+ </InputGroup>
14
+
15
+ {{#if (eq @options.selectedLayerType "Zone")}}
16
+ <InputGroup @name={{t "modals.map-layer-form.service-area"}} @helpText={{t "modals.map-layer-form.service-area-text"}}>
17
+ <ModelSelect
18
+ @modelName="service-area"
19
+ @selectedModel={{@options.layerOptions.service_area}}
20
+ @placeholder={{t "modals.map-layer-form.select-service-area"}}
21
+ @triggerClass="form-select form-input"
22
+ @infiniteScroll={{false}}
23
+ @renderInPlace={{true}}
24
+ @onChange={{fn (mut @options.layerOptions.service_area)}}
25
+ as |model|
26
+ >
27
+ {{model.name}}
28
+ </ModelSelect>
29
+ </InputGroup>
30
+ {{/if}}
31
+
32
+ <InputGroup
33
+ @value={{@options.layerOptions.name}}
34
+ @name="{{@options.selectedLayerType}} Name"
35
+ @helpText={{t "modals.map-layer-form.set-name" htmlSafe=true options=@options.selectedLayerType}}
36
+ @required={{true}}
11
37
  />
12
- </InputGroup>
13
- </div>
14
- {{#if (eq @options.selectedLayerType "Zone")}}
15
- <InputGroup @name={{t "modals.map-layer-form.service-area"}} @helpText={{t "modals.map-layer-form.service-area-text"}}>
16
- <ModelSelect
17
- @modelName="service-area"
18
- @selectedModel={{@options.layerOptions.service_area}}
19
- @placeholder={{t "modals.map-layer-form.select-service-area"}}
20
- @triggerClass="form-select form-input"
21
- @infiniteScroll={{false}}
22
- @renderInPlace={{true}}
23
- @onChange={{fn (mut @options.layerOptions.service_area)}}
24
- as |model|
25
- >
26
- {{model.name}}
27
- </ModelSelect>
28
- </InputGroup>
29
- {{/if}}
30
- <InputGroup
31
- @value={{@options.layerOptions.name}}
32
- @name="{{@options.selectedLayerType}} Name"
33
- @helpText={{t "modals.map-layer-form.set-name" htmlSafe=true options=@options.selectedLayerType}}
34
- @required={{true}}
35
- />
36
- {{#if (eq @options.selectedLayerType "Service Area")}}
37
- <InputGroup @name={{t "modals.map-layer-form.service-type"}} @helpText={{t "modals.map-layer-form.service-type-text"}}>
38
- <Select
39
- @options={{@options.serviceAreaTypes}}
40
- @value={{@options.layerOptions.type}}
41
- @onSelect={{fn (mut @options.layerOptions.type)}}
42
- @placeholder={{t "modals.map-layer-form.select-service-type"}}
43
- class="w-full"
38
+
39
+ {{#if (eq @options.selectedLayerType "Service Area")}}
40
+ <InputGroup @name={{t "modals.map-layer-form.service-type"}} @helpText={{t "modals.map-layer-form.service-type-text"}}>
41
+ <Select
42
+ @options={{@options.serviceAreaTypes}}
43
+ @value={{@options.layerOptions.type}}
44
+ @onSelect={{fn (mut @options.layerOptions.type)}}
45
+ @placeholder={{t "modals.map-layer-form.select-service-type"}}
46
+ class="w-full"
47
+ />
48
+ </InputGroup>
49
+ {{/if}}
50
+
51
+ <div class="grid grid-cols-1 lg:grid-cols-2 gap-2">
52
+ <InputGroup
53
+ @type="color"
54
+ @value={{@options.layerOptions.stroke_color}}
55
+ @name="{{@options.selectedLayerType}} Border Color"
56
+ @helpText={{t "modals.map-layer-form.customize-border" htmlSafe=true options=@options.selectedLayerType}}
57
+ class="p-0 border-0 m-0 w-8 h-8 bg-transparent rounded-none"
58
+ />
59
+ <InputGroup
60
+ @type="color"
61
+ @value={{@options.layerOptions.color}}
62
+ @name="{{@options.selectedLayerType}} Color"
63
+ @helpText={{t "modals.map-layer-form.customize-fill-color" htmlSafe=true options=@options.selectedLayerType}}
64
+ class="p-0 border-0 m-0 w-8 h-8 bg-transparent rounded-none"
65
+ />
66
+ </div>
67
+
68
+ {{#if (eq @options.selectedLayerType "Service Area")}}
69
+ <InputGroup @name={{t "modals.map-layer-form.service-area-country"}} @helpText={{t "modals.map-layer-form.service-area-country-text"}}>
70
+ <CountrySelect class="w-full form-input" @value={{@options.layerOptions.country}} @onChange={{fn (mut @options.layerOptions.country)}} />
71
+ </InputGroup>
72
+ {{else}}
73
+ <InputGroup @name={{t "modals.zone-form.description-zone"}} @helpText={{t "modals.zone-form.optionally"}}>
74
+ <Textarea @value={{@options.layerOptions.description}} rows={{3}} type="text" class="w-full form-input" placeholder={{t "modals.zone-form.description-zone"}} />
75
+ </InputGroup>
76
+ {{/if}}
77
+ </div>
78
+ </ContentPanel>
79
+
80
+ <ContentPanel @title="Geofence Triggers" @open={{true}} @wrapperClass="bordered-top">
81
+ <div class="space-y-4">
82
+ <p class="text-sm text-gray-500 dark:text-gray-400">
83
+ Configure when this {{if (eq @options.selectedLayerType "Zone") "zone" "service area"}} fires active geofence events. Entry and exit events are dispatched to webhooks, WebSocket channels, and the notification system whenever a driver crosses the boundary.
84
+ </p>
85
+
86
+ <div class="grid grid-cols-1 gap-4 sm:grid-cols-2">
87
+ <div class="flex items-start space-x-3 p-3 rounded-lg border border-gray-200 dark:border-gray-700">
88
+ <div class="flex-shrink-0 mt-0.5">
89
+ <Toggle
90
+ @isToggled={{@options.layerOptions.trigger_on_entry}}
91
+ @onToggle={{fn (mut @options.layerOptions.trigger_on_entry)}}
92
+ />
93
+ </div>
94
+ <div>
95
+ <p class="text-sm font-medium text-gray-900 dark:text-white">Trigger on Entry</p>
96
+ <p class="text-xs text-gray-500 dark:text-gray-400 mt-0.5">
97
+ Fire a <code class="font-mono">geofence.entered</code> event when a driver enters this {{if (eq @options.selectedLayerType "Zone") "zone" "service area"}}.
98
+ </p>
99
+ </div>
100
+ </div>
101
+
102
+ <div class="flex items-start space-x-3 p-3 rounded-lg border border-gray-200 dark:border-gray-700">
103
+ <div class="flex-shrink-0 mt-0.5">
104
+ <Toggle
105
+ @isToggled={{@options.layerOptions.trigger_on_exit}}
106
+ @onToggle={{fn (mut @options.layerOptions.trigger_on_exit)}}
107
+ />
108
+ </div>
109
+ <div>
110
+ <p class="text-sm font-medium text-gray-900 dark:text-white">Trigger on Exit</p>
111
+ <p class="text-xs text-gray-500 dark:text-gray-400 mt-0.5">
112
+ Fire a <code class="font-mono">geofence.exited</code> event when a driver leaves this {{if (eq @options.selectedLayerType "Zone") "zone" "service area"}}.
113
+ </p>
114
+ </div>
115
+ </div>
116
+ </div>
117
+
118
+ <InputGroup
119
+ @name="Dwell Threshold (minutes)"
120
+ @value={{@options.layerOptions.dwell_threshold_minutes}}
121
+ @type="number"
122
+ @placeholder="e.g. 10"
123
+ @helpText="Fire a geofence.dwelled event after a driver has been inside this boundary for this many minutes. Leave blank to disable dwell tracking."
44
124
  />
45
- </InputGroup>
46
- {{/if}}
47
- <InputGroup
48
- @type="color"
49
- @value={{@options.layerOptions.stroke_color}}
50
- @name="{{@options.selectedLayerType}} Border Color"
51
- @helpText={{t "modals.map-layer-form.customize-border" htmlSafe=true options=@options.selectedLayerType}}
52
- class="p-0 border-0 m-0 w-8 h-8 bg-transparent rounded-none"
53
- />
54
- <InputGroup
55
- @type="color"
56
- @value={{@options.layerOptions.color}}
57
- @name="{{@options.selectedLayerType}} Color"
58
- @helpText={{t "modals.map-layer-form.customize-fill-color" htmlSafe=true options=@options.selectedLayerType}}
59
- class="p-0 border-0 m-0 w-8 h-8 bg-transparent rounded-none"
60
- />
61
- {{#if (eq @options.selectedLayerType "Service Area")}}
62
- <InputGroup @name={{t "modals.map-layer-form.service-area-country"}} @helpText={{t "modals.map-layer-form.service-area-country-text"}}>
63
- <CountrySelect class="w-full form-input" @value={{@options.layerOptions.country}} @onChange={{fn (mut @options.layerOptions.country)}} />
64
- </InputGroup>
65
- {{/if}}
125
+
126
+ <InputGroup
127
+ @name="Speed Limit (km/h)"
128
+ @value={{@options.layerOptions.speed_limit_kmh}}
129
+ @type="number"
130
+ @placeholder="e.g. 50"
131
+ @helpText="Optional speed limit enforced within this boundary. Leave blank for no limit."
132
+ />
133
+ </div>
134
+ </ContentPanel>
66
135
  </div>
67
- </Modal::Default>
136
+ </Modal::Default>