@fleetbase/fleetops-engine 0.6.49 → 0.6.50
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/addon/components/customer/create-order-form.hbs +2 -2
- package/addon/components/customer/form.hbs +2 -2
- package/addon/components/customer/order-form.hbs +2 -2
- package/addon/components/customer/orders.hbs +2 -2
- package/addon/components/display-place.hbs +2 -2
- package/addon/components/map/google-live-map.js +6 -2
- package/addon/components/map/leaflet-live-map.hbs +48 -14
- package/addon/components/map/leaflet-live-map.js +184 -20
- package/addon/components/modals/order-change-destination.hbs +46 -0
- package/addon/components/modals/order-change-destination.js +106 -0
- package/addon/components/modals/stop-activity.hbs +28 -0
- package/addon/components/modals/stop-activity.js +52 -0
- package/addon/components/modals/update-order-activity.hbs +45 -3
- package/addon/components/modals/update-order-activity.js +143 -5
- package/addon/components/orchestrator/resource-panel.hbs +0 -10
- package/addon/components/orchestrator/resource-panel.js +2 -4
- package/addon/components/order/activity-list.hbs +2 -2
- package/addon/components/order/activity-timeline.hbs +31 -29
- package/addon/components/order/details/activity.hbs +3 -3
- package/addon/components/order/details/activity.js +11 -0
- package/addon/components/order/details/documents.js +2 -0
- package/addon/components/order/details/purchase-rate.hbs +2 -2
- package/addon/components/order/details/route.hbs +11 -3
- package/addon/components/order/details/route.js +91 -14
- package/addon/components/order/details/tracking.hbs +101 -72
- package/addon/components/order/details/tracking.js +149 -2
- package/addon/components/order/form/details.hbs +3 -3
- package/addon/components/order/form/documents.js +11 -4
- package/addon/components/order/form/route.hbs +108 -104
- package/addon/components/order/form/route.js +37 -1
- package/addon/components/order/form/service-rate.hbs +2 -2
- package/addon/components/order-config-manager/activity-flow.hbs +4 -1
- package/addon/components/order-config-manager/activity-flow.js +178 -12
- package/addon/components/place/address.js +1 -1
- package/addon/components/route-list.hbs +17 -17
- package/addon/components/route-list.js +42 -10
- package/addon/components/service-rate/details.hbs +41 -0
- package/addon/components/service-rate/form.hbs +118 -0
- package/addon/components/service-rate/form.js +55 -0
- package/addon/components/vehicle/form.js +1 -1
- package/addon/components/vehicle/pill.hbs +1 -1
- package/addon/controllers/management/vehicles/index/new.js +1 -1
- package/addon/controllers/operations/orders/index/details.js +4 -4
- package/addon/controllers/operations/orders/index.js +14 -0
- package/addon/helpers/place-address-html.js +2 -2
- package/addon/routes/operations/service-rates/index.js +1 -1
- package/addon/services/geofence.js +163 -19
- package/addon/services/leaflet-contextmenu-manager.js +27 -0
- package/addon/services/vehicle-actions.js +1 -1
- package/addon/styles/fleetops-engine.css +91 -1
- package/addon/utils/fleet-ops-options.js +2 -1
- package/addon/utils/normalize-order-config-flow.js +41 -0
- package/addon/utils/place-address-html.js +3 -2
- package/addon/utils/route-visualization.js +54 -7
- package/app/components/modals/order-change-destination.js +1 -0
- package/app/components/modals/stop-activity.js +1 -0
- package/composer.json +1 -1
- package/extension.json +1 -1
- package/package.json +3 -3
- package/server/migrations/2026_05_21_000001_add_geographic_rules_to_service_rate_fees_table.php +37 -0
- package/server/migrations/2026_05_25_000001_add_endpoint_tracking_refs_to_payloads_table.php +50 -0
- package/server/migrations/2026_05_27_000001_default_driver_vehicle_statuses_to_available.php +48 -0
- package/server/resources/views/labels/default.php +57 -49
- package/server/resources/views/labels/waypoint-label.php +27 -20
- package/server/seeders/Testing/ConnectivitySeeder.php +1 -0
- package/server/seeders/Testing/FleetSeeder.php +22 -22
- package/server/seeders/Testing/OrdersSeeder.php +6 -22
- package/server/seeders/Testing/VroomOrchestrationSeeder.php +21 -21
- package/server/src/Console/Commands/AuditCustomerUserConflicts.php +75 -0
- package/server/src/Exceptions/CustomerUserConflictException.php +13 -0
- package/server/src/Http/Controllers/Api/v1/ContactController.php +17 -2
- package/server/src/Http/Controllers/Api/v1/CustomerController.php +876 -0
- package/server/src/Http/Controllers/Api/v1/DriverController.php +1 -1
- package/server/src/Http/Controllers/Api/v1/OrderConfigController.php +54 -0
- package/server/src/Http/Controllers/Api/v1/OrderController.php +98 -70
- package/server/src/Http/Controllers/Api/v1/ServiceAreaController.php +2 -2
- package/server/src/Http/Controllers/Api/v1/VehicleController.php +20 -3
- package/server/src/Http/Controllers/Internal/v1/ContactController.php +19 -0
- package/server/src/Http/Controllers/Internal/v1/LiveController.php +101 -66
- package/server/src/Http/Controllers/Internal/v1/OrderController.php +429 -26
- package/server/src/Http/Controllers/Internal/v1/ServiceQuoteController.php +1 -1
- package/server/src/Http/Controllers/Internal/v1/SettingController.php +2 -2
- package/server/src/Http/Controllers/Internal/v1/VehicleController.php +54 -1
- package/server/src/Http/Middleware/AuthenticateCustomerToken.php +34 -0
- package/server/src/Http/Requests/CreateCustomerOrderRequest.php +63 -0
- package/server/src/Http/Requests/CreateCustomerRequest.php +48 -0
- package/server/src/Http/Requests/CreateDriverRequest.php +1 -1
- package/server/src/Http/Requests/CreateVehicleRequest.php +25 -1
- package/server/src/Http/Requests/Internal/CreateDriverRequest.php +1 -1
- package/server/src/Http/Requests/VerifyCreateCustomerRequest.php +29 -0
- package/server/src/Http/Resources/Internal/v1/OrderConfig.php +41 -0
- package/server/src/Http/Resources/v1/Customer.php +79 -0
- package/server/src/Http/Resources/v1/OrderConfig.php +74 -0
- package/server/src/Http/Resources/v1/Payload.php +23 -20
- package/server/src/Http/Resources/v1/ServiceArea.php +33 -19
- package/server/src/Http/Resources/v1/ServiceRateFee.php +7 -0
- package/server/src/Listeners/HandleOrderDispatched.php +2 -2
- package/server/src/Models/Contact.php +92 -15
- package/server/src/Models/Customer.php +64 -0
- package/server/src/Models/Driver.php +12 -3
- package/server/src/Models/Entity.php +2 -2
- package/server/src/Models/Order.php +29 -4
- package/server/src/Models/Payload.php +17 -1
- package/server/src/Models/ServiceRate.php +273 -1
- package/server/src/Models/ServiceRateFee.php +39 -9
- package/server/src/Models/TrackingNumber.php +8 -2
- package/server/src/Models/Vehicle.php +18 -2
- package/server/src/Models/Waypoint.php +2 -2
- package/server/src/Notifications/LateDeparture.php +2 -2
- package/server/src/Notifications/ProlongedStoppage.php +2 -2
- package/server/src/Notifications/RouteDeviation.php +2 -2
- package/server/src/Observers/ContactObserver.php +2 -0
- package/server/src/Observers/ServiceRateObserver.php +6 -6
- package/server/src/Orchestration/Engines/VroomOrchestrationEngine.php +1 -1
- package/server/src/Providers/FleetOpsServiceProvider.php +1 -0
- package/server/src/Support/Analytics/AbstractAnalytics.php +6 -6
- package/server/src/Support/Analytics/FuelEfficiency.php +2 -2
- package/server/src/Support/Analytics/OrdersByStatus.php +1 -1
- package/server/src/Support/Analytics/RevenueTrend.php +0 -1
- package/server/src/Support/Analytics/TopDrivers.php +1 -1
- package/server/src/Support/CustomerAuth.php +98 -0
- package/server/src/Support/LabelPdf.php +52 -0
- package/server/src/Support/Metrics/AbstractMetric.php +4 -4
- package/server/src/Support/ResolvesOrderServiceStops.php +512 -0
- package/server/src/Tracking/TrackingContextBuilder.php +41 -38
- package/server/src/Tracking/TrackingIntelligenceService.php +109 -9
- package/server/src/Tracking/TrackingStop.php +13 -11
- package/server/src/routes.php +43 -9
- package/server/tests/CapacityAllocationEngineTest.php +10 -10
- package/server/tests/CustomerContactIdentitySafetyTest.php +54 -0
- package/server/tests/CustomerEndpointTest.php +199 -0
- package/server/tests/DriverVehicleStatusDefaultTest.php +39 -0
- package/server/tests/LabelPdfRenderingTest.php +23 -0
- package/server/tests/LiveControllerViewportTest.php +60 -0
- package/server/tests/MetricsRegistryTest.php +4 -8
- package/server/tests/OrchestrationPayloadBuilderTest.php +2 -2
- package/server/tests/OrderConfigEndpointTest.php +86 -0
- package/server/tests/OrderFileAttachmentTest.php +20 -0
- package/server/tests/ServiceAreaZoneApiShapeTest.php +34 -0
- package/server/tests/VehicleDriverAssignmentTest.php +18 -0
- package/translations/en-us.yaml +1 -0
- package/server/migrations/2026_05_13_000001_repair_customer_contact_user_types.php +0 -86
|
@@ -335,7 +335,7 @@
|
|
|
335
335
|
{{#each serviceQuote.items as |item|}}
|
|
336
336
|
<tr>
|
|
337
337
|
<td>
|
|
338
|
-
<span class="font-semibold">{{
|
|
338
|
+
<span class="font-semibold">{{item.details}}</span>
|
|
339
339
|
</td>
|
|
340
340
|
<td>
|
|
341
341
|
<span>{{format-currency item.amount serviceQuote.currency}}</span>
|
|
@@ -449,4 +449,4 @@
|
|
|
449
449
|
|
|
450
450
|
<Spacer @height="300px" />
|
|
451
451
|
</Overlay::Body>
|
|
452
|
-
</Overlay>
|
|
452
|
+
</Overlay>
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
@modelName="user"
|
|
7
7
|
@selectedModel={{@resource.user}}
|
|
8
8
|
@placeholder={{t "customer.fields.select-user"}}
|
|
9
|
-
@query={{hash doesnt_have_contact=true}}
|
|
9
|
+
@query={{hash doesnt_have_contact=true is_customer=true}}
|
|
10
10
|
@triggerClass="form-select form-input"
|
|
11
11
|
@infiniteScroll={{false}}
|
|
12
12
|
@renderInPlace={{true}}
|
|
@@ -141,4 +141,4 @@
|
|
|
141
141
|
<RegistryYield @registry="fleet-ops:component:customer:form" as |RegistryComponent|>
|
|
142
142
|
<RegistryComponent @resource={{@resource}} @controller={{@controller}} @permission={{get-write-permission @resource}} />
|
|
143
143
|
</RegistryYield>
|
|
144
|
-
</div>
|
|
144
|
+
</div>
|
|
@@ -351,7 +351,7 @@
|
|
|
351
351
|
{{#each serviceQuote.items as |item|}}
|
|
352
352
|
<tr>
|
|
353
353
|
<td>
|
|
354
|
-
<span class="font-semibold">{{
|
|
354
|
+
<span class="font-semibold">{{item.details}}</span>
|
|
355
355
|
</td>
|
|
356
356
|
<td>
|
|
357
357
|
<span>{{format-currency item.amount serviceQuote.currency}}</span>
|
|
@@ -473,4 +473,4 @@
|
|
|
473
473
|
|
|
474
474
|
<Spacer @height="300px" />
|
|
475
475
|
</Overlay::Body>
|
|
476
|
-
</Overlay>
|
|
476
|
+
</Overlay>
|
|
@@ -274,7 +274,7 @@
|
|
|
274
274
|
{{#each order.purchase_rate.service_quote.items as |item|}}
|
|
275
275
|
<tr>
|
|
276
276
|
<td>
|
|
277
|
-
<span class="font-semibold">{{
|
|
277
|
+
<span class="font-semibold">{{item.details}}</span>
|
|
278
278
|
</td>
|
|
279
279
|
<td>
|
|
280
280
|
<div class="flex flex-row items-center justify-end">{{format-currency
|
|
@@ -320,4 +320,4 @@
|
|
|
320
320
|
</div>
|
|
321
321
|
<div id="customer-portal-content-wormhole"></div>
|
|
322
322
|
{{/let}}
|
|
323
|
-
</div>
|
|
323
|
+
</div>
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
<div class="next-dd-menu-seperator"></div>
|
|
32
32
|
{{#each-in @waypointActions as |actionId action|}}
|
|
33
33
|
<div class="px-1">
|
|
34
|
-
<a href="javascript:;" class="next-dd-item" disabled={{cannot "fleet-ops view order"}} {{on "click" (dropdown-fn dd action.fn this.place)}}>
|
|
34
|
+
<a href="javascript:;" class="next-dd-item" disabled={{cannot "fleet-ops view order"}} {{on "click" (dropdown-fn dd action.fn (or @waypointActionContext this.place))}}>
|
|
35
35
|
{{action.label}}
|
|
36
36
|
</a>
|
|
37
37
|
</div>
|
|
@@ -65,4 +65,4 @@
|
|
|
65
65
|
{{/if}}
|
|
66
66
|
</address>
|
|
67
67
|
{{/if}}
|
|
68
|
-
</div>
|
|
68
|
+
</div>
|
|
@@ -276,15 +276,19 @@ export default class MapGoogleLiveMapComponent extends Component {
|
|
|
276
276
|
}
|
|
277
277
|
|
|
278
278
|
_showServiceAreaContextMenu(serviceArea, event) {
|
|
279
|
-
const items = this.mapManager.getComposedContextMenuItems(
|
|
279
|
+
const items = this.mapManager.getComposedContextMenuItems(this._resourceContextKey('service-area', serviceArea));
|
|
280
280
|
this.mapManager.showContextMenu(event, items);
|
|
281
281
|
}
|
|
282
282
|
|
|
283
283
|
_showZoneContextMenu(zone, event) {
|
|
284
|
-
const items = this.mapManager.getComposedContextMenuItems(
|
|
284
|
+
const items = this.mapManager.getComposedContextMenuItems(this._resourceContextKey('zone', zone));
|
|
285
285
|
this.mapManager.showContextMenu(event, items);
|
|
286
286
|
}
|
|
287
287
|
|
|
288
|
+
_resourceContextKey(type, resource) {
|
|
289
|
+
return `${type}:${resource?.public_id ?? resource?.id}`;
|
|
290
|
+
}
|
|
291
|
+
|
|
288
292
|
_closeAllInfoWindows() {
|
|
289
293
|
this._infoWindows.forEach((iw) => iw.close());
|
|
290
294
|
this.mapManager.closeContextMenu();
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
through the provider-agnostic MapManagerService.
|
|
14
14
|
}}
|
|
15
15
|
|
|
16
|
-
<div class="live-map-component {{@wrapperClass}}">
|
|
16
|
+
<div class="live-map-component {{@wrapperClass}}" {{did-update this.syncServiceAreaContextMenus this.serviceAreaActions.serviceAreas this.serviceAreaActions.serviceAreas.length}}>
|
|
17
17
|
|
|
18
18
|
{{#if this.shouldUseGoogleMaps}}
|
|
19
19
|
{{! ─── Google Maps branch ──────────────────────────────────────────── }}
|
|
@@ -87,7 +87,11 @@
|
|
|
87
87
|
<div class="flex items-center gap-1.5 text-xs font-semibold mb-1.5">
|
|
88
88
|
<span class="inline-block w-2 h-2 rounded-full {{if driver.online 'bg-green-400' 'bg-red-500'}}"></span>
|
|
89
89
|
<span>{{driver.name}}</span>
|
|
90
|
-
<div
|
|
90
|
+
<div
|
|
91
|
+
class="status-badge
|
|
92
|
+
{{if driver.status (dasherize driver.status) 'default'}}-status-badge status-badge-{{if driver.status (dasherize driver.status) 'default'}}
|
|
93
|
+
shadow-none ml-auto"
|
|
94
|
+
>
|
|
91
95
|
<span class="rounded-full text-[9px] px-1.5 py-0.5">{{n-a driver.meta.status_label (smart-humanize driver.status)}}</span>
|
|
92
96
|
</div>
|
|
93
97
|
</div>
|
|
@@ -122,7 +126,10 @@
|
|
|
122
126
|
</div>
|
|
123
127
|
<div class="rounded bg-gray-900 shadow-md px-1.5 py-1 min-w-0">
|
|
124
128
|
<div class="text-[9px] font-semibold uppercase text-gray-400 leading-none">Location</div>
|
|
125
|
-
<div class="text-[11px] text-white leading-tight whitespace-normal break-words">{{n-a
|
|
129
|
+
<div class="text-[11px] text-white leading-tight whitespace-normal break-words">{{n-a
|
|
130
|
+
driver.meta.location_coordinates
|
|
131
|
+
(point-coordinates driver.location)
|
|
132
|
+
}}</div>
|
|
126
133
|
</div>
|
|
127
134
|
</div>
|
|
128
135
|
</div>
|
|
@@ -132,7 +139,11 @@
|
|
|
132
139
|
<div class="flex items-center gap-1.5 text-xs font-semibold mb-1.5">
|
|
133
140
|
<span class="inline-block w-2 h-2 rounded-full {{if driver.online 'bg-green-400' 'bg-red-500'}}"></span>
|
|
134
141
|
<span>{{driver.name}}</span>
|
|
135
|
-
<div
|
|
142
|
+
<div
|
|
143
|
+
class="status-badge
|
|
144
|
+
{{if driver.status (dasherize driver.status) 'default'}}-status-badge status-badge-{{if driver.status (dasherize driver.status) 'default'}}
|
|
145
|
+
shadow-none ml-auto"
|
|
146
|
+
>
|
|
136
147
|
<span class="rounded-full text-[9px] px-1.5 py-0.5">{{n-a driver.meta.status_label (smart-humanize driver.status)}}</span>
|
|
137
148
|
</div>
|
|
138
149
|
</div>
|
|
@@ -167,7 +178,10 @@
|
|
|
167
178
|
</div>
|
|
168
179
|
<div class="rounded bg-gray-900 shadow-md px-1.5 py-1 min-w-0">
|
|
169
180
|
<div class="text-[9px] font-semibold uppercase text-gray-400 leading-none">Location</div>
|
|
170
|
-
<div class="text-[11px] text-white leading-tight whitespace-normal break-words">{{n-a
|
|
181
|
+
<div class="text-[11px] text-white leading-tight whitespace-normal break-words">{{n-a
|
|
182
|
+
driver.meta.location_coordinates
|
|
183
|
+
(point-coordinates driver.location)
|
|
184
|
+
}}</div>
|
|
171
185
|
</div>
|
|
172
186
|
</div>
|
|
173
187
|
</div>
|
|
@@ -192,14 +206,24 @@
|
|
|
192
206
|
<div class="flex items-center gap-1.5 text-xs font-semibold mb-1.5">
|
|
193
207
|
<span class="inline-block w-2 h-2 rounded-full {{if vehicle.online 'bg-green-400' 'bg-red-500'}}"></span>
|
|
194
208
|
<span>{{vehicle.displayName}}</span>
|
|
195
|
-
<div
|
|
209
|
+
<div
|
|
210
|
+
class="status-badge
|
|
211
|
+
{{if vehicle.status (dasherize vehicle.status) 'default'}}-status-badge status-badge-{{if vehicle.status (dasherize vehicle.status) 'default'}}
|
|
212
|
+
shadow-none ml-auto"
|
|
213
|
+
>
|
|
196
214
|
<span class="rounded-full text-[9px] px-1.5 py-0.5">{{n-a vehicle.meta.status_label}}</span>
|
|
197
215
|
</div>
|
|
198
216
|
</div>
|
|
199
217
|
<div class="grid grid-cols-2 gap-1">
|
|
200
218
|
<div class="rounded bg-gray-900 shadow-md px-1.5 py-1 min-w-0">
|
|
201
219
|
<div class="text-[9px] font-semibold uppercase text-gray-400 leading-none">Vehicle #</div>
|
|
202
|
-
<div class="text-[11px] text-white leading-tight whitespace-normal break-words">{{n-a
|
|
220
|
+
<div class="text-[11px] text-white leading-tight whitespace-normal break-words">{{n-a
|
|
221
|
+
vehicle.internal_id
|
|
222
|
+
vehicle.public_id
|
|
223
|
+
vehicle.plate_number
|
|
224
|
+
vehicle.serial_number
|
|
225
|
+
vehicle.vin
|
|
226
|
+
}}</div>
|
|
203
227
|
</div>
|
|
204
228
|
<div class="rounded bg-gray-900 shadow-md px-1.5 py-1 min-w-0">
|
|
205
229
|
<div class="text-[9px] font-semibold uppercase text-gray-400 leading-none">Driver</div>
|
|
@@ -229,14 +253,24 @@
|
|
|
229
253
|
<div class="flex items-center gap-1.5 text-xs font-semibold mb-1.5">
|
|
230
254
|
<span class="inline-block w-2 h-2 rounded-full {{if vehicle.online 'bg-green-400' 'bg-red-500'}}"></span>
|
|
231
255
|
<span>{{vehicle.displayName}}</span>
|
|
232
|
-
<div
|
|
256
|
+
<div
|
|
257
|
+
class="status-badge
|
|
258
|
+
{{if vehicle.status (dasherize vehicle.status) 'default'}}-status-badge status-badge-{{if vehicle.status (dasherize vehicle.status) 'default'}}
|
|
259
|
+
shadow-none ml-auto"
|
|
260
|
+
>
|
|
233
261
|
<span class="rounded-full text-[9px] px-1.5 py-0.5">{{n-a vehicle.meta.status_label}}</span>
|
|
234
262
|
</div>
|
|
235
263
|
</div>
|
|
236
264
|
<div class="grid grid-cols-2 gap-1">
|
|
237
265
|
<div class="rounded bg-gray-900 shadow-md px-1.5 py-1 min-w-0">
|
|
238
266
|
<div class="text-[9px] font-semibold uppercase text-gray-400 leading-none">Vehicle #</div>
|
|
239
|
-
<div class="text-[11px] text-white leading-tight whitespace-normal break-words">{{n-a
|
|
267
|
+
<div class="text-[11px] text-white leading-tight whitespace-normal break-words">{{n-a
|
|
268
|
+
vehicle.internal_id
|
|
269
|
+
vehicle.public_id
|
|
270
|
+
vehicle.plate_number
|
|
271
|
+
vehicle.serial_number
|
|
272
|
+
vehicle.vin
|
|
273
|
+
}}</div>
|
|
240
274
|
</div>
|
|
241
275
|
<div class="rounded bg-gray-900 shadow-md px-1.5 py-1 min-w-0">
|
|
242
276
|
<div class="text-[9px] font-semibold uppercase text-gray-400 leading-none">Driver</div>
|
|
@@ -291,8 +325,8 @@
|
|
|
291
325
|
@id={{serviceArea.id}}
|
|
292
326
|
@record={{serviceArea}}
|
|
293
327
|
@locations={{unwrap-coordinates serviceArea.leafletCoordinates}}
|
|
294
|
-
@fillColor={{serviceArea.color}}
|
|
295
|
-
@color={{serviceArea.stroke_color}}
|
|
328
|
+
@fillColor={{or serviceArea.color "#3388ff"}}
|
|
329
|
+
@color={{or serviceArea.stroke_color serviceArea.color "#3388ff"}}
|
|
296
330
|
@onAdd={{fn this.trigger "onServiceAreaLayerAdded" serviceArea}}
|
|
297
331
|
as |polygon|
|
|
298
332
|
>
|
|
@@ -304,8 +338,8 @@
|
|
|
304
338
|
@id={{zone.id}}
|
|
305
339
|
@record={{zone}}
|
|
306
340
|
@locations={{unwrap-coordinates zone.leafletCoordinates}}
|
|
307
|
-
@fillColor={{zone.color}}
|
|
308
|
-
@color={{zone.stroke_color}}
|
|
341
|
+
@fillColor={{or zone.color "#3388ff"}}
|
|
342
|
+
@color={{or zone.stroke_color zone.color "#3388ff"}}
|
|
309
343
|
@onAdd={{fn this.trigger "onZoneLayerAdd" zone}}
|
|
310
344
|
as |polygon|
|
|
311
345
|
>
|
|
@@ -340,4 +374,4 @@
|
|
|
340
374
|
@liveMap={{this}}
|
|
341
375
|
@onDrawerReady={{this.setDrawerContext}}
|
|
342
376
|
/>
|
|
343
|
-
{{/if}} --}}
|
|
377
|
+
{{/if}} --}}
|
|
@@ -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
|
-
|
|
110
|
-
map.on('
|
|
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
|
|
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 =
|
|
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
|
-
|
|
498
|
-
...
|
|
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
|
|
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
|
|
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
|
|
524
|
-
callback: () => this
|
|
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:
|
|
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(
|
|
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
|
|
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
|
|
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
|
|
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
|
|
561
|
-
callback: () => this
|
|
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:
|
|
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(
|
|
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>
|