@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,168 @@
|
|
|
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
|
+
/**
|
|
8
|
+
* <Widget::KpiTile @slug="earnings" @title="..." @format="money" @showDelta={{true}}
|
|
9
|
+
* @showSparkline={{true}} @deltaInverse={{false}} @period="30d" />
|
|
10
|
+
*
|
|
11
|
+
* Renders a single KPI tile backed by `/fleet-ops/metrics/{slug}`. Hosts the
|
|
12
|
+
* format-specific value rendering, the optional period-over-period delta
|
|
13
|
+
* badge (with `deltaInverse` to flip semantics for inverse-good metrics like
|
|
14
|
+
* open_issues), and the optional sparkline line chart.
|
|
15
|
+
*/
|
|
16
|
+
export default class WidgetKpiTileComponent extends Component {
|
|
17
|
+
@service fetch;
|
|
18
|
+
@tracked data = null;
|
|
19
|
+
@tracked error = null;
|
|
20
|
+
|
|
21
|
+
get period() {
|
|
22
|
+
return this.args.period ?? '30d';
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
get showDelta() {
|
|
26
|
+
return this.args.showDelta ?? true;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
get showSparkline() {
|
|
30
|
+
return this.args.showSparkline ?? true;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
get deltaInverse() {
|
|
34
|
+
return this.args.deltaInverse ?? false;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
get format() {
|
|
38
|
+
return this.args.format ?? this.data?.format ?? 'count';
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
get currency() {
|
|
42
|
+
return this.data?.currency ?? 'USD';
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
get deltaPct() {
|
|
46
|
+
return this.data?.delta_pct;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
get hasDelta() {
|
|
50
|
+
return this.showDelta && typeof this.deltaPct === 'number';
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
get deltaStatus() {
|
|
54
|
+
const pct = this.deltaPct;
|
|
55
|
+
if (typeof pct !== 'number' || pct === 0) return 'info';
|
|
56
|
+
const positive = pct > 0;
|
|
57
|
+
const isGood = this.deltaInverse ? !positive : positive;
|
|
58
|
+
return isGood ? 'success' : 'danger';
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
get deltaText() {
|
|
62
|
+
const pct = this.deltaPct;
|
|
63
|
+
const sign = pct > 0 ? '+' : '';
|
|
64
|
+
return `${sign}${pct}%`;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Tints the card subtly based on trend direction so the dashboard pops. */
|
|
68
|
+
get cardAccentClass() {
|
|
69
|
+
const pct = this.deltaPct;
|
|
70
|
+
if (typeof pct !== 'number' || pct === 0) {
|
|
71
|
+
return 'kpi-accent-neutral';
|
|
72
|
+
}
|
|
73
|
+
const positive = pct > 0;
|
|
74
|
+
const isGood = this.deltaInverse ? !positive : positive;
|
|
75
|
+
return isGood ? 'kpi-accent-good' : 'kpi-accent-bad';
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
get valueTextClass() {
|
|
79
|
+
const pct = this.deltaPct;
|
|
80
|
+
if (typeof pct !== 'number' || pct === 0) {
|
|
81
|
+
return 'text-black dark:text-gray-100';
|
|
82
|
+
}
|
|
83
|
+
const positive = pct > 0;
|
|
84
|
+
const isGood = this.deltaInverse ? !positive : positive;
|
|
85
|
+
return isGood ? 'text-emerald-600 dark:text-emerald-400' : 'text-rose-600 dark:text-rose-400';
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** Neon palette per delta direction. Drives line + fill + value highlight. */
|
|
89
|
+
get accentColors() {
|
|
90
|
+
// Bright neon-ish trio that pops on the dark theme without being garish in light.
|
|
91
|
+
const good = { line: '#22c55e', fill: 'rgba(34, 197, 94, 0.22)', glow: 'rgba(34, 197, 94, 0.35)' };
|
|
92
|
+
const bad = { line: '#ef4444', fill: 'rgba(239, 68, 68, 0.22)', glow: 'rgba(239, 68, 68, 0.35)' };
|
|
93
|
+
const neutral = { line: '#3485e2', fill: 'rgba(52, 133, 226, 0.18)', glow: 'rgba(52, 133, 226, 0.30)' };
|
|
94
|
+
|
|
95
|
+
const pct = this.deltaPct;
|
|
96
|
+
if (typeof pct !== 'number' || pct === 0) return neutral;
|
|
97
|
+
const positive = pct > 0;
|
|
98
|
+
const isGood = this.deltaInverse ? !positive : positive;
|
|
99
|
+
return isGood ? good : bad;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
get sparklineDatasets() {
|
|
103
|
+
const points = this.data?.sparkline?.data;
|
|
104
|
+
if (!points || points.length === 0) return null;
|
|
105
|
+
|
|
106
|
+
const { line, fill } = this.accentColors;
|
|
107
|
+
|
|
108
|
+
return [
|
|
109
|
+
{
|
|
110
|
+
data: points,
|
|
111
|
+
borderColor: line,
|
|
112
|
+
backgroundColor: fill,
|
|
113
|
+
pointRadius: 0,
|
|
114
|
+
borderWidth: 1.75,
|
|
115
|
+
tension: 0.4,
|
|
116
|
+
// Anchor the area fill to the chart's y=0 axis so the sparkline
|
|
117
|
+
// visually sits at the BOTTOM of the card regardless of the data range.
|
|
118
|
+
fill: 'origin',
|
|
119
|
+
},
|
|
120
|
+
];
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
get sparklineLabels() {
|
|
124
|
+
return this.data?.sparkline?.labels ?? [];
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
get sparklineOptions() {
|
|
128
|
+
return {
|
|
129
|
+
responsive: true,
|
|
130
|
+
maintainAspectRatio: false,
|
|
131
|
+
animation: { duration: 600, easing: 'easeOutQuart' },
|
|
132
|
+
plugins: {
|
|
133
|
+
legend: { display: false },
|
|
134
|
+
tooltip: { enabled: false },
|
|
135
|
+
},
|
|
136
|
+
scales: {
|
|
137
|
+
x: { display: false },
|
|
138
|
+
// Lock min to 0 so flat-zero data sits flush along the bottom edge.
|
|
139
|
+
// suggestedMin/Max keep the line off the very top/bottom for non-zero data.
|
|
140
|
+
y: { display: false, min: 0, beginAtZero: true, grace: '5%' },
|
|
141
|
+
},
|
|
142
|
+
elements: { line: { borderJoinStyle: 'round' } },
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
constructor() {
|
|
147
|
+
super(...arguments);
|
|
148
|
+
this.load.perform();
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
@task *load() {
|
|
152
|
+
try {
|
|
153
|
+
this.data = yield this.fetch.get(`fleet-ops/metrics/${this.args.slug}`, {
|
|
154
|
+
period: this.period,
|
|
155
|
+
sparkline: this.showSparkline,
|
|
156
|
+
compare: this.showDelta,
|
|
157
|
+
});
|
|
158
|
+
this.error = null;
|
|
159
|
+
} catch (e) {
|
|
160
|
+
this.error = e?.message ?? 'Failed to load metric';
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
@action
|
|
165
|
+
refresh() {
|
|
166
|
+
this.load.perform();
|
|
167
|
+
}
|
|
168
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
<div class="fleet-ops-widget fleet-ops-live-fleet flex flex-col h-full {{@class}}" ...attributes>
|
|
2
|
+
<div class="flex items-center justify-between px-3 py-1.5 border dark:border-gray-700 border-gray-200 dark:bg-gray-800 bg-gray-50 rounded-t-md shadow-sm">
|
|
3
|
+
<span class="text-xs font-bold text-black dark:text-gray-100">{{t "widget.live-fleet.title"}}</span>
|
|
4
|
+
<div class="flex items-center gap-3 text-[10px] text-gray-500 dark:text-gray-400">
|
|
5
|
+
<span class="flex items-center" title="Drivers"><FaIcon @icon="id-card" class="mr-1" />{{this.drivers.length}}</span>
|
|
6
|
+
<span class="flex items-center" title="Vehicles"><FaIcon @icon="truck" class="mr-1" />{{this.vehicles.length}}</span>
|
|
7
|
+
<span class="flex items-center" title="Active orders"><FaIcon @icon="bolt" class="mr-1" />{{this.activeOrderCount}}</span>
|
|
8
|
+
<button type="button" class="text-gray-400 hover:text-gray-600 dark:hover:text-gray-200" {{on "click" this.refresh}}>
|
|
9
|
+
<FaIcon @icon="rotate" class={{if this.load.isRunning "animate-spin"}} />
|
|
10
|
+
</button>
|
|
11
|
+
</div>
|
|
12
|
+
</div>
|
|
13
|
+
|
|
14
|
+
<div class="flex-1 relative border border-t-0 dark:border-gray-700 border-gray-200 rounded-b-md overflow-hidden min-h-0 bg-gray-100 dark:bg-gray-900">
|
|
15
|
+
<div class="fleetbase-leaflet-map-container w-full h-full" {{set-container-dimensions}}>
|
|
16
|
+
<LeafletMap
|
|
17
|
+
class="w-full h-full"
|
|
18
|
+
@lat={{get this.center 0}}
|
|
19
|
+
@lng={{get this.center 1}}
|
|
20
|
+
@zoom={{this.zoom}}
|
|
21
|
+
@zoomControl={{true}}
|
|
22
|
+
@onLoad={{this.onMapLoad}}
|
|
23
|
+
{{set-container-dimensions}}
|
|
24
|
+
as |layers|
|
|
25
|
+
>
|
|
26
|
+
<layers.tile @url={{this.tileUrl}} />
|
|
27
|
+
|
|
28
|
+
{{#if this.load.isIdle}}
|
|
29
|
+
{{! Drivers — fleetops' rotating tracking-marker matches the operational
|
|
30
|
+
live-map's look and animates on heading changes. }}
|
|
31
|
+
{{#each this.drivers as |driver|}}
|
|
32
|
+
<layers.tracking-marker
|
|
33
|
+
@id={{driver.uuid}}
|
|
34
|
+
@publicId={{driver.public_id}}
|
|
35
|
+
@location={{array driver.lat driver.lng}}
|
|
36
|
+
@rotationAngle={{driver.heading}}
|
|
37
|
+
@icon={{icon iconUrl=(or driver.vehicle_avatar driver.avatar_url "/engines-dist/images/map-marker.png") iconSize=(array 28 28) iconAnchor=(array 14 28)}}
|
|
38
|
+
@draggable={{false}}
|
|
39
|
+
as |marker|
|
|
40
|
+
>
|
|
41
|
+
<marker.popup @maxWidth="240" @minWidth="180">
|
|
42
|
+
<div class="flex flex-row gap-2">
|
|
43
|
+
<div class="w-9 h-9 flex-shrink-0">
|
|
44
|
+
<img src={{or driver.avatar_url driver.vehicle_avatar}} alt={{driver.name}} class="rounded-md w-9 h-9 object-cover shadow-sm" />
|
|
45
|
+
</div>
|
|
46
|
+
<div class="flex-1 min-w-0">
|
|
47
|
+
<div class="text-xs font-semibold truncate">{{or driver.name "Driver"}}</div>
|
|
48
|
+
<div class="text-[11px]">
|
|
49
|
+
{{#if driver.current_order_uuid}}
|
|
50
|
+
<span class="text-blue-500"><FaIcon @icon="bolt" class="mr-1" />On order</span>
|
|
51
|
+
{{else if driver.online}}
|
|
52
|
+
<span class="text-green-500"><FaIcon @icon="circle" @size="2xs" class="mr-1" />Online</span>
|
|
53
|
+
{{else}}
|
|
54
|
+
<span class="text-gray-400"><FaIcon @icon="circle" @size="2xs" class="mr-1" />Offline</span>
|
|
55
|
+
{{/if}}
|
|
56
|
+
</div>
|
|
57
|
+
<div class="text-[10px] text-gray-400 mt-0.5 truncate">{{driver.lat}}, {{driver.lng}}</div>
|
|
58
|
+
</div>
|
|
59
|
+
</div>
|
|
60
|
+
</marker.popup>
|
|
61
|
+
<marker.tooltip @permanent={{false}} @sticky={{true}} @direction="top">
|
|
62
|
+
<div class="text-[11px] font-semibold">{{or driver.name "Driver"}}</div>
|
|
63
|
+
</marker.tooltip>
|
|
64
|
+
</layers.tracking-marker>
|
|
65
|
+
{{/each}}
|
|
66
|
+
|
|
67
|
+
{{! Vehicles — also rendered via tracking-marker so the heading
|
|
68
|
+
animation applies on telematics-driven position updates. }}
|
|
69
|
+
{{#each this.vehicles as |vehicle|}}
|
|
70
|
+
<layers.tracking-marker
|
|
71
|
+
@id={{vehicle.uuid}}
|
|
72
|
+
@publicId={{vehicle.public_id}}
|
|
73
|
+
@location={{array vehicle.lat vehicle.lng}}
|
|
74
|
+
@rotationAngle={{vehicle.heading}}
|
|
75
|
+
@icon={{icon iconUrl=(or vehicle.avatar_url vehicle.photo_url "/engines-dist/images/map-marker.png") iconSize=(array 28 28) iconAnchor=(array 14 28)}}
|
|
76
|
+
@draggable={{false}}
|
|
77
|
+
as |marker|
|
|
78
|
+
>
|
|
79
|
+
<marker.popup @maxWidth="240" @minWidth="180">
|
|
80
|
+
<div class="flex flex-row gap-2">
|
|
81
|
+
<div class="w-9 h-9 flex-shrink-0">
|
|
82
|
+
<img src={{or vehicle.photo_url vehicle.avatar_url}} alt={{vehicle.name}} class="rounded-md w-9 h-9 object-cover shadow-sm" />
|
|
83
|
+
</div>
|
|
84
|
+
<div class="flex-1 min-w-0">
|
|
85
|
+
<div class="text-xs font-semibold truncate">{{or vehicle.name "Vehicle"}}</div>
|
|
86
|
+
{{#if vehicle.plate_number}}
|
|
87
|
+
<div class="text-[10px] text-gray-500 truncate">{{vehicle.plate_number}}</div>
|
|
88
|
+
{{/if}}
|
|
89
|
+
{{#if vehicle.driver_name}}
|
|
90
|
+
<div class="text-[11px] text-gray-600 dark:text-gray-300 truncate">
|
|
91
|
+
<FaIcon @icon="id-card" class="mr-1 text-gray-400" />{{vehicle.driver_name}}
|
|
92
|
+
</div>
|
|
93
|
+
{{/if}}
|
|
94
|
+
<div class="text-[10px] text-gray-400 mt-0.5 truncate">{{vehicle.lat}}, {{vehicle.lng}}</div>
|
|
95
|
+
</div>
|
|
96
|
+
</div>
|
|
97
|
+
</marker.popup>
|
|
98
|
+
<marker.tooltip @permanent={{false}} @sticky={{true}} @direction="top">
|
|
99
|
+
<div class="text-[11px] font-semibold">{{or vehicle.name "Vehicle"}}</div>
|
|
100
|
+
</marker.tooltip>
|
|
101
|
+
</layers.tracking-marker>
|
|
102
|
+
{{/each}}
|
|
103
|
+
{{/if}}
|
|
104
|
+
</LeafletMap>
|
|
105
|
+
</div>
|
|
106
|
+
|
|
107
|
+
{{#if this.load.isRunning}}
|
|
108
|
+
<div class="absolute inset-0 flex items-center justify-center bg-white/70 dark:bg-gray-900/70 z-[400]">
|
|
109
|
+
<Spinner />
|
|
110
|
+
</div>
|
|
111
|
+
{{else if this.error}}
|
|
112
|
+
<div class="absolute inset-0 flex items-center justify-center text-xs text-red-500 z-[400]">{{this.error}}</div>
|
|
113
|
+
{{else if (not this.totalMarkerCount)}}
|
|
114
|
+
<div class="absolute inset-0 flex items-center justify-center pointer-events-none">
|
|
115
|
+
<div class="text-center text-[11px] text-gray-500 dark:text-gray-400 bg-white/90 dark:bg-gray-900/90 rounded-md px-3 py-2 shadow-sm">
|
|
116
|
+
<FaIcon @icon="map-location-dot" class="text-xl block mb-1 opacity-50" />
|
|
117
|
+
No active drivers or vehicles to display.
|
|
118
|
+
</div>
|
|
119
|
+
</div>
|
|
120
|
+
{{/if}}
|
|
121
|
+
</div>
|
|
122
|
+
</div>
|
|
@@ -0,0 +1,365 @@
|
|
|
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 { getOwner } from '@ember/application';
|
|
6
|
+
import { debug } from '@ember/debug';
|
|
7
|
+
import { task } from 'ember-concurrency';
|
|
8
|
+
import { renderCompleted, waitForInsertedAndSized } from '@fleetbase/ember-ui/utils/dom';
|
|
9
|
+
import LeafletTrackingMarkerComponent from '../leaflet-tracking-marker';
|
|
10
|
+
|
|
11
|
+
const DEFAULT_CENTER = [1.31, 103.85];
|
|
12
|
+
const DEFAULT_ZOOM = 11;
|
|
13
|
+
// Match the rest of fleetops' Leaflet maps (light Carto basemap), which keeps the
|
|
14
|
+
// dashboard styling consistent with the operational live map. Falls back gracefully
|
|
15
|
+
// in dark mode via the next-leaflet-container-map theming.
|
|
16
|
+
const TILE_URL = 'https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}.png';
|
|
17
|
+
const TILE_URL_DARK = 'https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}.png';
|
|
18
|
+
const TILE_ATTRIBUTION = '© OpenStreetMap contributors © CARTO';
|
|
19
|
+
const RECONCILE_INTERVAL_MS = 5 * 60_000;
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Real-time fleet map. Initial GET seeds driver positions; the widget then
|
|
23
|
+
* subscribes to `company.{uuid}` and applies driver.location_changed deltas
|
|
24
|
+
* directly to the markers list. Full reconciliation every 5 minutes.
|
|
25
|
+
*/
|
|
26
|
+
export default class WidgetLiveFleetComponent extends Component {
|
|
27
|
+
static widgetId = 'fleet-ops-live-fleet-widget';
|
|
28
|
+
|
|
29
|
+
@service fetch;
|
|
30
|
+
@service socket;
|
|
31
|
+
@service currentUser;
|
|
32
|
+
|
|
33
|
+
@tracked data = null;
|
|
34
|
+
@tracked error = null;
|
|
35
|
+
|
|
36
|
+
tileAttribution = TILE_ATTRIBUTION;
|
|
37
|
+
|
|
38
|
+
get tileUrl() {
|
|
39
|
+
const isDark = typeof document !== 'undefined' && document.documentElement?.dataset?.theme === 'dark';
|
|
40
|
+
return isDark ? TILE_URL_DARK : TILE_URL;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
socketChannel = null;
|
|
44
|
+
socketActive = false;
|
|
45
|
+
reconcileTimer = null;
|
|
46
|
+
/** Watches the map container for size changes (gridstack settle, sibling widgets
|
|
47
|
+
* loading, theme/font swaps) and re-runs Leaflet's invalidateSize so the tile
|
|
48
|
+
* layer always fills the actual painted bounds, not the bounds Leaflet measured
|
|
49
|
+
* at first paint. */
|
|
50
|
+
#resizeObserver = null;
|
|
51
|
+
#resizeRaf = 0;
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Deferred that resolves once <LeafletMap @onLoad> fires. Mirrors the
|
|
55
|
+
* leaflet-map-manager service's #mapSetPromise so we can synchronously
|
|
56
|
+
* wait for the map instance from anywhere in this component.
|
|
57
|
+
*/
|
|
58
|
+
#map = null;
|
|
59
|
+
#mapSetPromise = null;
|
|
60
|
+
#resolveMapSet = null;
|
|
61
|
+
|
|
62
|
+
constructor() {
|
|
63
|
+
super(...arguments);
|
|
64
|
+
// Important: register tracking-marker against THIS owner's ember-leaflet
|
|
65
|
+
// service before any <LeafletMap> in our template is constructed.
|
|
66
|
+
//
|
|
67
|
+
// The fleetops `register-leaflet-tracking-marker` instance-initializer
|
|
68
|
+
// registers `tracking-marker` on the fleetops ENGINE's ember-leaflet
|
|
69
|
+
// service. When this widget is rendered from the host dashboard route
|
|
70
|
+
// (e.g. console.home), LazyEngineComponent resolves the component class
|
|
71
|
+
// from the engine but renders it under the HOST owner — so the
|
|
72
|
+
// <LeafletMap> inside this template injects the host's ember-leaflet
|
|
73
|
+
// service, which never received the registration. The yield for
|
|
74
|
+
// `layers.tracking-marker` silently resolves to undefined and no marker
|
|
75
|
+
// ever paints. Doing the registration on whatever owner this widget
|
|
76
|
+
// happens to live under closes that gap.
|
|
77
|
+
this.#ensureTrackingMarkerRegistered();
|
|
78
|
+
this.#patchLeafletDrawInitHook();
|
|
79
|
+
this.#resetMapDeferred();
|
|
80
|
+
this.load.perform();
|
|
81
|
+
this.subscribe();
|
|
82
|
+
this.reconcileTimer = setInterval(() => this.load.perform(), RECONCILE_INTERVAL_MS);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* leaflet-draw's `Edit.Marker.js` registers an init hook on `L.Marker` that
|
|
87
|
+
* does `if (L.Edit.Marker) { this.editing = new L.Edit.Marker(this); … }`.
|
|
88
|
+
* The check is unguarded — it reads `.Marker` off `L.Edit` directly. When
|
|
89
|
+
* leaflet-draw is loaded but the dashboard route's window.L doesn't carry
|
|
90
|
+
* the `Edit` namespace (load-order race in the host route, or two leaflet
|
|
91
|
+
* copies in the runtime caused by engine/host module duplication), every
|
|
92
|
+
* marker creation throws a TypeError and our tracking-marker churn during
|
|
93
|
+
* dashboard switching repeats it thousands of times.
|
|
94
|
+
*
|
|
95
|
+
* Stubbing `L.Edit` to `{}` keeps the leaflet-draw hook's check (`if
|
|
96
|
+
* (L.Edit.Marker)`) safely falsy when the namespace is missing, so it
|
|
97
|
+
* becomes a no-op. If leaflet-draw HAS fully loaded, `L.Edit.Marker` is
|
|
98
|
+
* already set and the `||` keeps it.
|
|
99
|
+
*
|
|
100
|
+
* We don't need draw/editing handlers on dashboard markers anyway — this
|
|
101
|
+
* widget is read-only.
|
|
102
|
+
*/
|
|
103
|
+
#patchLeafletDrawInitHook() {
|
|
104
|
+
const L = typeof window !== 'undefined' ? (window.L ?? window.leaflet) : null;
|
|
105
|
+
if (L && !L.Edit) {
|
|
106
|
+
L.Edit = {};
|
|
107
|
+
}
|
|
108
|
+
if (L && !L.Marker) {
|
|
109
|
+
L.Marker = {};
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Idempotent: registers the fleetops `tracking-marker` component on the
|
|
115
|
+
* `ember-leaflet` service belonging to this widget's owner if it isn't
|
|
116
|
+
* already there. Safe to call multiple times across widget instances
|
|
117
|
+
* because we guard on `components.find(...)` before calling
|
|
118
|
+
* `registerComponent` (which throws on duplicate registration).
|
|
119
|
+
*/
|
|
120
|
+
#ensureTrackingMarkerRegistered() {
|
|
121
|
+
const owner = getOwner(this);
|
|
122
|
+
const emberLeaflet = owner?.lookup?.('service:ember-leaflet');
|
|
123
|
+
if (!emberLeaflet) return;
|
|
124
|
+
|
|
125
|
+
const already = emberLeaflet.components?.find((c) => c.name === 'leaflet-tracking-marker' || c.as === 'tracking-marker');
|
|
126
|
+
if (already) return;
|
|
127
|
+
|
|
128
|
+
emberLeaflet.registerComponent('leaflet-tracking-marker', {
|
|
129
|
+
as: 'tracking-marker',
|
|
130
|
+
component: LeafletTrackingMarkerComponent,
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
willDestroy() {
|
|
135
|
+
super.willDestroy(...arguments);
|
|
136
|
+
this.unsubscribe();
|
|
137
|
+
if (this.reconcileTimer) clearInterval(this.reconcileTimer);
|
|
138
|
+
if (this.#resizeRaf) cancelAnimationFrame(this.#resizeRaf);
|
|
139
|
+
this.#resizeObserver?.disconnect();
|
|
140
|
+
this.#resizeObserver = null;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
get drivers() {
|
|
144
|
+
return this.data?.drivers ?? [];
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
get vehicles() {
|
|
148
|
+
return this.data?.vehicles ?? [];
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
get activeOrderCount() {
|
|
152
|
+
return this.data?.active_orders?.length ?? 0;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/** Both drivers and vehicles are renderable map subjects — total chip count. */
|
|
156
|
+
get totalMarkerCount() {
|
|
157
|
+
return this.drivers.length + this.vehicles.length;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/** Center on the first available marker (driver, then vehicle), else default. */
|
|
161
|
+
get center() {
|
|
162
|
+
const first = this.drivers[0] ?? this.vehicles[0];
|
|
163
|
+
if (first?.lat && first?.lng) return [first.lat, first.lng];
|
|
164
|
+
return DEFAULT_CENTER;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
get zoom() {
|
|
168
|
+
return DEFAULT_ZOOM;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
statusColor(status) {
|
|
172
|
+
switch (status) {
|
|
173
|
+
case 'in_progress':
|
|
174
|
+
case 'enroute':
|
|
175
|
+
case 'dispatched':
|
|
176
|
+
return '#3485e2';
|
|
177
|
+
case 'completed':
|
|
178
|
+
return '#22c55e';
|
|
179
|
+
default:
|
|
180
|
+
return '#9ca3af';
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
@task *load() {
|
|
185
|
+
try {
|
|
186
|
+
// Wait for the LeafletMap to mount, finish loading, and have a non-zero
|
|
187
|
+
// container before we add markers. Without this, markers added to a
|
|
188
|
+
// not-yet-sized map silently no-op (the data arrives, count chip ticks
|
|
189
|
+
// up, but nothing paints).
|
|
190
|
+
yield this.ensureInteractive();
|
|
191
|
+
this.data = yield this.fetch.get('fleet-ops/analytics/live-fleet');
|
|
192
|
+
this.error = null;
|
|
193
|
+
} catch (e) {
|
|
194
|
+
this.error = e?.message ?? 'Failed to load live fleet';
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Mirrors LeafletMapManagerService#ensureInteractive: waits for the
|
|
200
|
+
* Leaflet instance to be available + loaded, for the host DOM container
|
|
201
|
+
* to be inserted and sized, then forces a re-measure. Returns the map.
|
|
202
|
+
*/
|
|
203
|
+
async ensureInteractive({ timeoutMs = 8000 } = {}) {
|
|
204
|
+
const map = await this.#whenMapLoaded({ timeoutMs });
|
|
205
|
+
|
|
206
|
+
const getContainer = () => map?.getContainer?.() || map?._container || null;
|
|
207
|
+
|
|
208
|
+
await renderCompleted();
|
|
209
|
+
await waitForInsertedAndSized(getContainer, { timeoutMs });
|
|
210
|
+
|
|
211
|
+
map.invalidateSize(false);
|
|
212
|
+
|
|
213
|
+
// Subsequent size changes (gridstack settling, sibling widgets loading,
|
|
214
|
+
// theme/font swaps, widget resize handles) need invalidateSize() again
|
|
215
|
+
// or the tile layer ends up clipped to the cached bounding box.
|
|
216
|
+
this.#attachResizeObserver(map, getContainer());
|
|
217
|
+
|
|
218
|
+
return map;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Watch the Leaflet container with a ResizeObserver and re-run invalidateSize
|
|
223
|
+
* on any dimension change. rAF-debounced so a stream of resize callbacks during
|
|
224
|
+
* a gridstack drag/animation collapses into one Leaflet recompute per frame.
|
|
225
|
+
*/
|
|
226
|
+
#attachResizeObserver(map, container) {
|
|
227
|
+
if (!container || typeof ResizeObserver === 'undefined') return;
|
|
228
|
+
this.#resizeObserver?.disconnect();
|
|
229
|
+
|
|
230
|
+
this.#resizeObserver = new ResizeObserver(() => {
|
|
231
|
+
if (this.#resizeRaf) cancelAnimationFrame(this.#resizeRaf);
|
|
232
|
+
this.#resizeRaf = requestAnimationFrame(() => {
|
|
233
|
+
this.#resizeRaf = 0;
|
|
234
|
+
try {
|
|
235
|
+
map.invalidateSize(false);
|
|
236
|
+
} catch (e) {
|
|
237
|
+
debug(`[live-fleet] invalidateSize failed: ${e?.message ?? e}`);
|
|
238
|
+
}
|
|
239
|
+
});
|
|
240
|
+
});
|
|
241
|
+
this.#resizeObserver.observe(container);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/** Hook fired by <LeafletMap @onLoad>. Stores the instance + resolves the deferred. */
|
|
245
|
+
@action onMapLoad({ target: map }) {
|
|
246
|
+
this.#map = map;
|
|
247
|
+
this.#resolveMapSet?.(map);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/** ──────────────────────────────────────────────
|
|
251
|
+
* Internal map-readiness deferred (same shape as
|
|
252
|
+
* leaflet-map-manager.js).
|
|
253
|
+
* ────────────────────────────────────────────── */
|
|
254
|
+
|
|
255
|
+
#resetMapDeferred() {
|
|
256
|
+
this.#mapSetPromise = new Promise((resolve) => {
|
|
257
|
+
this.#resolveMapSet = resolve;
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
async #waitForMap({ timeoutMs = 8000 } = {}) {
|
|
262
|
+
if (this.#map) return this.#map;
|
|
263
|
+
|
|
264
|
+
let to;
|
|
265
|
+
try {
|
|
266
|
+
return await Promise.race([
|
|
267
|
+
this.#mapSetPromise,
|
|
268
|
+
new Promise((_, rej) => {
|
|
269
|
+
if (timeoutMs != null) {
|
|
270
|
+
to = setTimeout(() => rej(new Error('waitForMap timed out')), timeoutMs);
|
|
271
|
+
}
|
|
272
|
+
}),
|
|
273
|
+
]);
|
|
274
|
+
} finally {
|
|
275
|
+
if (to) clearTimeout(to);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
async #whenMapLoaded({ timeoutMs } = {}) {
|
|
280
|
+
const map = await this.#waitForMap({ timeoutMs });
|
|
281
|
+
if (map._loaded) return map;
|
|
282
|
+
|
|
283
|
+
return new Promise((resolve, reject) => {
|
|
284
|
+
let t;
|
|
285
|
+
const onLoad = () => {
|
|
286
|
+
clearTimeout(t);
|
|
287
|
+
map.off('load', onLoad);
|
|
288
|
+
resolve(map);
|
|
289
|
+
};
|
|
290
|
+
map.on('load', onLoad);
|
|
291
|
+
if (timeoutMs != null) {
|
|
292
|
+
t = setTimeout(() => {
|
|
293
|
+
map.off('load', onLoad);
|
|
294
|
+
reject(new Error('whenMapLoaded timed out'));
|
|
295
|
+
}, timeoutMs);
|
|
296
|
+
}
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
@action
|
|
301
|
+
refresh() {
|
|
302
|
+
this.load.perform();
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
async subscribe() {
|
|
306
|
+
const companyId = this.currentUser?.companyId;
|
|
307
|
+
if (!companyId) return;
|
|
308
|
+
|
|
309
|
+
try {
|
|
310
|
+
const sc = this.socket.instance();
|
|
311
|
+
const channel = sc.subscribe(`company.${companyId}`);
|
|
312
|
+
this.socketChannel = channel;
|
|
313
|
+
this.socketActive = true;
|
|
314
|
+
|
|
315
|
+
if (channel.state !== 'subscribed') {
|
|
316
|
+
await channel.subscribe();
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
for await (const msg of channel) {
|
|
320
|
+
if (!this.socketActive) break;
|
|
321
|
+
this.applyEvent(msg);
|
|
322
|
+
}
|
|
323
|
+
} catch (e) {
|
|
324
|
+
debug(`[live-fleet] socket error: ${e?.message ?? e}`);
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
async unsubscribe() {
|
|
329
|
+
this.socketActive = false;
|
|
330
|
+
try {
|
|
331
|
+
await this.socketChannel?.unsubscribe();
|
|
332
|
+
} catch (e) {
|
|
333
|
+
debug(`[live-fleet] unsubscribe error: ${e?.message ?? e}`);
|
|
334
|
+
}
|
|
335
|
+
this.socketChannel = null;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
applyEvent(msg) {
|
|
339
|
+
if (!this.data || !msg) return;
|
|
340
|
+
|
|
341
|
+
switch (msg.event) {
|
|
342
|
+
case 'driver.location_changed':
|
|
343
|
+
this.applyLocationDelta(msg.data);
|
|
344
|
+
break;
|
|
345
|
+
case 'driver.online':
|
|
346
|
+
case 'driver.offline':
|
|
347
|
+
// Presence flips are reconciled on the next 5-min refetch.
|
|
348
|
+
break;
|
|
349
|
+
default:
|
|
350
|
+
return;
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
applyLocationDelta(data) {
|
|
355
|
+
if (!data?.uuid && !data?.driver_uuid) return;
|
|
356
|
+
const targetUuid = data.uuid ?? data.driver_uuid;
|
|
357
|
+
const lat = data.lat ?? data.latitude;
|
|
358
|
+
const lng = data.lng ?? data.longitude;
|
|
359
|
+
if (typeof lat !== 'number' || typeof lng !== 'number') return;
|
|
360
|
+
|
|
361
|
+
const drivers = (this.data.drivers ?? []).map((d) => (d.uuid === targetUuid ? { ...d, lat, lng, heading: data.heading ?? d.heading } : d));
|
|
362
|
+
|
|
363
|
+
this.data = { ...this.data, drivers };
|
|
364
|
+
}
|
|
365
|
+
}
|