@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.
Files changed (151) hide show
  1. package/addon/components/driver/details.hbs +5 -1
  2. package/addon/components/driver/form.hbs +11 -1
  3. package/addon/components/fleet-listing-panel.hbs +2 -2
  4. package/addon/components/fleet-listing-panel.js +10 -0
  5. package/addon/components/map/google-live-map.js +12 -73
  6. package/addon/components/map/leaflet-live-map.hbs +149 -33
  7. package/addon/components/orchestrator/phase-builder.hbs +21 -0
  8. package/addon/components/orchestrator/phase-builder.js +13 -0
  9. package/addon/components/orchestrator-workbench.js +1 -0
  10. package/addon/components/order/form.hbs +2 -2
  11. package/addon/components/widget/fleet-ops-key-metrics.hbs +19 -17
  12. package/addon/components/widget/fleet-ops-key-metrics.js +26 -0
  13. package/addon/components/widget/fuel-efficiency.hbs +26 -0
  14. package/addon/components/widget/fuel-efficiency.js +43 -0
  15. package/addon/components/widget/geofence-violations.hbs +44 -0
  16. package/addon/components/widget/geofence-violations.js +55 -0
  17. package/addon/components/widget/issues-insights.hbs +43 -0
  18. package/addon/components/widget/issues-insights.js +62 -0
  19. package/addon/components/widget/kpi-active-orders.hbs +10 -0
  20. package/addon/components/widget/kpi-active-orders.js +5 -0
  21. package/addon/components/widget/kpi-aov.hbs +10 -0
  22. package/addon/components/widget/kpi-aov.js +5 -0
  23. package/addon/components/widget/kpi-distance.hbs +10 -0
  24. package/addon/components/widget/kpi-distance.js +5 -0
  25. package/addon/components/widget/kpi-drivers-online.hbs +10 -0
  26. package/addon/components/widget/kpi-drivers-online.js +5 -0
  27. package/addon/components/widget/kpi-earnings.hbs +10 -0
  28. package/addon/components/widget/kpi-earnings.js +5 -0
  29. package/addon/components/widget/kpi-open-issues.hbs +11 -0
  30. package/addon/components/widget/kpi-open-issues.js +5 -0
  31. package/addon/components/widget/kpi-tile.hbs +59 -0
  32. package/addon/components/widget/kpi-tile.js +168 -0
  33. package/addon/components/widget/live-fleet.hbs +122 -0
  34. package/addon/components/widget/live-fleet.js +365 -0
  35. package/addon/components/widget/maintenance-overview.hbs +45 -0
  36. package/addon/components/widget/maintenance-overview.js +40 -0
  37. package/addon/components/widget/on-time-delivery.hbs +27 -0
  38. package/addon/components/widget/on-time-delivery.js +73 -0
  39. package/addon/components/widget/operations-pulse.hbs +51 -0
  40. package/addon/components/widget/operations-pulse.js +173 -0
  41. package/addon/components/widget/orders-by-status.hbs +26 -0
  42. package/addon/components/widget/orders-by-status.js +54 -0
  43. package/addon/components/widget/revenue-trend.hbs +34 -0
  44. package/addon/components/widget/revenue-trend.js +67 -0
  45. package/addon/components/widget/top-drivers.hbs +44 -0
  46. package/addon/components/widget/top-drivers.js +54 -0
  47. package/addon/controllers/settings/routing.js +26 -0
  48. package/addon/extension.js +172 -3
  49. package/addon/services/map-adapter/google.js +13 -2
  50. package/addon/styles/fleetops-engine.css +114 -87
  51. package/addon/templates/maintenance/equipment/index.hbs +1 -1
  52. package/addon/templates/maintenance/parts/index.hbs +1 -1
  53. package/addon/templates/maintenance/schedules/index.hbs +1 -1
  54. package/addon/templates/management/vehicles/index.hbs +1 -1
  55. package/addon/templates/settings/avatars.hbs +1 -1
  56. package/addon/templates/settings/custom-fields.hbs +1 -1
  57. package/addon/templates/settings/map.hbs +1 -1
  58. package/addon/templates/settings/navigator-app.hbs +1 -1
  59. package/addon/templates/settings/notifications.hbs +1 -1
  60. package/addon/templates/settings/orchestrator.hbs +1 -1
  61. package/addon/templates/settings/payments/index.hbs +1 -1
  62. package/addon/templates/settings/payments/onboard.hbs +1 -1
  63. package/addon/templates/settings/routing.hbs +22 -2
  64. package/addon/templates/settings/scheduling.hbs +1 -1
  65. package/addon/utils/live-map-card-content.js +171 -0
  66. package/app/components/widget/fuel-efficiency.js +1 -0
  67. package/app/components/widget/geofence-violations.js +1 -0
  68. package/app/components/widget/issues-insights.js +1 -0
  69. package/app/components/widget/kpi-active-orders.js +1 -0
  70. package/app/components/widget/kpi-aov.js +1 -0
  71. package/app/components/widget/kpi-distance.js +1 -0
  72. package/app/components/widget/kpi-drivers-online.js +1 -0
  73. package/app/components/widget/kpi-earnings.js +1 -0
  74. package/app/components/widget/kpi-open-issues.js +1 -0
  75. package/app/components/widget/kpi-tile.js +1 -0
  76. package/app/components/widget/live-fleet.js +1 -0
  77. package/app/components/widget/maintenance-overview.js +1 -0
  78. package/app/components/widget/on-time-delivery.js +1 -0
  79. package/app/components/widget/operations-pulse.js +1 -0
  80. package/app/components/widget/orders-by-status.js +1 -0
  81. package/app/components/widget/revenue-trend.js +1 -0
  82. package/app/components/widget/top-drivers.js +1 -0
  83. package/app/utils/live-map-card-content.js +1 -0
  84. package/composer.json +1 -1
  85. package/extension.json +1 -1
  86. package/package.json +3 -3
  87. package/pnpm-workspace.yaml +1 -0
  88. package/server/config/telematics.php +111 -1
  89. package/server/migrations/2026_05_18_000001_add_license_expiry_to_drivers_table.php +25 -0
  90. package/server/seeders/Testing/Concerns/SeedsTestingData.php +183 -0
  91. package/server/seeders/Testing/ConnectivitySeeder.php +212 -0
  92. package/server/seeders/Testing/FleetSeeder.php +341 -0
  93. package/server/seeders/Testing/MaintenanceSeeder.php +238 -0
  94. package/server/seeders/Testing/NetworkSeeder.php +250 -0
  95. package/server/seeders/Testing/OrdersSeeder.php +214 -0
  96. package/server/seeders/Testing/TestingSeeder.php +40 -0
  97. package/server/seeders/{VroomOrchestrationSeeder.php → Testing/VroomOrchestrationSeeder.php} +8 -1
  98. package/server/src/Console/Commands/ProcessOperationalAlerts.php +279 -0
  99. package/server/src/Http/Controllers/Internal/v1/AnalyticsController.php +119 -0
  100. package/server/src/Http/Controllers/Internal/v1/MetricsController.php +82 -3
  101. package/server/src/Http/Controllers/Internal/v1/OrderController.php +12 -6
  102. package/server/src/Http/Controllers/Internal/v1/SettingController.php +46 -1
  103. package/server/src/Http/Resources/v1/Driver.php +2 -0
  104. package/server/src/Http/Resources/v1/Fleet.php +4 -0
  105. package/server/src/Http/Resources/v1/Index/Driver.php +48 -0
  106. package/server/src/Http/Resources/v1/Index/Vehicle.php +74 -1
  107. package/server/src/Models/Driver.php +2 -0
  108. package/server/src/Models/OrderConfig.php +14 -11
  109. package/server/src/Notifications/LateDeparture.php +48 -0
  110. package/server/src/Notifications/ProlongedStoppage.php +48 -0
  111. package/server/src/Notifications/RouteDeviation.php +48 -0
  112. package/server/src/Orchestration/Engines/VroomOrchestrationEngine.php +28 -1
  113. package/server/src/Providers/FleetOpsServiceProvider.php +5 -0
  114. package/server/src/Providers/NotificationServiceProvider.php +3 -0
  115. package/server/src/Support/Analytics/AbstractAnalytics.php +77 -0
  116. package/server/src/Support/Analytics/FuelEfficiency.php +83 -0
  117. package/server/src/Support/Analytics/GeofenceViolations.php +63 -0
  118. package/server/src/Support/Analytics/IssuesInsights.php +67 -0
  119. package/server/src/Support/Analytics/LiveFleet.php +127 -0
  120. package/server/src/Support/Analytics/MaintenanceOverview.php +70 -0
  121. package/server/src/Support/Analytics/OnTimeDelivery.php +75 -0
  122. package/server/src/Support/Analytics/OperationsPulse.php +86 -0
  123. package/server/src/Support/Analytics/OrdersByStatus.php +64 -0
  124. package/server/src/Support/Analytics/RevenueTrend.php +79 -0
  125. package/server/src/Support/Analytics/TopDrivers.php +92 -0
  126. package/server/src/Support/Metrics/AbstractMetric.php +170 -0
  127. package/server/src/Support/Metrics/ActiveLiveOrdersMetric.php +28 -0
  128. package/server/src/Support/Metrics/AvgOrderValueMetric.php +53 -0
  129. package/server/src/Support/Metrics/DriversOnlineMetric.php +30 -0
  130. package/server/src/Support/Metrics/EarningsMetric.php +30 -0
  131. package/server/src/Support/Metrics/FuelCostsMetric.php +30 -0
  132. package/server/src/Support/Metrics/MoneyMetric.php +30 -0
  133. package/server/src/Support/Metrics/OpenIssuesMetric.php +35 -0
  134. package/server/src/Support/Metrics/OrdersCanceledMetric.php +35 -0
  135. package/server/src/Support/Metrics/OrdersCompletedMetric.php +35 -0
  136. package/server/src/Support/Metrics/OrdersInProgressMetric.php +51 -0
  137. package/server/src/Support/Metrics/OrdersScheduledMetric.php +37 -0
  138. package/server/src/Support/Metrics/Registry.php +46 -0
  139. package/server/src/Support/Metrics/ResolvedIssuesMetric.php +35 -0
  140. package/server/src/Support/Metrics/TotalCustomersMetric.php +29 -0
  141. package/server/src/Support/Metrics/TotalDistanceTraveledMetric.php +35 -0
  142. package/server/src/Support/Metrics/TotalDriversMetric.php +28 -0
  143. package/server/src/Support/Metrics/TotalTimeTraveledMetric.php +35 -0
  144. package/server/src/Support/Metrics.php +46 -249
  145. package/server/src/Support/Telematics/Providers/AfaqyProvider.php +290 -0
  146. package/server/src/Support/Telematics/Providers/SafeeProvider.php +333 -0
  147. package/server/src/routes.php +16 -0
  148. package/server/tests/AnalyticsRoutesTest.php +41 -0
  149. package/server/tests/MetricsRegistryTest.php +88 -0
  150. package/server/tests/VroomOrchestrationEngineTest.php +91 -0
  151. 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={{@itemDropdownButtonCalculatePosition}}
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: [24, 24],
63
+ iconSize: [20, 20],
129
64
  title: driver.name,
130
- tooltip: buildDriverTooltipContent(driver),
65
+ tooltip: buildDriverLiveMapContent(driver),
131
66
  tooltipOptions: { html: true },
132
67
  rotationAngle: driver.heading,
133
- onClick: () => this.#openInfoWindow(`driver:${driver.id}`, buildDriverInfoWindowContent(driver), driver.id, () => this.args.onDriverClicked?.(driver)),
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: [24, 24],
93
+ iconSize: [20, 20],
159
94
  title: vehicle.displayName,
160
- tooltip: buildVehicleTooltipContent(vehicle),
95
+ tooltip: buildVehicleLiveMapContent(vehicle),
161
96
  tooltipOptions: { html: true },
162
97
  rotationAngle: vehicle.heading,
163
- onClick: () => this.#openInfoWindow(`vehicle:${vehicle.id}`, buildVehicleInfoWindowContent(vehicle), vehicle.id, () => this.args.onVehicleClicked?.(vehicle)),
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="flex flex-row">
87
- <div class="w-14">
88
- <img src={{driver.photoUrl}} alt={{driver.name}} class="rounded-md w-12 h-12 shadow-sm" />
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="flex-1">
91
- <div class="text-xs font-semibold">{{driver.name}}</div>
92
- <div class="text-xs">{{t "common.phone"}}: {{n-a driver.phone}}</div>
93
- <div class="text-xs">{{t "resource.vehicle"}}: {{n-a driver.vehicle_name}}</div>
94
- <div class="text-xs">{{t "common.status"}}:
95
- <span class="{{if driver.online 'text-green-500' 'text-red-400'}}">{{if driver.online "Online" "Offline"}}</span></div>
96
- <div class="text-xs truncate">{{t "common.pos"}}: {{point-coordinates driver.location}}</div>
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="flex items-center space-x-1">
102
- <div class="text-xs font-semibold">{{driver.name}}</div>
103
- <div>•</div>
104
- <div class="text-xs {{if driver.online 'text-green-500' 'text-red-400'}}">{{if driver.online "Online" "Offline"}}</div>
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="flex flex-row">
125
- <div class="w-14 mr-2">
126
- <img src={{vehicle.photo_url}} alt={{vehicle.display_name}} class="rounded-md w-14 h-12 shadow-sm" />
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="flex-1">
129
- <div class="text-xs font-semibold">{{vehicle.displayName}}</div>
130
- <div class="text-xs">ID: {{n-a vehicle.public_id}}</div>
131
- <div class="text-xs">Serial No: {{n-a vehicle.serial_number vehicle.vin vehicle.internal_id vehicle.id}}</div>
132
- <div class="text-xs">Driver: {{n-a vehicle.driver_name}}</div>
133
- <div class="text-xs">Status: <span class="{{if vehicle.online 'text-green-500' 'text-red-400'}}">{{if vehicle.online "Online" "Offline"}}</span></div>
134
- <div class="text-xs truncate">Pos: {{point-coordinates vehicle.location}}</div>
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="flex items-center space-x-1">
140
- <div class="text-xs font-semibold">{{vehicle.displayName}}</div>
141
- <div>•</div>
142
- <div class="text-xs {{if vehicle.online 'text-green-500' 'text-red-400'}}">{{if vehicle.online "Online" "Offline"}}</div>
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={{@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-col">
3
- <div class="flex flex-row items-center justify-between px-4 py-2 border dark:border-gray-700 border-gray-200 dark:bg-gray-800 bg-gray-50 rounded-lg shadow-sm">
4
- <span class="text-base font-bold text-black dark:text-gray-100">{{t "widget.key-metrics.title"}}</span>
5
- </div>
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
- <div class="mt-4">
8
- {{#if this.getDashboardMetrics.isRunning}}
9
- <Spinner />
10
- {{else}}
11
- <div class="grid grid-cols-2 lg:grid-cols-12 gap-4">
12
- {{#each-in this.metrics as |title options|}}
13
- <Widget::Count @title={{smart-humanize title}} @options={{options}} class="lg:col-span-2 h-full" />
14
- {{/each-in}}
15
- </div>
16
- {{/if}}
17
- </div>
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>