@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
|
@@ -9,6 +9,7 @@ import Service, { inject as service } from '@ember/service';
|
|
|
9
9
|
import { action } from '@ember/object';
|
|
10
10
|
import { debug } from '@ember/debug';
|
|
11
11
|
import { dasherize } from '@ember/string';
|
|
12
|
+
import { later } from '@ember/runloop';
|
|
12
13
|
import toMultiPolygon from '../utils/to-multi-polygon';
|
|
13
14
|
|
|
14
15
|
export default class GeofenceService extends Service {
|
|
@@ -173,9 +174,11 @@ export default class GeofenceService extends Service {
|
|
|
173
174
|
},
|
|
174
175
|
{
|
|
175
176
|
callback: (serviceArea) => {
|
|
176
|
-
this.#
|
|
177
|
-
|
|
177
|
+
this.#finishDraftSave((draftLayer) => {
|
|
178
|
+
this.#reloadThenFocusServiceArea(serviceArea, { draftLayer });
|
|
179
|
+
});
|
|
178
180
|
},
|
|
181
|
+
refresh: false,
|
|
179
182
|
}
|
|
180
183
|
);
|
|
181
184
|
return;
|
|
@@ -184,7 +187,7 @@ export default class GeofenceService extends Service {
|
|
|
184
187
|
if (this._draftSession.mode === 'zone') {
|
|
185
188
|
const serviceArea = this._draftSession.serviceArea;
|
|
186
189
|
this.zoneActions.modal.create(
|
|
187
|
-
{ border: geoJson,
|
|
190
|
+
{ border: geoJson, service_area: serviceArea },
|
|
188
191
|
{
|
|
189
192
|
decline: (modal) => {
|
|
190
193
|
this.#resetDraftSession({ hideControl: true, removeDraft: true });
|
|
@@ -193,10 +196,11 @@ export default class GeofenceService extends Service {
|
|
|
193
196
|
},
|
|
194
197
|
{
|
|
195
198
|
callback: (zone) => {
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
+
this.#finishDraftSave((draftLayer) => {
|
|
200
|
+
this.#reloadThenFocusZone(serviceArea, zone, { draftLayer });
|
|
201
|
+
});
|
|
199
202
|
},
|
|
203
|
+
refresh: false,
|
|
200
204
|
}
|
|
201
205
|
);
|
|
202
206
|
return;
|
|
@@ -246,19 +250,19 @@ export default class GeofenceService extends Service {
|
|
|
246
250
|
} else {
|
|
247
251
|
record.setProperties({
|
|
248
252
|
border: geoJson,
|
|
249
|
-
|
|
250
|
-
serviceArea: selectedServiceArea,
|
|
253
|
+
service_area: selectedServiceArea,
|
|
251
254
|
});
|
|
252
255
|
}
|
|
253
256
|
|
|
254
257
|
return record.save().then((savedRecord) => {
|
|
255
|
-
this.#resetDraftSession({ hideControl: true, removeDraft: true });
|
|
256
|
-
|
|
257
258
|
if (selectedLayerType === 'Service Area') {
|
|
258
|
-
this.#
|
|
259
|
+
this.#finishDraftSave((draftLayer) => {
|
|
260
|
+
this.#reloadThenFocusServiceArea(savedRecord, { draftLayer });
|
|
261
|
+
});
|
|
259
262
|
} else {
|
|
260
|
-
|
|
261
|
-
|
|
263
|
+
this.#finishDraftSave((draftLayer) => {
|
|
264
|
+
this.#reloadThenFocusZone(selectedServiceArea, savedRecord, { draftLayer });
|
|
265
|
+
});
|
|
262
266
|
}
|
|
263
267
|
|
|
264
268
|
return savedRecord;
|
|
@@ -289,28 +293,168 @@ export default class GeofenceService extends Service {
|
|
|
289
293
|
this._draftSession = null;
|
|
290
294
|
}
|
|
291
295
|
|
|
296
|
+
#finishDraftSave(callback) {
|
|
297
|
+
const draftLayer = this._draftSession?.draftLayer ?? null;
|
|
298
|
+
callback?.(draftLayer);
|
|
299
|
+
this.#resetDraftSession({ hideControl: true, removeDraft: false });
|
|
300
|
+
}
|
|
301
|
+
|
|
292
302
|
async #editPolygonLayer(originalLayer, { focusBounds = null } = {}) {
|
|
293
303
|
return this.mapManager.editPolygon(originalLayer, { focusBounds });
|
|
294
304
|
}
|
|
295
305
|
|
|
296
306
|
#showZone(zone, { pin = false } = {}) {
|
|
297
|
-
const
|
|
307
|
+
const overlay = this.mapManager.getOverlay(zone.id);
|
|
308
|
+
const layer = overlay ?? zone?.leafletLayer ?? null;
|
|
298
309
|
this.#setLayerPinned(layer, pin);
|
|
299
|
-
|
|
310
|
+
if (overlay) {
|
|
311
|
+
this.mapManager.showPolygon(zone.id);
|
|
312
|
+
} else if (layer) {
|
|
313
|
+
this.mapManager.showLayer?.(layer);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
#includeServiceArea(serviceArea) {
|
|
318
|
+
if (!serviceArea?.id) return;
|
|
319
|
+
|
|
320
|
+
const currentServiceAreas = this.serviceAreaActions.serviceAreas;
|
|
321
|
+
if (!currentServiceAreas) {
|
|
322
|
+
this.serviceAreaActions.serviceAreas = [serviceArea];
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
const serviceAreas = Array.from(currentServiceAreas);
|
|
327
|
+
const exists = serviceAreas.some((existingServiceArea) => existingServiceArea?.id === serviceArea.id);
|
|
328
|
+
if (exists) return;
|
|
329
|
+
|
|
330
|
+
this.serviceAreaActions.serviceAreas = [...serviceAreas, serviceArea];
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
#includeZone(serviceArea, zone) {
|
|
334
|
+
if (!serviceArea?.id || !zone?.id) return;
|
|
335
|
+
|
|
336
|
+
const zones = serviceArea.zones;
|
|
337
|
+
if (!zones) {
|
|
338
|
+
serviceArea.set?.('zones', [zone]);
|
|
339
|
+
this.serviceAreaActions.serviceAreas = Array.from(this.serviceAreaActions.serviceAreas ?? []);
|
|
340
|
+
return;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
const exists = Array.from(zones).some((existingZone) => existingZone?.id === zone.id);
|
|
344
|
+
if (exists) return;
|
|
345
|
+
|
|
346
|
+
if (typeof zones.pushObject === 'function') {
|
|
347
|
+
zones.pushObject(zone);
|
|
348
|
+
} else if (typeof zones.addObject === 'function') {
|
|
349
|
+
zones.addObject(zone);
|
|
350
|
+
} else if (typeof zones.push === 'function') {
|
|
351
|
+
zones.push(zone);
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
this.serviceAreaActions.serviceAreas = Array.from(this.serviceAreaActions.serviceAreas ?? []);
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
#reloadThenFocusServiceArea(serviceArea, options = {}) {
|
|
358
|
+
this.#reloadServiceAreas()
|
|
359
|
+
.then((serviceAreas) => {
|
|
360
|
+
const canonicalServiceArea = this.#findResource(serviceAreas, serviceArea) ?? serviceArea;
|
|
361
|
+
this.#focusServiceAreaAfterCreate(canonicalServiceArea, options);
|
|
362
|
+
})
|
|
363
|
+
.catch(() => {
|
|
364
|
+
this.#includeServiceArea(serviceArea);
|
|
365
|
+
this.#focusServiceAreaAfterCreate(serviceArea, options);
|
|
366
|
+
});
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
#reloadThenFocusZone(serviceArea, zone, options = {}) {
|
|
370
|
+
this.#reloadServiceAreas()
|
|
371
|
+
.then((serviceAreas) => {
|
|
372
|
+
const canonicalServiceArea = this.#findResource(serviceAreas, serviceArea) ?? serviceArea;
|
|
373
|
+
const canonicalZone = this.#findResource(canonicalServiceArea?.zones, zone) ?? zone;
|
|
374
|
+
this.#focusZoneAfterCreate(canonicalZone, options);
|
|
375
|
+
})
|
|
376
|
+
.catch(() => {
|
|
377
|
+
this.#includeZone(serviceArea, zone);
|
|
378
|
+
this.#focusZoneAfterCreate(zone, options);
|
|
379
|
+
});
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
#reloadServiceAreas() {
|
|
383
|
+
return Promise.resolve(this.serviceAreaActions.loadAll.perform()).then(() => this.serviceAreaActions.serviceAreas ?? []);
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
#findResource(resources, resource) {
|
|
387
|
+
if (!resource || !resources) return null;
|
|
388
|
+
|
|
389
|
+
const resourceId = resource.id ?? resource.public_id;
|
|
390
|
+
const publicId = resource.public_id ?? resource.id;
|
|
391
|
+
|
|
392
|
+
return (
|
|
393
|
+
Array.from(resources).find((candidate) => {
|
|
394
|
+
return candidate?.id === resourceId || candidate?.id === publicId || candidate?.public_id === publicId || candidate?.public_id === resourceId;
|
|
395
|
+
}) ?? null
|
|
396
|
+
);
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
#focusServiceAreaAfterCreate(serviceArea, options = {}) {
|
|
400
|
+
this.#focusWhenReady([serviceArea], () => this.focusServiceArea(serviceArea), options);
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
#focusZoneAfterCreate(zone, options = {}) {
|
|
404
|
+
this.#focusWhenReady([zone], () => this.focusZone(zone), options);
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
#focusWhenReady(models, focus, { attempts = 60, delay = 100, draftLayer = null } = {}) {
|
|
408
|
+
const overlays = models
|
|
409
|
+
.map((model) => {
|
|
410
|
+
const overlay = this.mapManager.getOverlay(model?.id);
|
|
411
|
+
if (overlay) {
|
|
412
|
+
return overlay;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
return model?.leafletLayer === draftLayer ? null : model?.leafletLayer;
|
|
416
|
+
})
|
|
417
|
+
.filter(Boolean);
|
|
418
|
+
const allRegistered = overlays.length === models.length;
|
|
419
|
+
|
|
420
|
+
if (allRegistered || attempts <= 0) {
|
|
421
|
+
focus();
|
|
422
|
+
if (draftLayer && !draftLayer.__removedAfterCreate && !overlays.includes(draftLayer)) {
|
|
423
|
+
draftLayer.__removedAfterCreate = true;
|
|
424
|
+
this.mapManager.removeLayer(draftLayer);
|
|
425
|
+
}
|
|
426
|
+
return;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
later(this, () => this.#focusWhenReady(models, focus, { attempts: attempts - 1, delay, draftLayer }), delay);
|
|
300
430
|
}
|
|
301
431
|
|
|
302
432
|
#showServiceArea(serviceArea, { pin = false } = {}) {
|
|
303
433
|
[serviceArea, ...(serviceArea.zones ?? [])].forEach((model) => {
|
|
304
|
-
const
|
|
434
|
+
const overlay = this.mapManager.getOverlay(model.id);
|
|
435
|
+
const layer = overlay ?? model?.leafletLayer ?? null;
|
|
305
436
|
this.#setLayerPinned(layer, pin);
|
|
306
|
-
|
|
437
|
+
if (overlay) {
|
|
438
|
+
this.mapManager.showPolygon(model.id);
|
|
439
|
+
} else if (layer) {
|
|
440
|
+
this.mapManager.showLayer?.(layer);
|
|
441
|
+
}
|
|
307
442
|
});
|
|
308
443
|
}
|
|
309
444
|
|
|
310
445
|
#hideServiceArea(serviceArea) {
|
|
311
446
|
[serviceArea, ...(serviceArea.zones ?? [])].forEach((model) => {
|
|
312
|
-
|
|
313
|
-
|
|
447
|
+
const overlay = this.mapManager.getOverlay(model.id);
|
|
448
|
+
const layer = model?.leafletLayer;
|
|
449
|
+
|
|
450
|
+
this.#setLayerPinned(layer ?? overlay, false);
|
|
451
|
+
if (overlay) {
|
|
452
|
+
this.mapManager.hidePolygon(model.id);
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
if (layer && layer !== overlay) {
|
|
456
|
+
this.mapManager.hideLayer?.(layer);
|
|
457
|
+
}
|
|
314
458
|
});
|
|
315
459
|
}
|
|
316
460
|
|
|
@@ -85,6 +85,33 @@ export default class LeafletContextmenuManagerService extends Service {
|
|
|
85
85
|
return null;
|
|
86
86
|
}
|
|
87
87
|
|
|
88
|
+
/**
|
|
89
|
+
* Remove a registered context menu and detach it from its layer when possible.
|
|
90
|
+
*
|
|
91
|
+
* @param {string} registryName - The public context menu registry name.
|
|
92
|
+
* @returns {Object|null} The removed registry, or null when none existed.
|
|
93
|
+
*/
|
|
94
|
+
removeContextMenu(registryName) {
|
|
95
|
+
const internalRegistryName = this.createInternalRegistryName(registryName);
|
|
96
|
+
const registry = this.contextMenuRegistry[internalRegistryName];
|
|
97
|
+
|
|
98
|
+
if (!registry) {
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
try {
|
|
103
|
+
registry.contextmenuApi?.removeAllItems?.();
|
|
104
|
+
registry.layer?.unbindContextMenu?.();
|
|
105
|
+
} catch (error) {
|
|
106
|
+
// Context menus are disposable UI state; stale layer cleanup should not block record deletion.
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
delete this.contextMenuRegistry[internalRegistryName];
|
|
110
|
+
this.contextMenuRegistry = { ...this.contextMenuRegistry };
|
|
111
|
+
|
|
112
|
+
return registry;
|
|
113
|
+
}
|
|
114
|
+
|
|
88
115
|
/**
|
|
89
116
|
* Creates an internal registry name by camelizing the provided registry name and appending "ConextmenuRegistry" to it.
|
|
90
117
|
*
|
|
@@ -345,6 +345,51 @@ body.fleetbase-console .next-content-overlay > .next-content-overlay-panel-conta
|
|
|
345
345
|
gap: 0.5rem;
|
|
346
346
|
}
|
|
347
347
|
|
|
348
|
+
.fleetops-order-form-waypoint {
|
|
349
|
+
position: relative;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
.fleetops-order-form-waypoint--required {
|
|
353
|
+
border-color: #f97316 !important;
|
|
354
|
+
padding-top: 1.25rem;
|
|
355
|
+
box-shadow:
|
|
356
|
+
0 1px 3px 0 rgb(249 115 22 / 15%),
|
|
357
|
+
0 0 0 1px rgb(249 115 22 / 10%);
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
.fleetops-order-form-waypoint__required-tab {
|
|
361
|
+
position: absolute;
|
|
362
|
+
top: -1px;
|
|
363
|
+
right: -1px;
|
|
364
|
+
border-top-right-radius: 0.375rem;
|
|
365
|
+
border-bottom-left-radius: 0.375rem;
|
|
366
|
+
background-color: #f97316;
|
|
367
|
+
color: #fff7ed;
|
|
368
|
+
padding: 0.15rem 0.5rem;
|
|
369
|
+
font-size: 0.62rem;
|
|
370
|
+
font-weight: 700;
|
|
371
|
+
line-height: 1.2;
|
|
372
|
+
text-transform: uppercase;
|
|
373
|
+
letter-spacing: 0.02em;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
.fleetops-order-form-waypoint__marker {
|
|
377
|
+
display: flex;
|
|
378
|
+
flex-direction: row;
|
|
379
|
+
align-items: center;
|
|
380
|
+
gap: 0.6rem;
|
|
381
|
+
min-width: 3.25rem;
|
|
382
|
+
padding-top: 0.15rem;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
.fleetops-order-form-waypoint__badge {
|
|
386
|
+
min-width: 1.65rem;
|
|
387
|
+
width: 1.65rem;
|
|
388
|
+
height: 1.65rem;
|
|
389
|
+
font-size: 0.75rem;
|
|
390
|
+
flex-shrink: 0;
|
|
391
|
+
}
|
|
392
|
+
|
|
348
393
|
.fleetops-route-editor-empty-state {
|
|
349
394
|
border: 1px dashed rgb(75 85 99 / 75%);
|
|
350
395
|
border-radius: 0.5rem;
|
|
@@ -464,7 +509,52 @@ body[data-theme='light'] .flow-activity-status-pill text {
|
|
|
464
509
|
.activity-flow-graph-container {
|
|
465
510
|
position: relative;
|
|
466
511
|
width: 100%;
|
|
467
|
-
height:
|
|
512
|
+
height: calc(100vh - 16rem);
|
|
513
|
+
min-height: 24rem;
|
|
514
|
+
overflow: auto hidden;
|
|
515
|
+
scrollbar-width: none;
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
.activity-flow-graph-container > .joint-js-paper {
|
|
519
|
+
min-width: 100%;
|
|
520
|
+
max-width: none;
|
|
521
|
+
overflow: visible;
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
.activity-flow-graph-container::-webkit-scrollbar {
|
|
525
|
+
display: none;
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
.activity-flow-horizontal-scrollbar {
|
|
529
|
+
position: fixed;
|
|
530
|
+
bottom: 0;
|
|
531
|
+
z-index: 100;
|
|
532
|
+
height: 12px;
|
|
533
|
+
overflow: auto hidden;
|
|
534
|
+
background-color: transparent;
|
|
535
|
+
scrollbar-color: #6b7280 transparent;
|
|
536
|
+
scrollbar-width: thin;
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
.activity-flow-horizontal-scrollbar.is-hidden {
|
|
540
|
+
display: none;
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
.activity-flow-horizontal-scrollbar-content {
|
|
544
|
+
height: 1px;
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
.activity-flow-horizontal-scrollbar::-webkit-scrollbar {
|
|
548
|
+
height: 8px;
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
.activity-flow-horizontal-scrollbar::-webkit-scrollbar-thumb {
|
|
552
|
+
background-color: #6b7280;
|
|
553
|
+
border-radius: 9999px;
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
.activity-flow-horizontal-scrollbar::-webkit-scrollbar-track {
|
|
557
|
+
background-color: transparent;
|
|
468
558
|
}
|
|
469
559
|
|
|
470
560
|
.activity-flow-zoom-controls {
|
|
@@ -15,7 +15,7 @@ export const driverTypes = [
|
|
|
15
15
|
];
|
|
16
16
|
|
|
17
17
|
export const driverStatuses = [
|
|
18
|
-
{ label: '
|
|
18
|
+
{ label: 'Available', value: 'available', description: 'Driver is available for assignments' },
|
|
19
19
|
{ label: 'Inactive', value: 'inactive', description: 'Driver is not currently active' },
|
|
20
20
|
{ label: 'On Duty', value: 'on_duty', description: 'Driver is working and available' },
|
|
21
21
|
{ label: 'Off Duty', value: 'off_duty', description: 'Driver is not working' },
|
|
@@ -382,6 +382,7 @@ export const podOptions = [
|
|
|
382
382
|
export const serviceRateCalculationMethods = [
|
|
383
383
|
{ label: 'Per Meter', value: 'per_meter', description: 'Calculate cost based on the number of meters traveled.' },
|
|
384
384
|
{ label: 'Fixed Rate', value: 'fixed_meter', description: 'Charge a fixed rate per meter regardless of distance.' },
|
|
385
|
+
{ label: 'Multi-zone Distance', value: 'multi_zone_distance', description: 'Calculate cost by splitting route distance across priced service areas and zones.' },
|
|
385
386
|
{ label: 'Parcel Rate', value: 'parcel', description: 'Charge a dynamic rate based on parcel/payload weight and dimensions.' },
|
|
386
387
|
{ label: 'Per Drop-off', value: 'per_drop', description: 'Cost applied per drop-off point.' },
|
|
387
388
|
{ label: 'Algorithm', value: 'algo', description: 'Cost calculated dynamically using algorithmic logic.' },
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { isArray } from '@ember/array';
|
|
2
|
+
|
|
3
|
+
export function getOrderConfigFlowRootCode(flow = {}) {
|
|
4
|
+
if (flow.created) {
|
|
5
|
+
return 'created';
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
return Object.keys(flow)[0];
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export default function normalizeOrderConfigFlow(flow = {}) {
|
|
12
|
+
if (isArray(flow)) {
|
|
13
|
+
return flow.reduce((normalized, activity, index) => {
|
|
14
|
+
if (!activity || typeof activity !== 'object') {
|
|
15
|
+
return normalized;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const code = activity.code ?? activity.key;
|
|
19
|
+
if (!code) {
|
|
20
|
+
return normalized;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const nextActivity = flow.slice(index + 1).find((candidate) => candidate && typeof candidate === 'object' && (candidate.code ?? candidate.key));
|
|
24
|
+
|
|
25
|
+
normalized[code] = {
|
|
26
|
+
...activity,
|
|
27
|
+
code,
|
|
28
|
+
key: activity.key ?? code,
|
|
29
|
+
activities: isArray(activity.activities) ? activity.activities : nextActivity ? [nextActivity.code ?? nextActivity.key] : [],
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
return normalized;
|
|
33
|
+
}, {});
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (flow && typeof flow === 'object') {
|
|
37
|
+
return flow;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return {};
|
|
41
|
+
}
|
|
@@ -11,17 +11,18 @@ function line(content, className = '') {
|
|
|
11
11
|
return `<div${classAttribute}>${escapeHtml(content)}</div>`;
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
-
export default function placeAddressHtml(place) {
|
|
14
|
+
export default function placeAddressHtml(place, options = {}) {
|
|
15
15
|
if (!place) {
|
|
16
16
|
return htmlSafe('');
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
+
const { showTitle = true } = options;
|
|
19
20
|
const name = place.name === place.street1 ? null : place.name;
|
|
20
21
|
const cityStatePostalCode = [place.city, place.province, place.postal_code].filter((value) => !isBlank(value)).join(', ');
|
|
21
22
|
const lines = [];
|
|
22
23
|
|
|
23
24
|
if (name) {
|
|
24
|
-
lines.push(line(name, 'font-semibold'));
|
|
25
|
+
if (showTitle) lines.push(line(name, 'font-semibold'));
|
|
25
26
|
lines.push(line(place.street1));
|
|
26
27
|
} else if (place.street1) {
|
|
27
28
|
lines.push(line(place.street1, 'font-semibold'));
|
|
@@ -8,6 +8,18 @@ export function waypointToPlace(waypoint) {
|
|
|
8
8
|
return waypoint.place ?? waypoint;
|
|
9
9
|
}
|
|
10
10
|
|
|
11
|
+
export function endpointTrackingNumberUuid(payload, role) {
|
|
12
|
+
if (!payload || !role) {
|
|
13
|
+
return null;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
return payload[`${role}_tracking_number_uuid`] ?? null;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function waypointTrackingNumberUuid(waypoint) {
|
|
20
|
+
return waypoint?.tracking_number_uuid ?? waypoint?.tracking_number?.uuid ?? waypoint?.trackingNumber?.uuid ?? null;
|
|
21
|
+
}
|
|
22
|
+
|
|
11
23
|
export function buildPlaceAddressTooltip(title, place) {
|
|
12
24
|
if (!place) {
|
|
13
25
|
return title;
|
|
@@ -38,30 +50,65 @@ export function buildRoutePointsFromPayload(payload) {
|
|
|
38
50
|
const waypoints = payload.waypoints ?? [];
|
|
39
51
|
|
|
40
52
|
if (hasValidCoordinates(payload.pickup)) {
|
|
41
|
-
points.push({ role: 'pickup', place: payload.pickup });
|
|
53
|
+
points.push({ role: 'pickup', place: payload.pickup, trackingNumberUuid: endpointTrackingNumberUuid(payload, 'pickup') });
|
|
42
54
|
}
|
|
43
55
|
|
|
44
56
|
waypoints.forEach((waypoint, index) => {
|
|
45
57
|
const place = waypointToPlace(waypoint);
|
|
46
58
|
|
|
47
59
|
if (hasValidCoordinates(place)) {
|
|
48
|
-
points.push({ role: 'waypoint', place, stopNumber: index + 1 });
|
|
60
|
+
points.push({ role: 'waypoint', place, stopNumber: index + 1, trackingNumberUuid: waypointTrackingNumberUuid(waypoint) });
|
|
49
61
|
}
|
|
50
62
|
});
|
|
51
63
|
|
|
52
64
|
if (hasValidCoordinates(payload.dropoff)) {
|
|
53
|
-
points.push({ role: 'dropoff', place: payload.dropoff });
|
|
65
|
+
points.push({ role: 'dropoff', place: payload.dropoff, trackingNumberUuid: endpointTrackingNumberUuid(payload, 'dropoff') });
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (hasValidCoordinates(payload.return)) {
|
|
69
|
+
points.push({ role: 'return', place: payload.return, trackingNumberUuid: endpointTrackingNumberUuid(payload, 'return') });
|
|
54
70
|
}
|
|
55
71
|
|
|
56
72
|
return points;
|
|
57
73
|
}
|
|
58
74
|
|
|
75
|
+
export function buildServiceStopsFromPayload(payload) {
|
|
76
|
+
if (!payload) {
|
|
77
|
+
return [];
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const stops = [];
|
|
81
|
+
const waypoints = payload.waypoints ?? [];
|
|
82
|
+
|
|
83
|
+
if (payload.pickup) {
|
|
84
|
+
stops.push({ role: 'pickup', place: payload.pickup, trackingNumberUuid: endpointTrackingNumberUuid(payload, 'pickup') });
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
waypoints.forEach((waypoint, index) => {
|
|
88
|
+
const place = waypointToPlace(waypoint);
|
|
89
|
+
|
|
90
|
+
if (place) {
|
|
91
|
+
stops.push({ role: 'waypoint', place, stopNumber: index + 1, trackingNumberUuid: waypointTrackingNumberUuid(waypoint) });
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
if (payload.dropoff) {
|
|
96
|
+
stops.push({ role: 'dropoff', place: payload.dropoff, trackingNumberUuid: endpointTrackingNumberUuid(payload, 'dropoff') });
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (payload.return) {
|
|
100
|
+
stops.push({ role: 'return', place: payload.return, trackingNumberUuid: endpointTrackingNumberUuid(payload, 'return') });
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return stops;
|
|
104
|
+
}
|
|
105
|
+
|
|
59
106
|
export function describeRoutePoint(routePoint, routeColor) {
|
|
60
107
|
const role = routePoint?.role;
|
|
61
108
|
const stopNumber = routePoint?.stopNumber ?? 1;
|
|
62
|
-
const label = role === 'pickup' ? 'P' : role === 'dropoff' ? 'D' : String(stopNumber);
|
|
63
|
-
const markerColor = role === 'pickup' ? '#22C55E' : role === 'dropoff' ? '#EF4444' : routeColor;
|
|
64
|
-
const title = role === 'pickup' ? 'Pickup' : role === 'dropoff' ? 'Dropoff' : `Stop ${stopNumber}`;
|
|
109
|
+
const label = role === 'pickup' ? 'P' : role === 'dropoff' ? 'D' : role === 'return' ? 'R' : String(stopNumber);
|
|
110
|
+
const markerColor = role === 'pickup' ? '#22C55E' : role === 'dropoff' ? '#EF4444' : role === 'return' ? '#F97316' : routeColor;
|
|
111
|
+
const title = role === 'pickup' ? 'Pickup' : role === 'dropoff' ? 'Dropoff' : role === 'return' ? 'Return' : `Stop ${stopNumber}`;
|
|
65
112
|
|
|
66
113
|
return {
|
|
67
114
|
label,
|
|
@@ -74,7 +121,7 @@ export function buildRoutePointMarkerPresentation(routePoint, routeColor, option
|
|
|
74
121
|
const role = routePoint?.role;
|
|
75
122
|
const place = routePoint?.place;
|
|
76
123
|
const { label, markerColor, title } = describeRoutePoint(routePoint, routeColor);
|
|
77
|
-
const zIndexOffset = role === 'pickup' ? 2200 : role === 'dropoff' ? 2100 : 1800;
|
|
124
|
+
const zIndexOffset = role === 'pickup' ? 2200 : role === 'dropoff' ? 2100 : role === 'return' ? 2000 : 1800;
|
|
78
125
|
|
|
79
126
|
return {
|
|
80
127
|
waypointLabel: label,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default } from '@fleetbase/fleetops-engine/components/modals/order-change-destination';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default } from '@fleetbase/fleetops-engine/components/modals/stop-activity';
|
package/composer.json
CHANGED
package/extension.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fleetbase/fleetops-engine",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.50",
|
|
4
4
|
"description": "Fleet & Transport Management Extension for Fleetbase",
|
|
5
5
|
"fleetbase": {
|
|
6
6
|
"route": "fleet-ops"
|
|
@@ -43,8 +43,8 @@
|
|
|
43
43
|
"dependencies": {
|
|
44
44
|
"@babel/core": "^7.23.2",
|
|
45
45
|
"@fleetbase/ember-core": "^0.3.19",
|
|
46
|
-
"@fleetbase/ember-ui": "^0.3.
|
|
47
|
-
"@fleetbase/fleetops-data": "^0.1.
|
|
46
|
+
"@fleetbase/ember-ui": "^0.3.32",
|
|
47
|
+
"@fleetbase/fleetops-data": "^0.1.34",
|
|
48
48
|
"@fleetbase/leaflet-routing-machine": "^3.2.17",
|
|
49
49
|
"@fortawesome/ember-fontawesome": "^2.0.0",
|
|
50
50
|
"@fortawesome/fontawesome-svg-core": "6.4.0",
|
package/server/migrations/2026_05_21_000001_add_geographic_rules_to_service_rate_fees_table.php
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
<?php
|
|
2
|
+
|
|
3
|
+
use Illuminate\Database\Migrations\Migration;
|
|
4
|
+
use Illuminate\Database\Schema\Blueprint;
|
|
5
|
+
use Illuminate\Support\Facades\Schema;
|
|
6
|
+
|
|
7
|
+
return new class extends Migration {
|
|
8
|
+
/**
|
|
9
|
+
* Run the migrations.
|
|
10
|
+
*
|
|
11
|
+
* @return void
|
|
12
|
+
*/
|
|
13
|
+
public function up()
|
|
14
|
+
{
|
|
15
|
+
Schema::table('service_rate_fees', function (Blueprint $table) {
|
|
16
|
+
$table->uuid('service_area_uuid')->nullable()->index()->after('service_rate_uuid');
|
|
17
|
+
$table->uuid('zone_uuid')->nullable()->index()->after('service_area_uuid');
|
|
18
|
+
$table->string('label')->nullable()->after('zone_uuid');
|
|
19
|
+
$table->integer('priority')->default(0)->after('label');
|
|
20
|
+
$table->boolean('is_fallback')->default(false)->after('priority');
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Reverse the migrations.
|
|
26
|
+
*
|
|
27
|
+
* @return void
|
|
28
|
+
*/
|
|
29
|
+
public function down()
|
|
30
|
+
{
|
|
31
|
+
Schema::table('service_rate_fees', function (Blueprint $table) {
|
|
32
|
+
$table->dropIndex(['service_area_uuid']);
|
|
33
|
+
$table->dropIndex(['zone_uuid']);
|
|
34
|
+
$table->dropColumn(['service_area_uuid', 'zone_uuid', 'label', 'priority', 'is_fallback']);
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
};
|