@fleetbase/fleetops-engine 0.6.48 → 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/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/orchestrator/phase-builder.hbs +21 -0
- package/addon/components/orchestrator/phase-builder.js +13 -0
- package/addon/components/orchestrator-workbench.js +1 -0
- package/addon/components/order/form.hbs +2 -2
- 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 +26 -0
- package/addon/extension.js +172 -3
- package/addon/services/map-adapter/google.js +13 -2
- package/addon/styles/fleetops-engine.css +114 -87
- 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/live-map-card-content.js +171 -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/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/{VroomOrchestrationSeeder.php → Testing/VroomOrchestrationSeeder.php} +8 -1
- package/server/src/Console/Commands/ProcessOperationalAlerts.php +279 -0
- package/server/src/Http/Controllers/Internal/v1/AnalyticsController.php +119 -0
- package/server/src/Http/Controllers/Internal/v1/MetricsController.php +82 -3
- package/server/src/Http/Controllers/Internal/v1/OrderController.php +12 -6
- package/server/src/Http/Controllers/Internal/v1/SettingController.php +46 -1
- 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/Models/Driver.php +2 -0
- package/server/src/Models/OrderConfig.php +14 -11
- 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/Orchestration/Engines/VroomOrchestrationEngine.php +28 -1
- package/server/src/Providers/FleetOpsServiceProvider.php +5 -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/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/Telematics/Providers/AfaqyProvider.php +290 -0
- package/server/src/Support/Telematics/Providers/SafeeProvider.php +333 -0
- package/server/src/routes.php +16 -0
- package/server/tests/AnalyticsRoutesTest.php +41 -0
- package/server/tests/MetricsRegistryTest.php +88 -0
- package/server/tests/VroomOrchestrationEngineTest.php +91 -0
- package/translations/en-us.yaml +37 -0
|
@@ -30,6 +30,10 @@
|
|
|
30
30
|
<div class="field-name">{{t "driver.fields.driver-license"}}</div>
|
|
31
31
|
<div class="field-value">{{n-a @resource.drivers_license_number}}</div>
|
|
32
32
|
</div>
|
|
33
|
+
<div class="field-info-container">
|
|
34
|
+
<div class="field-name">{{t "driver.fields.license-expiry"}}</div>
|
|
35
|
+
<div class="field-value">{{n-a @resource.license_expiry}}</div>
|
|
36
|
+
</div>
|
|
33
37
|
<div class="field-info-container">
|
|
34
38
|
<div class="field-name">{{t "resource.vendor"}}</div>
|
|
35
39
|
<div class="field-value">{{n-a @resource.vendor_name}}</div>
|
|
@@ -92,4 +96,4 @@
|
|
|
92
96
|
>
|
|
93
97
|
<MetadataViewer @metadata={{@resource.meta}} />
|
|
94
98
|
</ContentPanel>
|
|
95
|
-
</div>
|
|
99
|
+
</div>
|
|
@@ -93,6 +93,16 @@
|
|
|
93
93
|
/>
|
|
94
94
|
</InputGroup>
|
|
95
95
|
|
|
96
|
+
<InputGroup @name={{t "driver.fields.license-expiry"}}>
|
|
97
|
+
<DatePicker
|
|
98
|
+
@value={{@resource.license_expiry}}
|
|
99
|
+
@onChange={{fn (mut @resource.license_expiry)}}
|
|
100
|
+
@placeholder={{t "driver.fields.license-expiry"}}
|
|
101
|
+
@disabled={{cannot-write @resource}}
|
|
102
|
+
class="w-full form-input"
|
|
103
|
+
/>
|
|
104
|
+
</InputGroup>
|
|
105
|
+
|
|
96
106
|
<InputGroup @name={{t "driver.fields.vendor"}}>
|
|
97
107
|
<ModelSelect
|
|
98
108
|
@modelName="vendor"
|
|
@@ -221,4 +231,4 @@
|
|
|
221
231
|
<ContentPanel @title={{t "common.metadata"}} @open={{true}} @wrapperClass="bordered-top">
|
|
222
232
|
<MetadataEditor @value={{@resource.meta}} @onChange={{fn (mut @resource.meta)}} />
|
|
223
233
|
</ContentPanel>
|
|
224
|
-
</div>
|
|
234
|
+
</div>
|
|
@@ -36,13 +36,13 @@
|
|
|
36
36
|
@dropdownButtonIcon={{@itemDropdownButtonIcon}}
|
|
37
37
|
@dropdownButtonIconPrefix={{@itemDropdownButtonIconPrefix}}
|
|
38
38
|
@dropdownButtonText={{@itemDropdownButtonText}}
|
|
39
|
-
@dropdownButtonCalculatePosition={{
|
|
39
|
+
@dropdownButtonCalculatePosition={{this.calculateDropdownItemPosition}}
|
|
40
40
|
@dropdownButtonMenuLabel={{vehicle.displayName}}
|
|
41
41
|
@dropdownButtonWrapperClass={{@itemDropdownButtonWrapperClass}}
|
|
42
42
|
@dropdownButtonTriggerClass={{@itemDropdownButtonTriggerClass}}
|
|
43
43
|
@registerDropdownButtonAPI={{@itemRegisterDropdownButtonAPI}}
|
|
44
|
-
@permission="fleet-ops view vehicle"
|
|
45
44
|
@dropdownButtonRenderInPlace={{false}}
|
|
45
|
+
@permission="fleet-ops view vehicle"
|
|
46
46
|
>{{vehicle.displayName}}</Layout::Sidebar::Item>
|
|
47
47
|
{{/each}}
|
|
48
48
|
</Layout::Sidebar::Panel>
|
|
@@ -31,4 +31,14 @@ export default class FleetListingPanelComponent extends Component {
|
|
|
31
31
|
this.args.onVehicleClicked(vehicle);
|
|
32
32
|
}
|
|
33
33
|
}
|
|
34
|
+
|
|
35
|
+
@action calculateDropdownItemPosition(trigger) {
|
|
36
|
+
let { top, left, width } = trigger.getBoundingClientRect();
|
|
37
|
+
let style = {
|
|
38
|
+
left: 11 + left + width,
|
|
39
|
+
top: top + 2,
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
return { style };
|
|
43
|
+
}
|
|
34
44
|
}
|
|
@@ -6,73 +6,7 @@ import { action } from '@ember/object';
|
|
|
6
6
|
import { isArray } from '@ember/array';
|
|
7
7
|
import { debug } from '@ember/debug';
|
|
8
8
|
import { guidFor } from '@ember/object/internals';
|
|
9
|
-
|
|
10
|
-
function buildDriverInfoWindowContent(driver) {
|
|
11
|
-
return `
|
|
12
|
-
<div class="fleetops-google-popover fleetops-google-popover--compact">
|
|
13
|
-
<div class="fleetops-google-popover__row">
|
|
14
|
-
<img src="${driver.photoUrl ?? ''}" alt="${driver.name ?? ''}" class="fleetops-google-popover__avatar" />
|
|
15
|
-
<div class="fleetops-google-popover__body">
|
|
16
|
-
<div class="fleetops-google-popover__title">${driver.name ?? '-'}</div>
|
|
17
|
-
<div class="fleetops-google-popover__meta">Phone: ${driver.phone ?? '-'}</div>
|
|
18
|
-
<div class="fleetops-google-popover__meta">Vehicle: ${driver.vehicle_name ?? '-'}</div>
|
|
19
|
-
<div class="fleetops-google-popover__meta">Status:
|
|
20
|
-
<span class="${driver.online ? 'text-green-500' : 'text-red-400'}">${driver.online ? 'Online' : 'Offline'}</span>
|
|
21
|
-
</div>
|
|
22
|
-
</div>
|
|
23
|
-
</div>
|
|
24
|
-
</div>`;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
function buildDriverTooltipContent(driver) {
|
|
28
|
-
return `
|
|
29
|
-
<div class="fleetops-google-hover-tooltip__title">${driver.name ?? '-'}</div>
|
|
30
|
-
<div class="fleetops-google-hover-tooltip__meta ${driver.online ? 'text-green-400' : 'text-red-400'}">${driver.online ? 'Online' : 'Offline'}</div>
|
|
31
|
-
`;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
function buildVehicleInfoWindowContent(vehicle) {
|
|
35
|
-
return `
|
|
36
|
-
<div class="fleetops-google-popover">
|
|
37
|
-
<div class="fleetops-google-popover__row">
|
|
38
|
-
<img src="${vehicle.photo_url ?? ''}" alt="${vehicle.display_name ?? ''}" class="fleetops-google-popover__vehicle" />
|
|
39
|
-
<div class="fleetops-google-popover__body">
|
|
40
|
-
<div class="fleetops-google-popover__title">${vehicle.displayName ?? '-'}</div>
|
|
41
|
-
<div class="fleetops-google-popover__meta">ID: ${vehicle.public_id ?? '-'}</div>
|
|
42
|
-
<div class="fleetops-google-popover__meta">Serial: ${vehicle.serial_number ?? vehicle.vin ?? '-'}</div>
|
|
43
|
-
<div class="fleetops-google-popover__meta">Driver: ${vehicle.driver_name ?? '-'}</div>
|
|
44
|
-
<div class="fleetops-google-popover__meta">Status:
|
|
45
|
-
<span class="${vehicle.online ? 'text-green-500' : 'text-red-400'}">${vehicle.online ? 'Online' : 'Offline'}</span>
|
|
46
|
-
</div>
|
|
47
|
-
</div>
|
|
48
|
-
</div>
|
|
49
|
-
</div>`;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
function buildVehicleTooltipContent(vehicle) {
|
|
53
|
-
return `
|
|
54
|
-
<div class="fleetops-google-hover-tooltip__title">${vehicle.displayName ?? '-'}</div>
|
|
55
|
-
<div class="fleetops-google-hover-tooltip__meta">ID: ${vehicle.public_id ?? '-'}</div>
|
|
56
|
-
<div class="fleetops-google-hover-tooltip__meta ${vehicle.online ? 'text-green-400' : 'text-red-400'}">${vehicle.online ? 'Online' : 'Offline'}</div>
|
|
57
|
-
`;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
function buildPlaceInfoWindowContent(place) {
|
|
61
|
-
return `
|
|
62
|
-
<div class="fleetops-google-popover fleetops-google-popover--compact">
|
|
63
|
-
<div class="fleetops-google-popover__body">
|
|
64
|
-
<div class="fleetops-google-popover__title">${place.name ?? place.address ?? '-'}</div>
|
|
65
|
-
<div class="fleetops-google-popover__meta">${place.address ?? ''}</div>
|
|
66
|
-
</div>
|
|
67
|
-
</div>`;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
function buildPlaceTooltipContent(place) {
|
|
71
|
-
return `
|
|
72
|
-
<div class="fleetops-google-hover-tooltip__title">${place.name ?? place.address ?? '-'}</div>
|
|
73
|
-
<div class="fleetops-google-hover-tooltip__meta">${place.address ?? ''}</div>
|
|
74
|
-
`;
|
|
75
|
-
}
|
|
9
|
+
import { buildDriverLiveMapContent, buildPlaceInfoWindowContent, buildPlaceTooltipContent, buildVehicleLiveMapContent } from '../../utils/live-map-card-content';
|
|
76
10
|
|
|
77
11
|
export default class MapGoogleLiveMapComponent extends Component {
|
|
78
12
|
@service mapManager;
|
|
@@ -100,6 +34,7 @@ export default class MapGoogleLiveMapComponent extends Component {
|
|
|
100
34
|
|
|
101
35
|
this.map = map;
|
|
102
36
|
this.mapManager.on('moveend', () => this.args.onViewportChanged?.());
|
|
37
|
+
this.mapManager.on('click', () => this._closeAllInfoWindows());
|
|
103
38
|
this.mapManager.on('rightclick', (event) => this._showMapContextMenu(event));
|
|
104
39
|
this.args.onLoad?.({ target: map });
|
|
105
40
|
this.syncResources();
|
|
@@ -125,12 +60,12 @@ export default class MapGoogleLiveMapComponent extends Component {
|
|
|
125
60
|
if (!marker) {
|
|
126
61
|
const createdMarker = await this.mapManager.addMarker(driver.id, coords.lat, coords.lng, {
|
|
127
62
|
iconUrl: driver.vehicle_avatar ?? '/engines-dist/images/driver-marker.png',
|
|
128
|
-
iconSize: [
|
|
63
|
+
iconSize: [20, 20],
|
|
129
64
|
title: driver.name,
|
|
130
|
-
tooltip:
|
|
65
|
+
tooltip: buildDriverLiveMapContent(driver),
|
|
131
66
|
tooltipOptions: { html: true },
|
|
132
67
|
rotationAngle: driver.heading,
|
|
133
|
-
onClick: () => this.#openInfoWindow(`driver:${driver.id}`,
|
|
68
|
+
onClick: () => this.#openInfoWindow(`driver:${driver.id}`, buildDriverLiveMapContent(driver, true), driver.id, () => this.args.onDriverClicked?.(driver)),
|
|
134
69
|
onRightClick: (event) => this._showDriverContextMenu(driver, event),
|
|
135
70
|
});
|
|
136
71
|
|
|
@@ -155,12 +90,12 @@ export default class MapGoogleLiveMapComponent extends Component {
|
|
|
155
90
|
if (!marker) {
|
|
156
91
|
const createdMarker = await this.mapManager.addMarker(vehicle.id, coords.lat, coords.lng, {
|
|
157
92
|
iconUrl: vehicle.avatar_url ?? '/engines-dist/images/vehicle-marker.png',
|
|
158
|
-
iconSize: [
|
|
93
|
+
iconSize: [20, 20],
|
|
159
94
|
title: vehicle.displayName,
|
|
160
|
-
tooltip:
|
|
95
|
+
tooltip: buildVehicleLiveMapContent(vehicle),
|
|
161
96
|
tooltipOptions: { html: true },
|
|
162
97
|
rotationAngle: vehicle.heading,
|
|
163
|
-
onClick: () => this.#openInfoWindow(`vehicle:${vehicle.id}`,
|
|
98
|
+
onClick: () => this.#openInfoWindow(`vehicle:${vehicle.id}`, buildVehicleLiveMapContent(vehicle, true), vehicle.id, () => this.args.onVehicleClicked?.(vehicle)),
|
|
164
99
|
onRightClick: (event) => this._showVehicleContextMenu(vehicle, event),
|
|
165
100
|
});
|
|
166
101
|
|
|
@@ -308,6 +243,10 @@ export default class MapGoogleLiveMapComponent extends Component {
|
|
|
308
243
|
if (!infoWindow) {
|
|
309
244
|
infoWindow = new google.maps.InfoWindow({ content });
|
|
310
245
|
this._infoWindows.set(infoId, infoWindow);
|
|
246
|
+
|
|
247
|
+
infoWindow.addListener('domready', () => {
|
|
248
|
+
document.querySelector('[data-fleetops-google-popover-close]')?.addEventListener('click', () => infoWindow.close(), { once: true });
|
|
249
|
+
});
|
|
311
250
|
} else {
|
|
312
251
|
infoWindow.setContent(content);
|
|
313
252
|
}
|
|
@@ -83,27 +83,94 @@
|
|
|
83
83
|
as |marker|
|
|
84
84
|
>
|
|
85
85
|
<marker.popup @maxWidth="500" @minWidth="225">
|
|
86
|
-
<div class="
|
|
87
|
-
<div class="
|
|
88
|
-
<
|
|
86
|
+
<div class="w-[280px] max-w-[calc(100vw-2rem)] rounded-lg bg-gray-900 shadow-lg p-1.5 text-white">
|
|
87
|
+
<div class="flex items-center gap-1.5 text-xs font-semibold mb-1.5">
|
|
88
|
+
<span class="inline-block w-2 h-2 rounded-full {{if driver.online 'bg-green-400' 'bg-red-500'}}"></span>
|
|
89
|
+
<span>{{driver.name}}</span>
|
|
90
|
+
<div class="status-badge {{if driver.status (dasherize driver.status) 'default'}}-status-badge status-badge-{{if driver.status (dasherize driver.status) 'default'}} shadow-none ml-auto">
|
|
91
|
+
<span class="rounded-full text-[9px] px-1.5 py-0.5">{{n-a driver.meta.status_label (smart-humanize driver.status)}}</span>
|
|
92
|
+
</div>
|
|
89
93
|
</div>
|
|
90
|
-
<div class="
|
|
91
|
-
<div class="
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
94
|
+
<div class="grid grid-cols-2 gap-1">
|
|
95
|
+
<div class="rounded bg-gray-900 shadow-md px-1.5 py-1 min-w-0">
|
|
96
|
+
<div class="text-[9px] font-semibold uppercase text-gray-400 leading-none">ID</div>
|
|
97
|
+
<div class="text-[11px] text-white leading-tight whitespace-normal break-words">{{n-a driver.public_id driver.id}}</div>
|
|
98
|
+
</div>
|
|
99
|
+
<div class="rounded bg-gray-900 shadow-md px-1.5 py-1 min-w-0">
|
|
100
|
+
<div class="text-[9px] font-semibold uppercase text-gray-400 leading-none">Phone</div>
|
|
101
|
+
<div class="text-[11px] text-white leading-tight whitespace-normal break-words">{{n-a driver.phone}}</div>
|
|
102
|
+
</div>
|
|
103
|
+
<div class="rounded bg-gray-900 shadow-md px-1.5 py-1 min-w-0">
|
|
104
|
+
<div class="text-[9px] font-semibold uppercase text-gray-400 leading-none">Vehicle</div>
|
|
105
|
+
<div class="text-[11px] text-white leading-tight whitespace-normal break-words">{{n-a driver.vehicle_name}}</div>
|
|
106
|
+
</div>
|
|
107
|
+
<div class="rounded bg-gray-900 shadow-md px-1.5 py-1 min-w-0">
|
|
108
|
+
<div class="text-[9px] font-semibold uppercase text-gray-400 leading-none">Email</div>
|
|
109
|
+
<div class="text-[11px] text-white leading-tight truncate min-w-0 w-full">{{n-a driver.email}}</div>
|
|
110
|
+
</div>
|
|
111
|
+
<div class="rounded bg-gray-900 shadow-md px-1.5 py-1 min-w-0">
|
|
112
|
+
<div class="text-[9px] font-semibold uppercase text-gray-400 leading-none">Order</div>
|
|
113
|
+
<div class="text-[11px] text-white leading-tight whitespace-normal break-words">{{n-a driver.meta.current_order_reference}}</div>
|
|
114
|
+
</div>
|
|
115
|
+
<div class="rounded bg-gray-900 shadow-md px-1.5 py-1 min-w-0">
|
|
116
|
+
<div class="text-[9px] font-semibold uppercase text-gray-400 leading-none">Speed</div>
|
|
117
|
+
<div class="text-[11px] text-white leading-tight whitespace-normal break-words">{{n-a driver.meta.speed_label}}</div>
|
|
118
|
+
</div>
|
|
119
|
+
<div class="rounded bg-gray-900 shadow-md px-1.5 py-1 min-w-0">
|
|
120
|
+
<div class="text-[9px] font-semibold uppercase text-gray-400 leading-none">Heading</div>
|
|
121
|
+
<div class="text-[11px] text-white leading-tight whitespace-normal break-words">{{n-a driver.meta.heading_label}}</div>
|
|
122
|
+
</div>
|
|
123
|
+
<div class="rounded bg-gray-900 shadow-md px-1.5 py-1 min-w-0">
|
|
124
|
+
<div class="text-[9px] font-semibold uppercase text-gray-400 leading-none">Location</div>
|
|
125
|
+
<div class="text-[11px] text-white leading-tight whitespace-normal break-words">{{n-a driver.meta.location_coordinates (point-coordinates driver.location)}}</div>
|
|
126
|
+
</div>
|
|
97
127
|
</div>
|
|
98
128
|
</div>
|
|
99
129
|
</marker.popup>
|
|
100
130
|
<marker.tooltip @permanent={{false}} @sticky={{true}}>
|
|
101
|
-
<div class="
|
|
102
|
-
<div class="text-xs font-semibold">
|
|
103
|
-
|
|
104
|
-
|
|
131
|
+
<div class="w-[280px] max-w-[calc(100vw-2rem)]">
|
|
132
|
+
<div class="flex items-center gap-1.5 text-xs font-semibold mb-1.5">
|
|
133
|
+
<span class="inline-block w-2 h-2 rounded-full {{if driver.online 'bg-green-400' 'bg-red-500'}}"></span>
|
|
134
|
+
<span>{{driver.name}}</span>
|
|
135
|
+
<div class="status-badge {{if driver.status (dasherize driver.status) 'default'}}-status-badge status-badge-{{if driver.status (dasherize driver.status) 'default'}} shadow-none ml-auto">
|
|
136
|
+
<span class="rounded-full text-[9px] px-1.5 py-0.5">{{n-a driver.meta.status_label (smart-humanize driver.status)}}</span>
|
|
137
|
+
</div>
|
|
138
|
+
</div>
|
|
139
|
+
<div class="grid grid-cols-2 gap-1">
|
|
140
|
+
<div class="rounded bg-gray-900 shadow-md px-1.5 py-1 min-w-0">
|
|
141
|
+
<div class="text-[9px] font-semibold uppercase text-gray-400 leading-none">ID</div>
|
|
142
|
+
<div class="text-[11px] text-white leading-tight whitespace-normal break-words">{{n-a driver.public_id driver.id}}</div>
|
|
143
|
+
</div>
|
|
144
|
+
<div class="rounded bg-gray-900 shadow-md px-1.5 py-1 min-w-0">
|
|
145
|
+
<div class="text-[9px] font-semibold uppercase text-gray-400 leading-none">Phone</div>
|
|
146
|
+
<div class="text-[11px] text-white leading-tight whitespace-normal break-words">{{n-a driver.phone}}</div>
|
|
147
|
+
</div>
|
|
148
|
+
<div class="rounded bg-gray-900 shadow-md px-1.5 py-1 min-w-0">
|
|
149
|
+
<div class="text-[9px] font-semibold uppercase text-gray-400 leading-none">Vehicle</div>
|
|
150
|
+
<div class="text-[11px] text-white leading-tight whitespace-normal break-words">{{n-a driver.vehicle_name}}</div>
|
|
151
|
+
</div>
|
|
152
|
+
<div class="rounded bg-gray-900 shadow-md px-1.5 py-1 min-w-0">
|
|
153
|
+
<div class="text-[9px] font-semibold uppercase text-gray-400 leading-none">Email</div>
|
|
154
|
+
<div class="text-[11px] text-white leading-tight truncate min-w-0 w-full">{{n-a driver.email}}</div>
|
|
155
|
+
</div>
|
|
156
|
+
<div class="rounded bg-gray-900 shadow-md px-1.5 py-1 min-w-0">
|
|
157
|
+
<div class="text-[9px] font-semibold uppercase text-gray-400 leading-none">Order</div>
|
|
158
|
+
<div class="text-[11px] text-white leading-tight whitespace-normal break-words">{{n-a driver.meta.current_order_reference}}</div>
|
|
159
|
+
</div>
|
|
160
|
+
<div class="rounded bg-gray-900 shadow-md px-1.5 py-1 min-w-0">
|
|
161
|
+
<div class="text-[9px] font-semibold uppercase text-gray-400 leading-none">Speed</div>
|
|
162
|
+
<div class="text-[11px] text-white leading-tight whitespace-normal break-words">{{n-a driver.meta.speed_label}}</div>
|
|
163
|
+
</div>
|
|
164
|
+
<div class="rounded bg-gray-900 shadow-md px-1.5 py-1 min-w-0">
|
|
165
|
+
<div class="text-[9px] font-semibold uppercase text-gray-400 leading-none">Heading</div>
|
|
166
|
+
<div class="text-[11px] text-white leading-tight whitespace-normal break-words">{{n-a driver.meta.heading_label}}</div>
|
|
167
|
+
</div>
|
|
168
|
+
<div class="rounded bg-gray-900 shadow-md px-1.5 py-1 min-w-0">
|
|
169
|
+
<div class="text-[9px] font-semibold uppercase text-gray-400 leading-none">Location</div>
|
|
170
|
+
<div class="text-[11px] text-white leading-tight whitespace-normal break-words">{{n-a driver.meta.location_coordinates (point-coordinates driver.location)}}</div>
|
|
171
|
+
</div>
|
|
172
|
+
</div>
|
|
105
173
|
</div>
|
|
106
|
-
<div class="text-xs truncate"><FaIcon @icon="location-dot" @size="xs" class="mr-0.5" />{{point-coordinates driver.location}}</div>
|
|
107
174
|
</marker.tooltip>
|
|
108
175
|
</layers.tracking-marker>
|
|
109
176
|
{{/each}}
|
|
@@ -121,29 +188,78 @@
|
|
|
121
188
|
as |marker|
|
|
122
189
|
>
|
|
123
190
|
<marker.popup @permanent={{false}} @sticky={{true}}>
|
|
124
|
-
<div class="
|
|
125
|
-
<div class="
|
|
126
|
-
<
|
|
191
|
+
<div class="w-[280px] max-w-[calc(100vw-2rem)] rounded-lg bg-gray-900 shadow-lg p-1.5 text-white">
|
|
192
|
+
<div class="flex items-center gap-1.5 text-xs font-semibold mb-1.5">
|
|
193
|
+
<span class="inline-block w-2 h-2 rounded-full {{if vehicle.online 'bg-green-400' 'bg-red-500'}}"></span>
|
|
194
|
+
<span>{{vehicle.displayName}}</span>
|
|
195
|
+
<div class="status-badge {{if vehicle.status (dasherize vehicle.status) 'default'}}-status-badge status-badge-{{if vehicle.status (dasherize vehicle.status) 'default'}} shadow-none ml-auto">
|
|
196
|
+
<span class="rounded-full text-[9px] px-1.5 py-0.5">{{n-a vehicle.meta.status_label}}</span>
|
|
197
|
+
</div>
|
|
127
198
|
</div>
|
|
128
|
-
<div class="
|
|
129
|
-
<div class="
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
<div class="
|
|
134
|
-
|
|
199
|
+
<div class="grid grid-cols-2 gap-1">
|
|
200
|
+
<div class="rounded bg-gray-900 shadow-md px-1.5 py-1 min-w-0">
|
|
201
|
+
<div class="text-[9px] font-semibold uppercase text-gray-400 leading-none">Vehicle #</div>
|
|
202
|
+
<div class="text-[11px] text-white leading-tight whitespace-normal break-words">{{n-a vehicle.internal_id vehicle.public_id vehicle.plate_number vehicle.serial_number vehicle.vin}}</div>
|
|
203
|
+
</div>
|
|
204
|
+
<div class="rounded bg-gray-900 shadow-md px-1.5 py-1 min-w-0">
|
|
205
|
+
<div class="text-[9px] font-semibold uppercase text-gray-400 leading-none">Driver</div>
|
|
206
|
+
<div class="text-[11px] text-white leading-tight whitespace-normal break-words">{{n-a vehicle.driver_name}}</div>
|
|
207
|
+
</div>
|
|
208
|
+
<div class="rounded bg-gray-900 shadow-md px-1.5 py-1 min-w-0">
|
|
209
|
+
<div class="text-[9px] font-semibold uppercase text-gray-400 leading-none">Order</div>
|
|
210
|
+
<div class="text-[11px] text-white leading-tight whitespace-normal break-words">{{n-a vehicle.meta.current_order_reference}}</div>
|
|
211
|
+
</div>
|
|
212
|
+
<div class="rounded bg-gray-900 shadow-md px-1.5 py-1 min-w-0">
|
|
213
|
+
<div class="text-[9px] font-semibold uppercase text-gray-400 leading-none">Speed</div>
|
|
214
|
+
<div class="text-[11px] text-white leading-tight whitespace-normal break-words">{{n-a vehicle.meta.speed_label}}</div>
|
|
215
|
+
</div>
|
|
216
|
+
<div class="rounded bg-gray-900 shadow-md px-1.5 py-1 min-w-0">
|
|
217
|
+
<div class="text-[9px] font-semibold uppercase text-gray-400 leading-none">Heading</div>
|
|
218
|
+
<div class="text-[11px] text-white leading-tight whitespace-normal break-words">{{n-a vehicle.meta.heading_label}}</div>
|
|
219
|
+
</div>
|
|
220
|
+
<div class="rounded bg-gray-900 shadow-md px-1.5 py-1 min-w-0">
|
|
221
|
+
<div class="text-[9px] font-semibold uppercase text-gray-400 leading-none">Location</div>
|
|
222
|
+
<div class="text-[11px] text-white leading-tight whitespace-normal break-words">{{n-a vehicle.meta.location_coordinates}}</div>
|
|
223
|
+
</div>
|
|
135
224
|
</div>
|
|
136
225
|
</div>
|
|
137
226
|
</marker.popup>
|
|
138
227
|
<marker.tooltip @permanent={{false}} @sticky={{true}}>
|
|
139
|
-
<div class="
|
|
140
|
-
<div class="text-xs font-semibold">
|
|
141
|
-
|
|
142
|
-
|
|
228
|
+
<div class="w-[280px] max-w-[calc(100vw-2rem)]">
|
|
229
|
+
<div class="flex items-center gap-1.5 text-xs font-semibold mb-1.5">
|
|
230
|
+
<span class="inline-block w-2 h-2 rounded-full {{if vehicle.online 'bg-green-400' 'bg-red-500'}}"></span>
|
|
231
|
+
<span>{{vehicle.displayName}}</span>
|
|
232
|
+
<div class="status-badge {{if vehicle.status (dasherize vehicle.status) 'default'}}-status-badge status-badge-{{if vehicle.status (dasherize vehicle.status) 'default'}} shadow-none ml-auto">
|
|
233
|
+
<span class="rounded-full text-[9px] px-1.5 py-0.5">{{n-a vehicle.meta.status_label}}</span>
|
|
234
|
+
</div>
|
|
235
|
+
</div>
|
|
236
|
+
<div class="grid grid-cols-2 gap-1">
|
|
237
|
+
<div class="rounded bg-gray-900 shadow-md px-1.5 py-1 min-w-0">
|
|
238
|
+
<div class="text-[9px] font-semibold uppercase text-gray-400 leading-none">Vehicle #</div>
|
|
239
|
+
<div class="text-[11px] text-white leading-tight whitespace-normal break-words">{{n-a vehicle.internal_id vehicle.public_id vehicle.plate_number vehicle.serial_number vehicle.vin}}</div>
|
|
240
|
+
</div>
|
|
241
|
+
<div class="rounded bg-gray-900 shadow-md px-1.5 py-1 min-w-0">
|
|
242
|
+
<div class="text-[9px] font-semibold uppercase text-gray-400 leading-none">Driver</div>
|
|
243
|
+
<div class="text-[11px] text-white leading-tight whitespace-normal break-words">{{n-a vehicle.driver_name}}</div>
|
|
244
|
+
</div>
|
|
245
|
+
<div class="rounded bg-gray-900 shadow-md px-1.5 py-1 min-w-0">
|
|
246
|
+
<div class="text-[9px] font-semibold uppercase text-gray-400 leading-none">Order</div>
|
|
247
|
+
<div class="text-[11px] text-white leading-tight whitespace-normal break-words">{{n-a vehicle.meta.current_order_reference}}</div>
|
|
248
|
+
</div>
|
|
249
|
+
<div class="rounded bg-gray-900 shadow-md px-1.5 py-1 min-w-0">
|
|
250
|
+
<div class="text-[9px] font-semibold uppercase text-gray-400 leading-none">Speed</div>
|
|
251
|
+
<div class="text-[11px] text-white leading-tight whitespace-normal break-words">{{n-a vehicle.meta.speed_label}}</div>
|
|
252
|
+
</div>
|
|
253
|
+
<div class="rounded bg-gray-900 shadow-md px-1.5 py-1 min-w-0">
|
|
254
|
+
<div class="text-[9px] font-semibold uppercase text-gray-400 leading-none">Heading</div>
|
|
255
|
+
<div class="text-[11px] text-white leading-tight whitespace-normal break-words">{{n-a vehicle.meta.heading_label}}</div>
|
|
256
|
+
</div>
|
|
257
|
+
<div class="rounded bg-gray-900 shadow-md px-1.5 py-1 min-w-0">
|
|
258
|
+
<div class="text-[9px] font-semibold uppercase text-gray-400 leading-none">Location</div>
|
|
259
|
+
<div class="text-[11px] text-white leading-tight whitespace-normal break-words">{{n-a vehicle.meta.location_coordinates}}</div>
|
|
260
|
+
</div>
|
|
261
|
+
</div>
|
|
143
262
|
</div>
|
|
144
|
-
<div class="text-xs">ID: {{n-a vehicle.public_id}}</div>
|
|
145
|
-
<div class="text-xs">Serial No: {{or vehicle.serial_number vehicle.vin vehicle.internal_id vehicle.id "-"}}</div>
|
|
146
|
-
<div class="text-xs truncate"><FaIcon @icon="location-dot" @size="xs" class="mr-0.5" />{{point-coordinates vehicle.location}}</div>
|
|
147
263
|
</marker.tooltip>
|
|
148
264
|
</layers.tracking-marker>
|
|
149
265
|
{{/each}}
|
|
@@ -224,4 +340,4 @@
|
|
|
224
340
|
@liveMap={{this}}
|
|
225
341
|
@onDrawerReady={{this.setDrawerContext}}
|
|
226
342
|
/>
|
|
227
|
-
{{/if}} --}}
|
|
343
|
+
{{/if}} --}}
|
|
@@ -130,6 +130,27 @@
|
|
|
130
130
|
</InputGroup>
|
|
131
131
|
{{/if}}
|
|
132
132
|
|
|
133
|
+
{{#if this.shouldShowVehiclePacking}}
|
|
134
|
+
<InputGroup @name={{t "orchestrator.vehicle-packing"}}>
|
|
135
|
+
<div class="grid grid-cols-2 gap-1">
|
|
136
|
+
{{#each this.vehiclePackingOptions as |opt|}}
|
|
137
|
+
<button
|
|
138
|
+
type="button"
|
|
139
|
+
class="flex items-center justify-center gap-1.5 px-2 py-1.5 rounded border text-xs transition-colors
|
|
140
|
+
{{if
|
|
141
|
+
(eq this.draftPhase.vehiclePacking opt.value)
|
|
142
|
+
'bg-indigo-50 dark:bg-indigo-900/30 border-indigo-400 dark:border-indigo-600 text-indigo-700 dark:text-indigo-300'
|
|
143
|
+
'border-gray-200 dark:border-gray-600 text-gray-600 dark:text-gray-400 hover:border-gray-300 dark:hover:border-gray-500'
|
|
144
|
+
}}"
|
|
145
|
+
{{on "click" (fn this.setDraftOption "vehiclePacking" opt.value)}}
|
|
146
|
+
>
|
|
147
|
+
{{opt.label}}
|
|
148
|
+
</button>
|
|
149
|
+
{{/each}}
|
|
150
|
+
</div>
|
|
151
|
+
</InputGroup>
|
|
152
|
+
{{/if}}
|
|
153
|
+
|
|
133
154
|
{{! Constraint toggles }}
|
|
134
155
|
<InputGroup @name={{t "orchestrator.constraints"}}>
|
|
135
156
|
<div class="space-y-1.5">
|
|
@@ -51,6 +51,7 @@ export default class OrchestratorPhaseBuilderComponent extends Component {
|
|
|
51
51
|
label: this.intl.t(`orchestrator.mode-${mode.replace(/_/g, '-')}`),
|
|
52
52
|
engine: 'greedy',
|
|
53
53
|
allocationStrategy: 'route_aware',
|
|
54
|
+
vehiclePacking: 'minimize_vehicles',
|
|
54
55
|
orderStatuses: ['created'],
|
|
55
56
|
balanceWorkload: false,
|
|
56
57
|
respectSkills: true,
|
|
@@ -130,6 +131,7 @@ export default class OrchestratorPhaseBuilderComponent extends Component {
|
|
|
130
131
|
...this.draftPhase,
|
|
131
132
|
engine,
|
|
132
133
|
allocationStrategy: engine === 'capacity' ? 'capacity_only' : (this.draftPhase.allocationStrategy ?? 'route_aware'),
|
|
134
|
+
vehiclePacking: this.draftPhase.vehiclePacking ?? 'minimize_vehicles',
|
|
133
135
|
};
|
|
134
136
|
}
|
|
135
137
|
|
|
@@ -158,10 +160,21 @@ export default class OrchestratorPhaseBuilderComponent extends Component {
|
|
|
158
160
|
];
|
|
159
161
|
}
|
|
160
162
|
|
|
163
|
+
get vehiclePackingOptions() {
|
|
164
|
+
return [
|
|
165
|
+
{ value: 'minimize_vehicles', label: this.intl.t('orchestrator.vehicle-packing-minimize-vehicles') },
|
|
166
|
+
{ value: 'balanced', label: this.intl.t('orchestrator.vehicle-packing-balanced') },
|
|
167
|
+
];
|
|
168
|
+
}
|
|
169
|
+
|
|
161
170
|
get shouldShowAllocationStrategy() {
|
|
162
171
|
return this.draftPhase?.mode === 'assign_vehicles' && this.draftPhase?.engine === 'vroom';
|
|
163
172
|
}
|
|
164
173
|
|
|
174
|
+
get shouldShowVehiclePacking() {
|
|
175
|
+
return this.shouldShowAllocationStrategy && this.draftPhase?.allocationStrategy === 'capacity_only';
|
|
176
|
+
}
|
|
177
|
+
|
|
165
178
|
get shouldShowRouteOptions() {
|
|
166
179
|
return this.draftPhase?.engine !== 'capacity' && this.draftPhase?.allocationStrategy !== 'capacity_only';
|
|
167
180
|
}
|
|
@@ -271,6 +271,7 @@ export default class OrchestratorWorkbenchComponent extends Component {
|
|
|
271
271
|
options: {
|
|
272
272
|
engine: phase.engine ?? 'greedy',
|
|
273
273
|
allocation_strategy: phase.allocationStrategy ?? 'route_aware',
|
|
274
|
+
vehicle_packing: phase.vehiclePacking ?? 'minimize_vehicles',
|
|
274
275
|
balance_workload: phase.balanceWorkload ?? false,
|
|
275
276
|
respect_skills: phase.respectSkills ?? true,
|
|
276
277
|
respect_capacity: phase.respectCapacity ?? true,
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
<Order::Form::Details @resource={{@resource}} />
|
|
18
18
|
<Order::Form::Route @resource={{@resource}} />
|
|
19
19
|
<RegistryYield @registry="fleet-ops:component:order:form" as |RegistryComponent|>
|
|
20
|
-
<RegistryComponent @order={{@
|
|
20
|
+
<RegistryComponent @order={{@resource}} @resource={{@resource}} />
|
|
21
21
|
</RegistryYield>
|
|
22
22
|
<Order::Form::Payload @resource={{@resource}} />
|
|
23
23
|
<Order::Form::ServiceRate @resource={{@resource}} />
|
|
@@ -25,4 +25,4 @@
|
|
|
25
25
|
<Order::Form::Documents @resource={{@resource}} />
|
|
26
26
|
<Order::Form::Metadata @resource={{@resource}} />
|
|
27
27
|
{{/if}}
|
|
28
|
-
</div>
|
|
28
|
+
</div>
|
|
@@ -1,19 +1,21 @@
|
|
|
1
|
-
<div class="fleet-ops-key-metrics" ...attributes>
|
|
2
|
-
<div class="flex flex-
|
|
3
|
-
<
|
|
4
|
-
|
|
5
|
-
|
|
1
|
+
<div class="fleet-ops-key-metrics flex flex-col h-full" ...attributes>
|
|
2
|
+
<div class="flex flex-row 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.key-metrics.title"}}</span>
|
|
4
|
+
<span class="text-[9px] uppercase tracking-wide text-amber-500 font-bold">Legacy</span>
|
|
5
|
+
</div>
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
7
|
+
<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 min-h-0">
|
|
8
|
+
{{#if this.getDashboardMetrics.isRunning}}
|
|
9
|
+
<div class="flex items-center justify-center h-full"><Spinner /></div>
|
|
10
|
+
{{else}}
|
|
11
|
+
<div class="grid grid-cols-2 lg:grid-cols-3 gap-1.5">
|
|
12
|
+
{{#each this.topMetrics as |entry|}}
|
|
13
|
+
<div class="rounded-md border dark:border-gray-700 border-gray-200 dark:bg-gray-800 bg-gray-50 p-2 flex flex-col">
|
|
14
|
+
<span class="text-[9px] uppercase tracking-wide text-gray-500 dark:text-gray-400 font-semibold truncate">{{smart-humanize entry.key}}</span>
|
|
15
|
+
<span class="text-base font-bold text-black dark:text-gray-100 leading-tight mt-0.5 truncate">{{entry.formattedValue}}</span>
|
|
16
|
+
</div>
|
|
17
|
+
{{/each}}
|
|
18
|
+
</div>
|
|
19
|
+
{{/if}}
|
|
18
20
|
</div>
|
|
19
|
-
</div>
|
|
21
|
+
</div>
|
|
@@ -2,6 +2,18 @@ import Component from '@glimmer/component';
|
|
|
2
2
|
import { tracked } from '@glimmer/tracking';
|
|
3
3
|
import { inject as service } from '@ember/service';
|
|
4
4
|
import { task } from 'ember-concurrency';
|
|
5
|
+
import formatCurrency from '@fleetbase/ember-ui/utils/format-currency';
|
|
6
|
+
import formatMeters from '@fleetbase/ember-ui/utils/format-meters';
|
|
7
|
+
|
|
8
|
+
/** Subset shown in the compact legacy widget. Full 13 metrics still come back from
|
|
9
|
+
* the API; users who want the rest should adopt the per-tile KPI widgets. */
|
|
10
|
+
const TOP_KEYS = ['earnings', 'orders_completed', 'orders_in_progress', 'drivers_online', 'fuel_costs', 'total_distance_traveled'];
|
|
11
|
+
|
|
12
|
+
const FORMATTERS = {
|
|
13
|
+
money: (v, currency) => formatCurrency(v ?? 0, currency || 'USD'),
|
|
14
|
+
meters: (v) => formatMeters(v ?? 0),
|
|
15
|
+
count: (v) => v ?? 0,
|
|
16
|
+
};
|
|
5
17
|
|
|
6
18
|
export default class WidgetFleetOpsKeyMetricsComponent extends Component {
|
|
7
19
|
/**
|
|
@@ -89,4 +101,18 @@ export default class WidgetFleetOpsKeyMetricsComponent extends Component {
|
|
|
89
101
|
|
|
90
102
|
return options;
|
|
91
103
|
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Pre-formatted top-N entries the compact template renders. Surfaces a fixed
|
|
107
|
+
* priority order rather than dumping every available metric — the per-tile
|
|
108
|
+
* KPI widgets cover the long tail for users who want everything.
|
|
109
|
+
*/
|
|
110
|
+
get topMetrics() {
|
|
111
|
+
if (!this.metrics) return [];
|
|
112
|
+
return TOP_KEYS.filter((key) => this.metrics[key] !== undefined).map((key) => {
|
|
113
|
+
const { value, format } = this.metrics[key];
|
|
114
|
+
const formatter = FORMATTERS[format] ?? FORMATTERS.count;
|
|
115
|
+
return { key, formattedValue: formatter(value) };
|
|
116
|
+
});
|
|
117
|
+
}
|
|
92
118
|
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
<div class="fleet-ops-widget fleet-ops-fuel-efficiency 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.fuel-efficiency.title"}}</span>
|
|
4
|
+
<FaIcon @icon="gas-pump" 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="flex items-baseline justify-between mb-1.5">
|
|
14
|
+
<span class="text-sm font-bold text-black dark:text-gray-100">
|
|
15
|
+
{{format-currency this.data.summary.total_cost this.data.summary.currency}}
|
|
16
|
+
</span>
|
|
17
|
+
<span class="text-[10px] text-gray-500 dark:text-gray-400">
|
|
18
|
+
{{format-currency this.data.summary.avg_cost_per_km this.data.summary.currency}}/km avg
|
|
19
|
+
</span>
|
|
20
|
+
</div>
|
|
21
|
+
<div class="flex-1 min-h-0">
|
|
22
|
+
<Chart @type="bar" @labels={{this.data.labels}} @datasets={{this.data.datasets}} @options={{this.chartOptions}} />
|
|
23
|
+
</div>
|
|
24
|
+
{{/if}}
|
|
25
|
+
</div>
|
|
26
|
+
</div>
|