@fleetbase/fleetops-engine 0.6.48 → 0.6.50
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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/driver/details.hbs +5 -1
- package/addon/components/driver/form.hbs +11 -1
- package/addon/components/fleet-listing-panel.hbs +2 -2
- package/addon/components/fleet-listing-panel.js +10 -0
- package/addon/components/map/google-live-map.js +18 -75
- package/addon/components/map/leaflet-live-map.hbs +187 -37
- 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/phase-builder.hbs +21 -0
- package/addon/components/orchestrator/phase-builder.js +13 -0
- package/addon/components/orchestrator/resource-panel.hbs +0 -10
- package/addon/components/orchestrator/resource-panel.js +2 -4
- package/addon/components/orchestrator-workbench.js +1 -0
- 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/form.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/components/widget/fleet-ops-key-metrics.hbs +19 -17
- package/addon/components/widget/fleet-ops-key-metrics.js +26 -0
- package/addon/components/widget/fuel-efficiency.hbs +26 -0
- package/addon/components/widget/fuel-efficiency.js +43 -0
- package/addon/components/widget/geofence-violations.hbs +44 -0
- package/addon/components/widget/geofence-violations.js +55 -0
- package/addon/components/widget/issues-insights.hbs +43 -0
- package/addon/components/widget/issues-insights.js +62 -0
- package/addon/components/widget/kpi-active-orders.hbs +10 -0
- package/addon/components/widget/kpi-active-orders.js +5 -0
- package/addon/components/widget/kpi-aov.hbs +10 -0
- package/addon/components/widget/kpi-aov.js +5 -0
- package/addon/components/widget/kpi-distance.hbs +10 -0
- package/addon/components/widget/kpi-distance.js +5 -0
- package/addon/components/widget/kpi-drivers-online.hbs +10 -0
- package/addon/components/widget/kpi-drivers-online.js +5 -0
- package/addon/components/widget/kpi-earnings.hbs +10 -0
- package/addon/components/widget/kpi-earnings.js +5 -0
- package/addon/components/widget/kpi-open-issues.hbs +11 -0
- package/addon/components/widget/kpi-open-issues.js +5 -0
- package/addon/components/widget/kpi-tile.hbs +59 -0
- package/addon/components/widget/kpi-tile.js +168 -0
- package/addon/components/widget/live-fleet.hbs +122 -0
- package/addon/components/widget/live-fleet.js +365 -0
- package/addon/components/widget/maintenance-overview.hbs +45 -0
- package/addon/components/widget/maintenance-overview.js +40 -0
- package/addon/components/widget/on-time-delivery.hbs +27 -0
- package/addon/components/widget/on-time-delivery.js +73 -0
- package/addon/components/widget/operations-pulse.hbs +51 -0
- package/addon/components/widget/operations-pulse.js +173 -0
- package/addon/components/widget/orders-by-status.hbs +26 -0
- package/addon/components/widget/orders-by-status.js +54 -0
- package/addon/components/widget/revenue-trend.hbs +34 -0
- package/addon/components/widget/revenue-trend.js +67 -0
- package/addon/components/widget/top-drivers.hbs +44 -0
- package/addon/components/widget/top-drivers.js +54 -0
- 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/controllers/settings/routing.js +26 -0
- package/addon/extension.js +172 -3
- 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/map-adapter/google.js +13 -2
- package/addon/services/vehicle-actions.js +1 -1
- package/addon/styles/fleetops-engine.css +205 -88
- package/addon/templates/maintenance/equipment/index.hbs +1 -1
- package/addon/templates/maintenance/parts/index.hbs +1 -1
- package/addon/templates/maintenance/schedules/index.hbs +1 -1
- package/addon/templates/management/vehicles/index.hbs +1 -1
- package/addon/templates/settings/avatars.hbs +1 -1
- package/addon/templates/settings/custom-fields.hbs +1 -1
- package/addon/templates/settings/map.hbs +1 -1
- package/addon/templates/settings/navigator-app.hbs +1 -1
- package/addon/templates/settings/notifications.hbs +1 -1
- package/addon/templates/settings/orchestrator.hbs +1 -1
- package/addon/templates/settings/payments/index.hbs +1 -1
- package/addon/templates/settings/payments/onboard.hbs +1 -1
- package/addon/templates/settings/routing.hbs +22 -2
- package/addon/templates/settings/scheduling.hbs +1 -1
- package/addon/utils/fleet-ops-options.js +2 -1
- package/addon/utils/live-map-card-content.js +171 -0
- 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/app/components/widget/fuel-efficiency.js +1 -0
- package/app/components/widget/geofence-violations.js +1 -0
- package/app/components/widget/issues-insights.js +1 -0
- package/app/components/widget/kpi-active-orders.js +1 -0
- package/app/components/widget/kpi-aov.js +1 -0
- package/app/components/widget/kpi-distance.js +1 -0
- package/app/components/widget/kpi-drivers-online.js +1 -0
- package/app/components/widget/kpi-earnings.js +1 -0
- package/app/components/widget/kpi-open-issues.js +1 -0
- package/app/components/widget/kpi-tile.js +1 -0
- package/app/components/widget/live-fleet.js +1 -0
- package/app/components/widget/maintenance-overview.js +1 -0
- package/app/components/widget/on-time-delivery.js +1 -0
- package/app/components/widget/operations-pulse.js +1 -0
- package/app/components/widget/orders-by-status.js +1 -0
- package/app/components/widget/revenue-trend.js +1 -0
- package/app/components/widget/top-drivers.js +1 -0
- package/app/utils/live-map-card-content.js +1 -0
- package/composer.json +1 -1
- package/extension.json +1 -1
- package/package.json +3 -3
- package/pnpm-workspace.yaml +1 -0
- package/server/config/telematics.php +111 -1
- package/server/migrations/2026_05_18_000001_add_license_expiry_to_drivers_table.php +25 -0
- 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/Concerns/SeedsTestingData.php +183 -0
- package/server/seeders/Testing/ConnectivitySeeder.php +213 -0
- package/server/seeders/Testing/FleetSeeder.php +341 -0
- package/server/seeders/Testing/MaintenanceSeeder.php +238 -0
- package/server/seeders/Testing/NetworkSeeder.php +250 -0
- package/server/seeders/Testing/OrdersSeeder.php +198 -0
- package/server/seeders/Testing/TestingSeeder.php +40 -0
- package/server/seeders/{VroomOrchestrationSeeder.php → Testing/VroomOrchestrationSeeder.php} +29 -22
- package/server/src/Console/Commands/AuditCustomerUserConflicts.php +75 -0
- package/server/src/Console/Commands/ProcessOperationalAlerts.php +279 -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/AnalyticsController.php +119 -0
- 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/MetricsController.php +82 -3
- package/server/src/Http/Controllers/Internal/v1/OrderController.php +441 -32
- package/server/src/Http/Controllers/Internal/v1/ServiceQuoteController.php +1 -1
- package/server/src/Http/Controllers/Internal/v1/SettingController.php +46 -1
- 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/Driver.php +2 -0
- package/server/src/Http/Resources/v1/Fleet.php +4 -0
- package/server/src/Http/Resources/v1/Index/Driver.php +48 -0
- package/server/src/Http/Resources/v1/Index/Vehicle.php +74 -1
- 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 +14 -3
- package/server/src/Models/Entity.php +2 -2
- package/server/src/Models/Order.php +29 -4
- package/server/src/Models/OrderConfig.php +14 -11
- 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 +48 -0
- package/server/src/Notifications/ProlongedStoppage.php +48 -0
- package/server/src/Notifications/RouteDeviation.php +48 -0
- package/server/src/Observers/ContactObserver.php +2 -0
- package/server/src/Observers/ServiceRateObserver.php +6 -6
- package/server/src/Orchestration/Engines/VroomOrchestrationEngine.php +29 -2
- package/server/src/Providers/FleetOpsServiceProvider.php +6 -0
- package/server/src/Providers/NotificationServiceProvider.php +3 -0
- package/server/src/Support/Analytics/AbstractAnalytics.php +77 -0
- package/server/src/Support/Analytics/FuelEfficiency.php +83 -0
- package/server/src/Support/Analytics/GeofenceViolations.php +63 -0
- package/server/src/Support/Analytics/IssuesInsights.php +67 -0
- package/server/src/Support/Analytics/LiveFleet.php +127 -0
- package/server/src/Support/Analytics/MaintenanceOverview.php +70 -0
- package/server/src/Support/Analytics/OnTimeDelivery.php +75 -0
- package/server/src/Support/Analytics/OperationsPulse.php +86 -0
- package/server/src/Support/Analytics/OrdersByStatus.php +64 -0
- package/server/src/Support/Analytics/RevenueTrend.php +78 -0
- package/server/src/Support/Analytics/TopDrivers.php +92 -0
- package/server/src/Support/CustomerAuth.php +98 -0
- package/server/src/Support/LabelPdf.php +52 -0
- package/server/src/Support/Metrics/AbstractMetric.php +170 -0
- package/server/src/Support/Metrics/ActiveLiveOrdersMetric.php +28 -0
- package/server/src/Support/Metrics/AvgOrderValueMetric.php +53 -0
- package/server/src/Support/Metrics/DriversOnlineMetric.php +30 -0
- package/server/src/Support/Metrics/EarningsMetric.php +30 -0
- package/server/src/Support/Metrics/FuelCostsMetric.php +30 -0
- package/server/src/Support/Metrics/MoneyMetric.php +30 -0
- package/server/src/Support/Metrics/OpenIssuesMetric.php +35 -0
- package/server/src/Support/Metrics/OrdersCanceledMetric.php +35 -0
- package/server/src/Support/Metrics/OrdersCompletedMetric.php +35 -0
- package/server/src/Support/Metrics/OrdersInProgressMetric.php +51 -0
- package/server/src/Support/Metrics/OrdersScheduledMetric.php +37 -0
- package/server/src/Support/Metrics/Registry.php +46 -0
- package/server/src/Support/Metrics/ResolvedIssuesMetric.php +35 -0
- package/server/src/Support/Metrics/TotalCustomersMetric.php +29 -0
- package/server/src/Support/Metrics/TotalDistanceTraveledMetric.php +35 -0
- package/server/src/Support/Metrics/TotalDriversMetric.php +28 -0
- package/server/src/Support/Metrics/TotalTimeTraveledMetric.php +35 -0
- package/server/src/Support/Metrics.php +46 -249
- package/server/src/Support/ResolvesOrderServiceStops.php +512 -0
- package/server/src/Support/Telematics/Providers/AfaqyProvider.php +290 -0
- package/server/src/Support/Telematics/Providers/SafeeProvider.php +333 -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 +50 -0
- package/server/tests/AnalyticsRoutesTest.php +41 -0
- 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 +84 -0
- 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/server/tests/VroomOrchestrationEngineTest.php +91 -0
- package/translations/en-us.yaml +38 -0
- package/server/migrations/2026_05_13_000001_repair_customer_contact_user_types.php +0 -86
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import Component from '@glimmer/component';
|
|
2
|
+
import { tracked } from '@glimmer/tracking';
|
|
3
|
+
import { action } from '@ember/object';
|
|
4
|
+
import { inject as service } from '@ember/service';
|
|
5
|
+
import { task } from 'ember-concurrency';
|
|
6
|
+
|
|
7
|
+
const SORTS = [
|
|
8
|
+
{ key: 'orders_completed', label: 'Orders' },
|
|
9
|
+
{ key: 'on_time', label: 'On-time' },
|
|
10
|
+
{ key: 'distance', label: 'Distance' },
|
|
11
|
+
];
|
|
12
|
+
|
|
13
|
+
export default class WidgetTopDriversComponent extends Component {
|
|
14
|
+
static widgetId = 'fleet-ops-top-drivers-widget';
|
|
15
|
+
|
|
16
|
+
@service fetch;
|
|
17
|
+
|
|
18
|
+
@tracked sortBy = 'orders_completed';
|
|
19
|
+
@tracked data = null;
|
|
20
|
+
@tracked error = null;
|
|
21
|
+
|
|
22
|
+
sorts = SORTS;
|
|
23
|
+
|
|
24
|
+
constructor() {
|
|
25
|
+
super(...arguments);
|
|
26
|
+
this.load.perform();
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
@task *load() {
|
|
30
|
+
try {
|
|
31
|
+
this.data = yield this.fetch.get('fleet-ops/analytics/top-drivers', {
|
|
32
|
+
period: '30d',
|
|
33
|
+
limit: 10,
|
|
34
|
+
sort_by: this.sortBy,
|
|
35
|
+
});
|
|
36
|
+
this.error = null;
|
|
37
|
+
} catch (e) {
|
|
38
|
+
this.error = e?.message ?? 'Failed to load top drivers';
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
@action
|
|
43
|
+
setSort(key) {
|
|
44
|
+
this.sortBy = key;
|
|
45
|
+
this.load.perform();
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
onTimeStatus(pct) {
|
|
49
|
+
if (typeof pct !== 'number') return 'info';
|
|
50
|
+
if (pct >= 95) return 'success';
|
|
51
|
+
if (pct >= 85) return 'warning';
|
|
52
|
+
return 'danger';
|
|
53
|
+
}
|
|
54
|
+
}
|
|
@@ -4,7 +4,7 @@ import { inject as service } from '@ember/service';
|
|
|
4
4
|
import { action } from '@ember/object';
|
|
5
5
|
import { task } from 'ember-concurrency';
|
|
6
6
|
|
|
7
|
-
const DEFAULT_PROPERTIES = { status: '
|
|
7
|
+
const DEFAULT_PROPERTIES = { status: 'available' };
|
|
8
8
|
|
|
9
9
|
export default class ManagementVehiclesIndexNewController extends Controller {
|
|
10
10
|
@service store;
|
|
@@ -103,10 +103,10 @@ export default class OperationsOrdersIndexDetailsController extends Controller {
|
|
|
103
103
|
}),
|
|
104
104
|
},
|
|
105
105
|
{
|
|
106
|
-
text: 'Unassign driver',
|
|
107
|
-
icon: 'user-xmark',
|
|
108
|
-
disabled: this.model.
|
|
109
|
-
fn: () => this.orderActions.unassignDriver(this.model),
|
|
106
|
+
text: this.model.has_driver_assigned ? 'Unassign driver' : 'Assign driver',
|
|
107
|
+
icon: this.model.has_driver_assigned ? 'user-xmark' : 'edit',
|
|
108
|
+
disabled: this.model.has_driver_assigned ? !this.model.hasActiveStatus || !this.model.driver_assigned : !this.model.hasActiveStatus,
|
|
109
|
+
fn: () => (this.model.has_driver_assigned ? this.orderActions.unassignDriver(this.model) : this.orderActions.assignDriver(this.model)),
|
|
110
110
|
},
|
|
111
111
|
{
|
|
112
112
|
text: 'View order label',
|
|
@@ -385,6 +385,20 @@ export default class OperationsOrdersIndexController extends Controller {
|
|
|
385
385
|
permission: 'fleet-ops dispatch order',
|
|
386
386
|
isVisible: (order) => order.canBeDispatched,
|
|
387
387
|
},
|
|
388
|
+
{
|
|
389
|
+
label: this.intl.t('common.assign-driver'),
|
|
390
|
+
icon: 'edit',
|
|
391
|
+
fn: this.orderActions.assignDriver,
|
|
392
|
+
permission: 'fleet-ops update order',
|
|
393
|
+
isVisible: (order) => !order.has_driver_assigned,
|
|
394
|
+
},
|
|
395
|
+
{
|
|
396
|
+
label: this.intl.t('common.unassign-driver'),
|
|
397
|
+
icon: 'edit',
|
|
398
|
+
fn: this.orderActions.unassignDriver,
|
|
399
|
+
permission: 'fleet-ops update order',
|
|
400
|
+
isVisible: (order) => order.has_driver_assigned,
|
|
401
|
+
},
|
|
388
402
|
{
|
|
389
403
|
label: this.intl.t('common.cancel-resource', { resource: this.intl.t('resource.order') }),
|
|
390
404
|
icon: 'ban',
|
|
@@ -20,6 +20,12 @@ export default class SettingsRoutingController extends Controller {
|
|
|
20
20
|
@tracked trackingRouteCacheTtlSeconds = 600;
|
|
21
21
|
@tracked trackingStaleLocationThresholdSeconds = 300;
|
|
22
22
|
@tracked trackingDefaultVehicleSpeedKph = 35;
|
|
23
|
+
@tracked lateDepartureAlertsEnabled = true;
|
|
24
|
+
@tracked lateDepartureGracePeriodMinutes = 15;
|
|
25
|
+
@tracked routeDeviationAlertsEnabled = true;
|
|
26
|
+
@tracked routeDeviationDistanceThresholdMeters = 500;
|
|
27
|
+
@tracked prolongedStoppageAlertsEnabled = true;
|
|
28
|
+
@tracked prolongedStoppageDurationThresholdMinutes = 30;
|
|
23
29
|
@tracked showTrackingAdvancedSettings = false;
|
|
24
30
|
@tracked trackingProviderOptions = [
|
|
25
31
|
{ value: 'google_routes', label: 'Google Routes' },
|
|
@@ -89,6 +95,12 @@ export default class SettingsRoutingController extends Controller {
|
|
|
89
95
|
this.trackingRouteCacheTtlSeconds = trackingSettings.route_cache_ttl_seconds ?? 600;
|
|
90
96
|
this.trackingStaleLocationThresholdSeconds = trackingSettings.stale_location_threshold_seconds ?? 300;
|
|
91
97
|
this.trackingDefaultVehicleSpeedKph = trackingSettings.default_vehicle_speed_kph ?? 35;
|
|
98
|
+
this.lateDepartureAlertsEnabled = trackingSettings.alerts?.late_departures?.enabled ?? true;
|
|
99
|
+
this.lateDepartureGracePeriodMinutes = trackingSettings.alerts?.late_departures?.grace_period_minutes ?? 15;
|
|
100
|
+
this.routeDeviationAlertsEnabled = trackingSettings.alerts?.route_deviations?.enabled ?? true;
|
|
101
|
+
this.routeDeviationDistanceThresholdMeters = trackingSettings.alerts?.route_deviations?.distance_threshold_meters ?? 500;
|
|
102
|
+
this.prolongedStoppageAlertsEnabled = trackingSettings.alerts?.prolonged_stoppages?.enabled ?? true;
|
|
103
|
+
this.prolongedStoppageDurationThresholdMinutes = trackingSettings.alerts?.prolonged_stoppages?.duration_threshold_minutes ?? 30;
|
|
92
104
|
this.trackingProviderOptions = this.normalizeProviderOptions(trackingSettings.providers ?? this.trackingProviderOptions);
|
|
93
105
|
} catch (error) {
|
|
94
106
|
this.notifications.serverError(error);
|
|
@@ -110,6 +122,20 @@ export default class SettingsRoutingController extends Controller {
|
|
|
110
122
|
route_cache_ttl_seconds: Number(this.trackingRouteCacheTtlSeconds) || 600,
|
|
111
123
|
stale_location_threshold_seconds: Number(this.trackingStaleLocationThresholdSeconds) || 300,
|
|
112
124
|
default_vehicle_speed_kph: Number(this.trackingDefaultVehicleSpeedKph) || 35,
|
|
125
|
+
alerts: {
|
|
126
|
+
late_departures: {
|
|
127
|
+
enabled: this.lateDepartureAlertsEnabled,
|
|
128
|
+
grace_period_minutes: Number(this.lateDepartureGracePeriodMinutes) || 15,
|
|
129
|
+
},
|
|
130
|
+
route_deviations: {
|
|
131
|
+
enabled: this.routeDeviationAlertsEnabled,
|
|
132
|
+
distance_threshold_meters: Number(this.routeDeviationDistanceThresholdMeters) || 500,
|
|
133
|
+
},
|
|
134
|
+
prolonged_stoppages: {
|
|
135
|
+
enabled: this.prolongedStoppageAlertsEnabled,
|
|
136
|
+
duration_threshold_minutes: Number(this.prolongedStoppageDurationThresholdMinutes) || 30,
|
|
137
|
+
},
|
|
138
|
+
},
|
|
113
139
|
};
|
|
114
140
|
}
|
|
115
141
|
|
package/addon/extension.js
CHANGED
|
@@ -128,13 +128,182 @@ export default {
|
|
|
128
128
|
|
|
129
129
|
registerWidgets(widgetService) {
|
|
130
130
|
const widgets = [
|
|
131
|
+
// Legacy monolithic 13-tile widget — kept registered for one release as
|
|
132
|
+
// users have it pinned to existing dashboards. The new KPI tile widgets
|
|
133
|
+
// below supersede it. Scheduled for removal in the next major.
|
|
131
134
|
new Widget({
|
|
132
135
|
id: 'fleet-ops-key-metrics-widget',
|
|
133
|
-
name: 'Fleet-Ops Metrics',
|
|
134
|
-
description: '
|
|
136
|
+
name: 'Fleet-Ops Metrics (Legacy)',
|
|
137
|
+
description: 'DEPRECATED — replaced by individual KPI tile widgets. Will be removed in the next major release.',
|
|
135
138
|
icon: 'truck',
|
|
136
139
|
component: new ExtensionComponent('@fleetbase/fleetops-engine', 'widget/fleet-ops-key-metrics'),
|
|
137
|
-
grid_options: { w: 12, h:
|
|
140
|
+
grid_options: { w: 12, h: 8, minW: 8, minH: 6 },
|
|
141
|
+
category: 'Legacy',
|
|
142
|
+
default: false,
|
|
143
|
+
}),
|
|
144
|
+
|
|
145
|
+
// Small KPI tile widgets — S-tier, each wraps <Widget::KpiTile> with a
|
|
146
|
+
// fixed slug pointing at GET /fleet-ops/metrics/{slug}.
|
|
147
|
+
new Widget({
|
|
148
|
+
id: 'fleet-ops-kpi-earnings-widget',
|
|
149
|
+
name: 'Earnings (30d)',
|
|
150
|
+
description: 'Total earnings over the last 30 days with trend.',
|
|
151
|
+
icon: 'sack-dollar',
|
|
152
|
+
component: new ExtensionComponent('@fleetbase/fleetops-engine', 'widget/kpi-earnings'),
|
|
153
|
+
grid_options: { w: 3, h: 4, minW: 3, minH: 4 },
|
|
154
|
+
category: 'KPI Tiles',
|
|
155
|
+
default: true,
|
|
156
|
+
}),
|
|
157
|
+
new Widget({
|
|
158
|
+
id: 'fleet-ops-kpi-aov-widget',
|
|
159
|
+
name: 'Avg Order Value (30d)',
|
|
160
|
+
description: 'Average revenue per completed order with trend.',
|
|
161
|
+
icon: 'receipt',
|
|
162
|
+
component: new ExtensionComponent('@fleetbase/fleetops-engine', 'widget/kpi-aov'),
|
|
163
|
+
grid_options: { w: 3, h: 4, minW: 3, minH: 4 },
|
|
164
|
+
category: 'KPI Tiles',
|
|
165
|
+
default: true,
|
|
166
|
+
}),
|
|
167
|
+
new Widget({
|
|
168
|
+
id: 'fleet-ops-kpi-distance-widget',
|
|
169
|
+
name: 'Distance Travelled (30d)',
|
|
170
|
+
description: 'Total kilometres delivered over the last 30 days.',
|
|
171
|
+
icon: 'route',
|
|
172
|
+
component: new ExtensionComponent('@fleetbase/fleetops-engine', 'widget/kpi-distance'),
|
|
173
|
+
grid_options: { w: 3, h: 4, minW: 3, minH: 4 },
|
|
174
|
+
category: 'KPI Tiles',
|
|
175
|
+
default: false,
|
|
176
|
+
}),
|
|
177
|
+
new Widget({
|
|
178
|
+
id: 'fleet-ops-kpi-active-orders-widget',
|
|
179
|
+
name: 'Active Orders',
|
|
180
|
+
description: 'Live count of orders currently in flight.',
|
|
181
|
+
icon: 'bolt',
|
|
182
|
+
component: new ExtensionComponent('@fleetbase/fleetops-engine', 'widget/kpi-active-orders'),
|
|
183
|
+
grid_options: { w: 3, h: 4, minW: 3, minH: 4 },
|
|
184
|
+
category: 'KPI Tiles',
|
|
185
|
+
default: true,
|
|
186
|
+
}),
|
|
187
|
+
new Widget({
|
|
188
|
+
id: 'fleet-ops-kpi-drivers-online-widget',
|
|
189
|
+
name: 'Drivers Online',
|
|
190
|
+
description: 'Live count of drivers currently active on a job.',
|
|
191
|
+
icon: 'id-card',
|
|
192
|
+
component: new ExtensionComponent('@fleetbase/fleetops-engine', 'widget/kpi-drivers-online'),
|
|
193
|
+
grid_options: { w: 3, h: 4, minW: 3, minH: 4 },
|
|
194
|
+
category: 'KPI Tiles',
|
|
195
|
+
default: true,
|
|
196
|
+
}),
|
|
197
|
+
new Widget({
|
|
198
|
+
id: 'fleet-ops-kpi-open-issues-widget',
|
|
199
|
+
name: 'Open Issues',
|
|
200
|
+
description: 'Pending issues across the fleet (lower is better).',
|
|
201
|
+
icon: 'triangle-exclamation',
|
|
202
|
+
component: new ExtensionComponent('@fleetbase/fleetops-engine', 'widget/kpi-open-issues'),
|
|
203
|
+
grid_options: { w: 3, h: 4, minW: 3, minH: 4 },
|
|
204
|
+
category: 'KPI Tiles',
|
|
205
|
+
default: false,
|
|
206
|
+
}),
|
|
207
|
+
|
|
208
|
+
// Analytics widgets — composed visualizations powered by /fleet-ops/analytics/*.
|
|
209
|
+
new Widget({
|
|
210
|
+
id: 'fleet-ops-operations-pulse-widget',
|
|
211
|
+
name: 'Operations Pulse',
|
|
212
|
+
description: 'Live operational snapshot with day-over-day deltas.',
|
|
213
|
+
icon: 'wave-pulse',
|
|
214
|
+
component: new ExtensionComponent('@fleetbase/fleetops-engine', 'widget/operations-pulse'),
|
|
215
|
+
grid_options: { w: 6, h: 6, minW: 5, minH: 5 },
|
|
216
|
+
category: 'Analytics',
|
|
217
|
+
default: false,
|
|
218
|
+
}),
|
|
219
|
+
new Widget({
|
|
220
|
+
id: 'fleet-ops-revenue-trend-widget',
|
|
221
|
+
name: 'Revenue Trend',
|
|
222
|
+
description: 'Revenue over time with period comparison.',
|
|
223
|
+
icon: 'chart-line',
|
|
224
|
+
component: new ExtensionComponent('@fleetbase/fleetops-engine', 'widget/revenue-trend'),
|
|
225
|
+
grid_options: { w: 6, h: 6, minW: 5, minH: 5 },
|
|
226
|
+
category: 'Analytics',
|
|
227
|
+
default: true,
|
|
228
|
+
}),
|
|
229
|
+
new Widget({
|
|
230
|
+
id: 'fleet-ops-orders-by-status-widget',
|
|
231
|
+
name: 'Order Volume by Status',
|
|
232
|
+
description: 'Daily stacked bars of order counts by status.',
|
|
233
|
+
icon: 'chart-column',
|
|
234
|
+
component: new ExtensionComponent('@fleetbase/fleetops-engine', 'widget/orders-by-status'),
|
|
235
|
+
grid_options: { w: 6, h: 6, minW: 5, minH: 5 },
|
|
236
|
+
category: 'Analytics',
|
|
237
|
+
default: false,
|
|
238
|
+
}),
|
|
239
|
+
new Widget({
|
|
240
|
+
id: 'fleet-ops-on-time-delivery-widget',
|
|
241
|
+
name: 'On-Time Delivery',
|
|
242
|
+
description: 'Percentage of deliveries completed within the SLA window.',
|
|
243
|
+
icon: 'clock',
|
|
244
|
+
component: new ExtensionComponent('@fleetbase/fleetops-engine', 'widget/on-time-delivery'),
|
|
245
|
+
grid_options: { w: 3, h: 4, minW: 3, minH: 4 },
|
|
246
|
+
category: 'Analytics',
|
|
247
|
+
default: false,
|
|
248
|
+
}),
|
|
249
|
+
new Widget({
|
|
250
|
+
id: 'fleet-ops-top-drivers-widget',
|
|
251
|
+
name: 'Top Drivers',
|
|
252
|
+
description: 'Driver leaderboard sortable by orders, on-time %, or distance.',
|
|
253
|
+
icon: 'medal',
|
|
254
|
+
component: new ExtensionComponent('@fleetbase/fleetops-engine', 'widget/top-drivers'),
|
|
255
|
+
grid_options: { w: 6, h: 6, minW: 5, minH: 5 },
|
|
256
|
+
category: 'Analytics',
|
|
257
|
+
default: false,
|
|
258
|
+
}),
|
|
259
|
+
new Widget({
|
|
260
|
+
id: 'fleet-ops-fuel-efficiency-widget',
|
|
261
|
+
name: 'Fuel Cost & Efficiency',
|
|
262
|
+
description: 'Weekly fuel cost and cost-per-km trend.',
|
|
263
|
+
icon: 'gas-pump',
|
|
264
|
+
component: new ExtensionComponent('@fleetbase/fleetops-engine', 'widget/fuel-efficiency'),
|
|
265
|
+
grid_options: { w: 6, h: 6, minW: 5, minH: 5 },
|
|
266
|
+
category: 'Analytics',
|
|
267
|
+
default: false,
|
|
268
|
+
}),
|
|
269
|
+
new Widget({
|
|
270
|
+
id: 'fleet-ops-issues-insights-widget',
|
|
271
|
+
name: 'Issues Insights',
|
|
272
|
+
description: 'Open/resolved issues, category breakdown, and average resolution time.',
|
|
273
|
+
icon: 'triangle-exclamation',
|
|
274
|
+
component: new ExtensionComponent('@fleetbase/fleetops-engine', 'widget/issues-insights'),
|
|
275
|
+
grid_options: { w: 6, h: 6, minW: 5, minH: 5 },
|
|
276
|
+
category: 'Analytics',
|
|
277
|
+
default: false,
|
|
278
|
+
}),
|
|
279
|
+
new Widget({
|
|
280
|
+
id: 'fleet-ops-maintenance-overview-widget',
|
|
281
|
+
name: 'Maintenance Overview',
|
|
282
|
+
description: 'Overdue, scheduled, and YTD maintenance spend.',
|
|
283
|
+
icon: 'wrench',
|
|
284
|
+
component: new ExtensionComponent('@fleetbase/fleetops-engine', 'widget/maintenance-overview'),
|
|
285
|
+
grid_options: { w: 6, h: 6, minW: 5, minH: 5 },
|
|
286
|
+
category: 'Analytics',
|
|
287
|
+
default: true,
|
|
288
|
+
}),
|
|
289
|
+
new Widget({
|
|
290
|
+
id: 'fleet-ops-geofence-violations-widget',
|
|
291
|
+
name: 'Geofence Violations',
|
|
292
|
+
description: 'Dwell-time outliers and geofence event hotspots.',
|
|
293
|
+
icon: 'location-crosshairs',
|
|
294
|
+
component: new ExtensionComponent('@fleetbase/fleetops-engine', 'widget/geofence-violations'),
|
|
295
|
+
grid_options: { w: 6, h: 6, minW: 5, minH: 5 },
|
|
296
|
+
category: 'Analytics',
|
|
297
|
+
default: false,
|
|
298
|
+
}),
|
|
299
|
+
new Widget({
|
|
300
|
+
id: 'fleet-ops-live-fleet-widget',
|
|
301
|
+
name: 'Live Fleet Map',
|
|
302
|
+
description: 'Real-time driver positions and active routes.',
|
|
303
|
+
icon: 'map-location-dot',
|
|
304
|
+
component: new ExtensionComponent('@fleetbase/fleetops-engine', 'widget/live-fleet'),
|
|
305
|
+
grid_options: { w: 12, h: 10, minW: 8, minH: 8 },
|
|
306
|
+
category: 'Maps',
|
|
138
307
|
default: true,
|
|
139
308
|
}),
|
|
140
309
|
];
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { helper } from '@ember/component/helper';
|
|
2
2
|
import placeAddressHtml from '../utils/place-address-html';
|
|
3
3
|
|
|
4
|
-
export default helper(function placeAddressHtmlHelper([place]) {
|
|
5
|
-
return placeAddressHtml(place);
|
|
4
|
+
export default helper(function placeAddressHtmlHelper([place], hash = {}) {
|
|
5
|
+
return placeAddressHtml(place, hash);
|
|
6
6
|
});
|
|
@@ -27,7 +27,7 @@ export default class OperationsServiceRatesIndexRoute extends Route {
|
|
|
27
27
|
model(params) {
|
|
28
28
|
return this.store.query('service-rate', {
|
|
29
29
|
...params,
|
|
30
|
-
with: ['parcelFees', 'rateFees', 'zone', 'serviceArea'],
|
|
30
|
+
with: ['parcelFees', 'rateFees.zone', 'rateFees.serviceArea', 'zone', 'serviceArea'],
|
|
31
31
|
});
|
|
32
32
|
}
|
|
33
33
|
}
|
|
@@ -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
|
*
|