@fleetbase/fleetops-engine 0.6.47 → 0.6.49
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/admin/routing-settings.hbs +71 -0
- package/addon/components/admin/routing-settings.js +124 -0
- package/addon/components/customer/orders.js +1 -1
- package/addon/components/display-place.hbs +1 -1
- 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 +12 -73
- package/addon/components/map/leaflet-live-map.hbs +149 -33
- package/addon/components/map/order-list-overlay/order.hbs +18 -3
- package/addon/components/map/toolbar.hbs +2 -2
- package/addon/components/map/toolbar.js +10 -2
- package/addon/components/orchestrator/phase-builder.hbs +49 -5
- package/addon/components/orchestrator/phase-builder.js +34 -1
- package/addon/components/orchestrator/resource-panel.hbs +19 -1
- package/addon/components/orchestrator/resource-panel.js +4 -0
- package/addon/components/orchestrator-workbench.js +4 -1
- package/addon/components/order/details/purchase-rate.hbs +19 -22
- package/addon/components/order/details/route.hbs +26 -1
- package/addon/components/order/details/route.js +50 -0
- package/addon/components/order/details/tracking.hbs +171 -27
- package/addon/components/order/details/tracking.js +457 -0
- package/addon/components/order/form.hbs +2 -2
- package/addon/components/order/kanban-card.hbs +18 -3
- package/addon/components/order-list-overlay/order.hbs +18 -3
- package/addon/components/order-progress-card.hbs +21 -6
- package/addon/components/order-tracking-lookup.hbs +7 -7
- package/addon/components/order-tracking-lookup.js +1 -1
- package/addon/components/route-list.hbs +78 -8
- package/addon/components/route-list.js +48 -6
- package/addon/components/tracking-stop-progress.hbs +45 -0
- package/addon/components/tracking-stop-progress.js +79 -0
- 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/settings/routing.js +124 -1
- package/addon/extension.js +172 -3
- package/addon/helpers/format-duration.js +38 -0
- package/addon/routes/application.js +6 -0
- package/addon/services/map-adapter/google.js +13 -2
- package/addon/services/order-list-overlay.js +2 -2
- package/addon/styles/fleetops-engine.css +806 -46
- 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 +82 -4
- package/addon/templates/settings/scheduling.hbs +1 -1
- package/addon/utils/live-map-card-content.js +171 -0
- package/app/components/tracking-stop-progress.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/helpers/format-duration.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/fleetops.php +32 -11
- package/server/config/telematics.php +111 -1
- package/server/migrations/2026_05_13_000001_repair_customer_contact_user_types.php +86 -0
- package/server/migrations/2026_05_18_000001_add_license_expiry_to_drivers_table.php +25 -0
- package/server/seeders/Testing/Concerns/SeedsTestingData.php +183 -0
- package/server/seeders/Testing/ConnectivitySeeder.php +212 -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 +214 -0
- package/server/seeders/Testing/TestingSeeder.php +40 -0
- package/server/seeders/Testing/VroomOrchestrationSeeder.php +409 -0
- package/server/src/Console/Commands/ProcessOperationalAlerts.php +279 -0
- package/server/src/Http/Controllers/Api/v1/OrchestrationController.php +64 -0
- package/server/src/Http/Controllers/Api/v1/OrderController.php +26 -26
- package/server/src/Http/Controllers/Api/v1/PayloadController.php +10 -10
- package/server/src/Http/Controllers/Api/v1/ServiceQuoteController.php +1 -1
- package/server/src/Http/Controllers/Internal/v1/AnalyticsController.php +119 -0
- package/server/src/Http/Controllers/Internal/v1/ContactController.php +41 -0
- package/server/src/Http/Controllers/Internal/v1/LiveController.php +6 -42
- package/server/src/Http/Controllers/Internal/v1/MetricsController.php +82 -3
- package/server/src/Http/Controllers/Internal/v1/OrchestrationController.php +1 -1
- package/server/src/Http/Controllers/Internal/v1/OrderController.php +52 -21
- package/server/src/Http/Controllers/Internal/v1/ServiceQuoteController.php +1 -1
- package/server/src/Http/Controllers/Internal/v1/SettingController.php +164 -12
- 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/Payload.php +2 -2
- package/server/src/Http/Resources/v1/Index/Vehicle.php +74 -1
- package/server/src/Models/Contact.php +122 -13
- package/server/src/Models/Driver.php +2 -0
- package/server/src/Models/Order.php +1 -1
- package/server/src/Models/OrderConfig.php +14 -11
- package/server/src/Models/Payload.php +8 -8
- package/server/src/Models/Place.php +0 -3
- package/server/src/Models/ServiceRate.php +1 -1
- 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 +8 -0
- package/server/src/Orchestration/Engines/CapacityAllocationEngine.php +220 -0
- package/server/src/Orchestration/Engines/VroomOrchestrationEngine.php +357 -38
- package/server/src/Orchestration/Support/OrchestrationPayloadBuilder.php +259 -51
- package/server/src/Providers/FleetOpsServiceProvider.php +32 -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 +79 -0
- package/server/src/Support/Analytics/TopDrivers.php +92 -0
- package/server/src/Support/LiveOrderQuery.php +75 -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 -234
- package/server/src/Support/OSRM.php +11 -6
- package/server/src/Support/OrderTracker.php +7 -582
- package/server/src/Support/Telematics/Providers/AfaqyProvider.php +290 -0
- package/server/src/Support/Telematics/Providers/SafeeProvider.php +333 -0
- package/server/src/Support/Utils.php +1 -1
- package/server/src/Tracking/Contracts/TrackingProviderInterface.php +19 -0
- package/server/src/Tracking/Providers/CalculatedTrackingProvider.php +65 -0
- package/server/src/Tracking/Providers/GoogleRoutesTrackingProvider.php +118 -0
- package/server/src/Tracking/Providers/OsrmTrackingProvider.php +71 -0
- package/server/src/Tracking/README.md +25 -0
- package/server/src/Tracking/Support/FakeTrackingProvider.php +42 -0
- package/server/src/Tracking/TrackingContext.php +63 -0
- package/server/src/Tracking/TrackingContextBuilder.php +204 -0
- package/server/src/Tracking/TrackingIntelligenceService.php +180 -0
- package/server/src/Tracking/TrackingOptions.php +49 -0
- package/server/src/Tracking/TrackingProviderCapabilities.php +30 -0
- package/server/src/Tracking/TrackingProviderManager.php +89 -0
- package/server/src/Tracking/TrackingProviderRegistry.php +35 -0
- package/server/src/Tracking/TrackingProviderResult.php +20 -0
- package/server/src/Tracking/TrackingStop.php +59 -0
- package/server/src/routes.php +27 -0
- package/server/tests/AnalyticsRoutesTest.php +41 -0
- package/server/tests/CapacityAllocationEngineTest.php +108 -0
- package/server/tests/LiveOrderQueryTest.php +34 -0
- package/server/tests/MetricsRegistryTest.php +88 -0
- package/server/tests/OrchestrationPayloadBuilderTest.php +132 -0
- package/server/tests/OrchestratorConsumableApiTest.php +74 -0
- package/server/tests/PingDriverEndpointTest.php +19 -0
- package/server/tests/TrackingIntelligenceTest.php +176 -0
- package/server/tests/VroomOrchestrationEngineTest.php +250 -0
- package/translations/en-us.yaml +41 -0
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
<div class="fleet-ops-widget fleet-ops-maintenance-overview 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.maintenance-overview.title"}}</span>
|
|
4
|
+
<FaIcon @icon="wrench" class="text-gray-400" />
|
|
5
|
+
</div>
|
|
6
|
+
|
|
7
|
+
<div class="flex-1 flex flex-col p-2.5 border border-t-0 dark:border-gray-700 border-gray-200 rounded-b-md bg-white dark:bg-gray-900 min-h-0">
|
|
8
|
+
{{#if this.load.isRunning}}
|
|
9
|
+
<div class="flex items-center justify-center h-full"><Spinner /></div>
|
|
10
|
+
{{else if this.error}}
|
|
11
|
+
<div class="flex items-center justify-center h-full text-xs text-red-500">{{this.error}}</div>
|
|
12
|
+
{{else if this.data}}
|
|
13
|
+
<div class="grid grid-cols-3 gap-1.5 mb-2">
|
|
14
|
+
<div class="rounded-md border dark:border-gray-700 border-gray-200 p-1.5 text-center">
|
|
15
|
+
<div class="text-[9px] uppercase text-gray-500 dark:text-gray-400 leading-none">Overdue</div>
|
|
16
|
+
<div class="text-lg font-bold leading-tight mt-0.5 {{if this.data.overdue 'text-red-500' 'text-gray-400'}}">{{this.data.overdue}}</div>
|
|
17
|
+
</div>
|
|
18
|
+
<div class="rounded-md border dark:border-gray-700 border-gray-200 p-1.5 text-center">
|
|
19
|
+
<div class="text-[9px] uppercase text-gray-500 dark:text-gray-400 leading-none">Next 7d</div>
|
|
20
|
+
<div class="text-lg font-bold text-amber-500 leading-tight mt-0.5">{{this.data.scheduled_next_7d}}</div>
|
|
21
|
+
</div>
|
|
22
|
+
<div class="rounded-md border dark:border-gray-700 border-gray-200 p-1.5 text-center">
|
|
23
|
+
<div class="text-[9px] uppercase text-gray-500 dark:text-gray-400 leading-none">MTD</div>
|
|
24
|
+
<div class="text-sm font-bold text-black dark:text-gray-100 leading-tight mt-0.5 truncate">{{format-currency this.data.cost_this_month this.data.currency}}</div>
|
|
25
|
+
</div>
|
|
26
|
+
</div>
|
|
27
|
+
|
|
28
|
+
<div class="flex-1 min-h-0 overflow-auto divide-y dark:divide-gray-700 border-t dark:border-gray-700 pt-1">
|
|
29
|
+
{{#each this.data.upcoming as |item|}}
|
|
30
|
+
<div class="flex items-center justify-between py-1 hover:bg-gray-50 dark:hover:bg-gray-800 px-1">
|
|
31
|
+
<div class="min-w-0 flex-1">
|
|
32
|
+
<div class="text-[11px] font-medium text-black dark:text-gray-100 truncate">{{or item.type "Maintenance"}}</div>
|
|
33
|
+
<div class="text-[10px] text-gray-500 dark:text-gray-400">{{format-date-fns item.scheduled_at "MMM d"}}</div>
|
|
34
|
+
</div>
|
|
35
|
+
{{#if item.priority}}
|
|
36
|
+
<Badge @status={{this.priorityStatus item.priority}} @text={{item.priority}} @hideIcon={{true}} @wrapperClass="text-[9px] px-1 py-0 ml-2" />
|
|
37
|
+
{{/if}}
|
|
38
|
+
</div>
|
|
39
|
+
{{else}}
|
|
40
|
+
<div class="text-[10px] text-gray-500 text-center py-3">No upcoming maintenance.</div>
|
|
41
|
+
{{/each}}
|
|
42
|
+
</div>
|
|
43
|
+
{{/if}}
|
|
44
|
+
</div>
|
|
45
|
+
</div>
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import Component from '@glimmer/component';
|
|
2
|
+
import { tracked } from '@glimmer/tracking';
|
|
3
|
+
import { inject as service } from '@ember/service';
|
|
4
|
+
import { task } from 'ember-concurrency';
|
|
5
|
+
|
|
6
|
+
export default class WidgetMaintenanceOverviewComponent extends Component {
|
|
7
|
+
static widgetId = 'fleet-ops-maintenance-overview-widget';
|
|
8
|
+
|
|
9
|
+
@service fetch;
|
|
10
|
+
|
|
11
|
+
@tracked data = null;
|
|
12
|
+
@tracked error = null;
|
|
13
|
+
|
|
14
|
+
constructor() {
|
|
15
|
+
super(...arguments);
|
|
16
|
+
this.load.perform();
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
priorityStatus(priority) {
|
|
20
|
+
switch (priority) {
|
|
21
|
+
case 'high':
|
|
22
|
+
case 'urgent':
|
|
23
|
+
return 'danger';
|
|
24
|
+
case 'medium':
|
|
25
|
+
case 'normal':
|
|
26
|
+
return 'warning';
|
|
27
|
+
default:
|
|
28
|
+
return 'info';
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
@task *load() {
|
|
33
|
+
try {
|
|
34
|
+
this.data = yield this.fetch.get('fleet-ops/analytics/maintenance-overview');
|
|
35
|
+
this.error = null;
|
|
36
|
+
} catch (e) {
|
|
37
|
+
this.error = e?.message ?? 'Failed to load maintenance overview';
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
<div class="fleet-ops-widget fleet-ops-on-time-delivery flex flex-col h-full rounded-md border dark:border-gray-700 border-gray-200 dark:bg-gray-800 bg-white shadow-sm {{@class}}" ...attributes>
|
|
2
|
+
<div class="flex items-center justify-between px-2.5 pt-2">
|
|
3
|
+
<span class="text-[10px] uppercase tracking-wide text-gray-500 dark:text-gray-400 font-semibold">
|
|
4
|
+
{{t "widget.on-time-delivery.title"}}
|
|
5
|
+
</span>
|
|
6
|
+
<FaIcon @icon="clock" class="text-gray-400 text-[11px]" />
|
|
7
|
+
</div>
|
|
8
|
+
|
|
9
|
+
<div class="flex-1 flex flex-col items-center justify-center px-2.5 pb-2 min-h-0">
|
|
10
|
+
{{#if this.load.isRunning}}
|
|
11
|
+
<Spinner />
|
|
12
|
+
{{else if this.error}}
|
|
13
|
+
<div class="text-[10px] text-red-500">{{this.error}}</div>
|
|
14
|
+
{{else if this.data}}
|
|
15
|
+
<div class="relative w-full max-w-24 aspect-square">
|
|
16
|
+
<Chart @type="doughnut" @labels={{this.chartLabels}} @datasets={{this.chartDatasets}} @options={{this.chartOptions}} />
|
|
17
|
+
<div class="absolute inset-0 flex flex-col items-center justify-center pointer-events-none">
|
|
18
|
+
<span class="text-base font-bold text-black dark:text-gray-100 leading-none">{{this.data.on_time_pct}}%</span>
|
|
19
|
+
<span class="text-[9px] text-gray-500 dark:text-gray-400 mt-0.5">{{this.data.on_time}}/{{this.data.total}}</span>
|
|
20
|
+
</div>
|
|
21
|
+
</div>
|
|
22
|
+
{{#if this.deltaStatus}}
|
|
23
|
+
<Badge @status={{this.deltaStatus}} @text={{this.deltaText}} @hideIcon={{true}} @disableHumanize={{true}} @wrapperClass="mt-1 text-[9px] px-1 py-0" />
|
|
24
|
+
{{/if}}
|
|
25
|
+
{{/if}}
|
|
26
|
+
</div>
|
|
27
|
+
</div>
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import Component from '@glimmer/component';
|
|
2
|
+
import { tracked } from '@glimmer/tracking';
|
|
3
|
+
import { inject as service } from '@ember/service';
|
|
4
|
+
import { task } from 'ember-concurrency';
|
|
5
|
+
|
|
6
|
+
export default class WidgetOnTimeDeliveryComponent extends Component {
|
|
7
|
+
static widgetId = 'fleet-ops-on-time-delivery-widget';
|
|
8
|
+
|
|
9
|
+
@service fetch;
|
|
10
|
+
|
|
11
|
+
@tracked data = null;
|
|
12
|
+
@tracked error = null;
|
|
13
|
+
|
|
14
|
+
constructor() {
|
|
15
|
+
super(...arguments);
|
|
16
|
+
this.load.perform();
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
get gaugeColor() {
|
|
20
|
+
const pct = this.data?.on_time_pct ?? 0;
|
|
21
|
+
if (pct >= 95) return '#22c55e';
|
|
22
|
+
if (pct >= 85) return '#84cc16';
|
|
23
|
+
if (pct >= 70) return '#f59e0b';
|
|
24
|
+
return '#ef4444';
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
get chartLabels() {
|
|
28
|
+
return ['On Time', 'Late'];
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
get chartDatasets() {
|
|
32
|
+
return [
|
|
33
|
+
{
|
|
34
|
+
data: [this.data?.on_time ?? 0, this.data?.late ?? 0],
|
|
35
|
+
backgroundColor: [this.gaugeColor, 'rgba(148,163,184,0.25)'],
|
|
36
|
+
borderWidth: 0,
|
|
37
|
+
},
|
|
38
|
+
];
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
get chartOptions() {
|
|
42
|
+
return {
|
|
43
|
+
responsive: true,
|
|
44
|
+
maintainAspectRatio: false,
|
|
45
|
+
cutout: '75%',
|
|
46
|
+
plugins: {
|
|
47
|
+
legend: { display: false },
|
|
48
|
+
tooltip: { enabled: true },
|
|
49
|
+
},
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
get deltaStatus() {
|
|
54
|
+
const d = this.data?.delta_pct;
|
|
55
|
+
if (typeof d !== 'number' || d === 0) return null;
|
|
56
|
+
return d > 0 ? 'success' : 'danger';
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
get deltaText() {
|
|
60
|
+
const d = this.data?.delta_pct;
|
|
61
|
+
if (typeof d !== 'number') return null;
|
|
62
|
+
return `${d > 0 ? '+' : ''}${d} pp`;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
@task *load() {
|
|
66
|
+
try {
|
|
67
|
+
this.data = yield this.fetch.get('fleet-ops/analytics/on-time-delivery', { period: '30d' });
|
|
68
|
+
this.error = null;
|
|
69
|
+
} catch (e) {
|
|
70
|
+
this.error = e?.message ?? 'Failed to load on-time delivery';
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
<div class="fleet-ops-widget fleet-ops-operations-pulse 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">
|
|
4
|
+
{{t "widget.operations-pulse.title"}}
|
|
5
|
+
</span>
|
|
6
|
+
<button type="button" class="text-gray-400 hover:text-gray-600 dark:hover:text-gray-200 text-[10px]" {{on "click" this.refresh}} title={{t "widget.refresh"}}>
|
|
7
|
+
<FaIcon @icon="rotate" class={{if this.load.isRunning "animate-spin"}} />
|
|
8
|
+
</button>
|
|
9
|
+
</div>
|
|
10
|
+
|
|
11
|
+
<div class="flex-1 p-2.5 border border-t-0 dark:border-gray-700 border-gray-200 rounded-b-md bg-white dark:bg-gray-900">
|
|
12
|
+
{{#if this.load.isRunning}}
|
|
13
|
+
<div class="flex items-center justify-center h-full">
|
|
14
|
+
<Spinner />
|
|
15
|
+
</div>
|
|
16
|
+
{{else if this.error}}
|
|
17
|
+
<div class="flex items-center justify-center h-full text-xs text-red-500">{{this.error}}</div>
|
|
18
|
+
{{else}}
|
|
19
|
+
<div class="grid grid-cols-2 lg:grid-cols-5 gap-2 h-full">
|
|
20
|
+
{{#each this.tiles as |tile|}}
|
|
21
|
+
<div class="fleet-ops-pulse-tile {{tile.meta.accent}} relative rounded-md border p-2 flex flex-col justify-between min-h-0 overflow-hidden">
|
|
22
|
+
{{!-- Accent stripe along the left edge for instant status read --}}
|
|
23
|
+
<span class="fleet-ops-pulse-stripe absolute inset-y-0 left-0"></span>
|
|
24
|
+
|
|
25
|
+
<div class="flex items-center justify-between relative z-10">
|
|
26
|
+
<span class="text-[9px] uppercase tracking-wider text-gray-600 dark:text-gray-300 font-bold truncate">{{tile.title}}</span>
|
|
27
|
+
{{#if tile.meta.icon}}
|
|
28
|
+
<div class="fleet-ops-pulse-icon flex-shrink-0">
|
|
29
|
+
<FaIcon @icon={{tile.meta.icon}} class="text-[10px]" />
|
|
30
|
+
</div>
|
|
31
|
+
{{/if}}
|
|
32
|
+
</div>
|
|
33
|
+
|
|
34
|
+
<div class="flex items-baseline gap-1 mt-1 relative z-10 fleet-ops-pulse-value-row">
|
|
35
|
+
<span class="fleet-ops-pulse-value text-2xl font-bold leading-none tabular-nums">{{tile.value}}</span>
|
|
36
|
+
{{#if tile.of}}
|
|
37
|
+
<span class="text-[10px] text-gray-500 dark:text-gray-400">/ {{tile.of}}</span>
|
|
38
|
+
{{/if}}
|
|
39
|
+
</div>
|
|
40
|
+
|
|
41
|
+
{{#let (this.deltaStatus tile) as |status|}}
|
|
42
|
+
{{#if status}}
|
|
43
|
+
<Badge @status={{status}} @text={{this.deltaText tile}} @hideIcon={{true}} @disableHumanize={{true}} @wrapperClass="text-[9px] py-0 px-1 leading-tight mt-1 self-start font-bold relative z-10" />
|
|
44
|
+
{{/if}}
|
|
45
|
+
{{/let}}
|
|
46
|
+
</div>
|
|
47
|
+
{{/each}}
|
|
48
|
+
</div>
|
|
49
|
+
{{/if}}
|
|
50
|
+
</div>
|
|
51
|
+
</div>
|