@fleetbase/fleetops-engine 0.6.48 → 0.6.50

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (266) hide show
  1. package/addon/components/customer/create-order-form.hbs +2 -2
  2. package/addon/components/customer/form.hbs +2 -2
  3. package/addon/components/customer/order-form.hbs +2 -2
  4. package/addon/components/customer/orders.hbs +2 -2
  5. package/addon/components/display-place.hbs +2 -2
  6. package/addon/components/driver/details.hbs +5 -1
  7. package/addon/components/driver/form.hbs +11 -1
  8. package/addon/components/fleet-listing-panel.hbs +2 -2
  9. package/addon/components/fleet-listing-panel.js +10 -0
  10. package/addon/components/map/google-live-map.js +18 -75
  11. package/addon/components/map/leaflet-live-map.hbs +187 -37
  12. package/addon/components/map/leaflet-live-map.js +184 -20
  13. package/addon/components/modals/order-change-destination.hbs +46 -0
  14. package/addon/components/modals/order-change-destination.js +106 -0
  15. package/addon/components/modals/stop-activity.hbs +28 -0
  16. package/addon/components/modals/stop-activity.js +52 -0
  17. package/addon/components/modals/update-order-activity.hbs +45 -3
  18. package/addon/components/modals/update-order-activity.js +143 -5
  19. package/addon/components/orchestrator/phase-builder.hbs +21 -0
  20. package/addon/components/orchestrator/phase-builder.js +13 -0
  21. package/addon/components/orchestrator/resource-panel.hbs +0 -10
  22. package/addon/components/orchestrator/resource-panel.js +2 -4
  23. package/addon/components/orchestrator-workbench.js +1 -0
  24. package/addon/components/order/activity-list.hbs +2 -2
  25. package/addon/components/order/activity-timeline.hbs +31 -29
  26. package/addon/components/order/details/activity.hbs +3 -3
  27. package/addon/components/order/details/activity.js +11 -0
  28. package/addon/components/order/details/documents.js +2 -0
  29. package/addon/components/order/details/purchase-rate.hbs +2 -2
  30. package/addon/components/order/details/route.hbs +11 -3
  31. package/addon/components/order/details/route.js +91 -14
  32. package/addon/components/order/details/tracking.hbs +101 -72
  33. package/addon/components/order/details/tracking.js +149 -2
  34. package/addon/components/order/form/details.hbs +3 -3
  35. package/addon/components/order/form/documents.js +11 -4
  36. package/addon/components/order/form/route.hbs +108 -104
  37. package/addon/components/order/form/route.js +37 -1
  38. package/addon/components/order/form/service-rate.hbs +2 -2
  39. package/addon/components/order/form.hbs +2 -2
  40. package/addon/components/order-config-manager/activity-flow.hbs +4 -1
  41. package/addon/components/order-config-manager/activity-flow.js +178 -12
  42. package/addon/components/place/address.js +1 -1
  43. package/addon/components/route-list.hbs +17 -17
  44. package/addon/components/route-list.js +42 -10
  45. package/addon/components/service-rate/details.hbs +41 -0
  46. package/addon/components/service-rate/form.hbs +118 -0
  47. package/addon/components/service-rate/form.js +55 -0
  48. package/addon/components/vehicle/form.js +1 -1
  49. package/addon/components/vehicle/pill.hbs +1 -1
  50. package/addon/components/widget/fleet-ops-key-metrics.hbs +19 -17
  51. package/addon/components/widget/fleet-ops-key-metrics.js +26 -0
  52. package/addon/components/widget/fuel-efficiency.hbs +26 -0
  53. package/addon/components/widget/fuel-efficiency.js +43 -0
  54. package/addon/components/widget/geofence-violations.hbs +44 -0
  55. package/addon/components/widget/geofence-violations.js +55 -0
  56. package/addon/components/widget/issues-insights.hbs +43 -0
  57. package/addon/components/widget/issues-insights.js +62 -0
  58. package/addon/components/widget/kpi-active-orders.hbs +10 -0
  59. package/addon/components/widget/kpi-active-orders.js +5 -0
  60. package/addon/components/widget/kpi-aov.hbs +10 -0
  61. package/addon/components/widget/kpi-aov.js +5 -0
  62. package/addon/components/widget/kpi-distance.hbs +10 -0
  63. package/addon/components/widget/kpi-distance.js +5 -0
  64. package/addon/components/widget/kpi-drivers-online.hbs +10 -0
  65. package/addon/components/widget/kpi-drivers-online.js +5 -0
  66. package/addon/components/widget/kpi-earnings.hbs +10 -0
  67. package/addon/components/widget/kpi-earnings.js +5 -0
  68. package/addon/components/widget/kpi-open-issues.hbs +11 -0
  69. package/addon/components/widget/kpi-open-issues.js +5 -0
  70. package/addon/components/widget/kpi-tile.hbs +59 -0
  71. package/addon/components/widget/kpi-tile.js +168 -0
  72. package/addon/components/widget/live-fleet.hbs +122 -0
  73. package/addon/components/widget/live-fleet.js +365 -0
  74. package/addon/components/widget/maintenance-overview.hbs +45 -0
  75. package/addon/components/widget/maintenance-overview.js +40 -0
  76. package/addon/components/widget/on-time-delivery.hbs +27 -0
  77. package/addon/components/widget/on-time-delivery.js +73 -0
  78. package/addon/components/widget/operations-pulse.hbs +51 -0
  79. package/addon/components/widget/operations-pulse.js +173 -0
  80. package/addon/components/widget/orders-by-status.hbs +26 -0
  81. package/addon/components/widget/orders-by-status.js +54 -0
  82. package/addon/components/widget/revenue-trend.hbs +34 -0
  83. package/addon/components/widget/revenue-trend.js +67 -0
  84. package/addon/components/widget/top-drivers.hbs +44 -0
  85. package/addon/components/widget/top-drivers.js +54 -0
  86. package/addon/controllers/management/vehicles/index/new.js +1 -1
  87. package/addon/controllers/operations/orders/index/details.js +4 -4
  88. package/addon/controllers/operations/orders/index.js +14 -0
  89. package/addon/controllers/settings/routing.js +26 -0
  90. package/addon/extension.js +172 -3
  91. package/addon/helpers/place-address-html.js +2 -2
  92. package/addon/routes/operations/service-rates/index.js +1 -1
  93. package/addon/services/geofence.js +163 -19
  94. package/addon/services/leaflet-contextmenu-manager.js +27 -0
  95. package/addon/services/map-adapter/google.js +13 -2
  96. package/addon/services/vehicle-actions.js +1 -1
  97. package/addon/styles/fleetops-engine.css +205 -88
  98. package/addon/templates/maintenance/equipment/index.hbs +1 -1
  99. package/addon/templates/maintenance/parts/index.hbs +1 -1
  100. package/addon/templates/maintenance/schedules/index.hbs +1 -1
  101. package/addon/templates/management/vehicles/index.hbs +1 -1
  102. package/addon/templates/settings/avatars.hbs +1 -1
  103. package/addon/templates/settings/custom-fields.hbs +1 -1
  104. package/addon/templates/settings/map.hbs +1 -1
  105. package/addon/templates/settings/navigator-app.hbs +1 -1
  106. package/addon/templates/settings/notifications.hbs +1 -1
  107. package/addon/templates/settings/orchestrator.hbs +1 -1
  108. package/addon/templates/settings/payments/index.hbs +1 -1
  109. package/addon/templates/settings/payments/onboard.hbs +1 -1
  110. package/addon/templates/settings/routing.hbs +22 -2
  111. package/addon/templates/settings/scheduling.hbs +1 -1
  112. package/addon/utils/fleet-ops-options.js +2 -1
  113. package/addon/utils/live-map-card-content.js +171 -0
  114. package/addon/utils/normalize-order-config-flow.js +41 -0
  115. package/addon/utils/place-address-html.js +3 -2
  116. package/addon/utils/route-visualization.js +54 -7
  117. package/app/components/modals/order-change-destination.js +1 -0
  118. package/app/components/modals/stop-activity.js +1 -0
  119. package/app/components/widget/fuel-efficiency.js +1 -0
  120. package/app/components/widget/geofence-violations.js +1 -0
  121. package/app/components/widget/issues-insights.js +1 -0
  122. package/app/components/widget/kpi-active-orders.js +1 -0
  123. package/app/components/widget/kpi-aov.js +1 -0
  124. package/app/components/widget/kpi-distance.js +1 -0
  125. package/app/components/widget/kpi-drivers-online.js +1 -0
  126. package/app/components/widget/kpi-earnings.js +1 -0
  127. package/app/components/widget/kpi-open-issues.js +1 -0
  128. package/app/components/widget/kpi-tile.js +1 -0
  129. package/app/components/widget/live-fleet.js +1 -0
  130. package/app/components/widget/maintenance-overview.js +1 -0
  131. package/app/components/widget/on-time-delivery.js +1 -0
  132. package/app/components/widget/operations-pulse.js +1 -0
  133. package/app/components/widget/orders-by-status.js +1 -0
  134. package/app/components/widget/revenue-trend.js +1 -0
  135. package/app/components/widget/top-drivers.js +1 -0
  136. package/app/utils/live-map-card-content.js +1 -0
  137. package/composer.json +1 -1
  138. package/extension.json +1 -1
  139. package/package.json +3 -3
  140. package/pnpm-workspace.yaml +1 -0
  141. package/server/config/telematics.php +111 -1
  142. package/server/migrations/2026_05_18_000001_add_license_expiry_to_drivers_table.php +25 -0
  143. package/server/migrations/2026_05_21_000001_add_geographic_rules_to_service_rate_fees_table.php +37 -0
  144. package/server/migrations/2026_05_25_000001_add_endpoint_tracking_refs_to_payloads_table.php +50 -0
  145. package/server/migrations/2026_05_27_000001_default_driver_vehicle_statuses_to_available.php +48 -0
  146. package/server/resources/views/labels/default.php +57 -49
  147. package/server/resources/views/labels/waypoint-label.php +27 -20
  148. package/server/seeders/Testing/Concerns/SeedsTestingData.php +183 -0
  149. package/server/seeders/Testing/ConnectivitySeeder.php +213 -0
  150. package/server/seeders/Testing/FleetSeeder.php +341 -0
  151. package/server/seeders/Testing/MaintenanceSeeder.php +238 -0
  152. package/server/seeders/Testing/NetworkSeeder.php +250 -0
  153. package/server/seeders/Testing/OrdersSeeder.php +198 -0
  154. package/server/seeders/Testing/TestingSeeder.php +40 -0
  155. package/server/seeders/{VroomOrchestrationSeeder.php → Testing/VroomOrchestrationSeeder.php} +29 -22
  156. package/server/src/Console/Commands/AuditCustomerUserConflicts.php +75 -0
  157. package/server/src/Console/Commands/ProcessOperationalAlerts.php +279 -0
  158. package/server/src/Exceptions/CustomerUserConflictException.php +13 -0
  159. package/server/src/Http/Controllers/Api/v1/ContactController.php +17 -2
  160. package/server/src/Http/Controllers/Api/v1/CustomerController.php +876 -0
  161. package/server/src/Http/Controllers/Api/v1/DriverController.php +1 -1
  162. package/server/src/Http/Controllers/Api/v1/OrderConfigController.php +54 -0
  163. package/server/src/Http/Controllers/Api/v1/OrderController.php +98 -70
  164. package/server/src/Http/Controllers/Api/v1/ServiceAreaController.php +2 -2
  165. package/server/src/Http/Controllers/Api/v1/VehicleController.php +20 -3
  166. package/server/src/Http/Controllers/Internal/v1/AnalyticsController.php +119 -0
  167. package/server/src/Http/Controllers/Internal/v1/ContactController.php +19 -0
  168. package/server/src/Http/Controllers/Internal/v1/LiveController.php +101 -66
  169. package/server/src/Http/Controllers/Internal/v1/MetricsController.php +82 -3
  170. package/server/src/Http/Controllers/Internal/v1/OrderController.php +441 -32
  171. package/server/src/Http/Controllers/Internal/v1/ServiceQuoteController.php +1 -1
  172. package/server/src/Http/Controllers/Internal/v1/SettingController.php +46 -1
  173. package/server/src/Http/Controllers/Internal/v1/VehicleController.php +54 -1
  174. package/server/src/Http/Middleware/AuthenticateCustomerToken.php +34 -0
  175. package/server/src/Http/Requests/CreateCustomerOrderRequest.php +63 -0
  176. package/server/src/Http/Requests/CreateCustomerRequest.php +48 -0
  177. package/server/src/Http/Requests/CreateDriverRequest.php +1 -1
  178. package/server/src/Http/Requests/CreateVehicleRequest.php +25 -1
  179. package/server/src/Http/Requests/Internal/CreateDriverRequest.php +1 -1
  180. package/server/src/Http/Requests/VerifyCreateCustomerRequest.php +29 -0
  181. package/server/src/Http/Resources/Internal/v1/OrderConfig.php +41 -0
  182. package/server/src/Http/Resources/v1/Customer.php +79 -0
  183. package/server/src/Http/Resources/v1/Driver.php +2 -0
  184. package/server/src/Http/Resources/v1/Fleet.php +4 -0
  185. package/server/src/Http/Resources/v1/Index/Driver.php +48 -0
  186. package/server/src/Http/Resources/v1/Index/Vehicle.php +74 -1
  187. package/server/src/Http/Resources/v1/OrderConfig.php +74 -0
  188. package/server/src/Http/Resources/v1/Payload.php +23 -20
  189. package/server/src/Http/Resources/v1/ServiceArea.php +33 -19
  190. package/server/src/Http/Resources/v1/ServiceRateFee.php +7 -0
  191. package/server/src/Listeners/HandleOrderDispatched.php +2 -2
  192. package/server/src/Models/Contact.php +92 -15
  193. package/server/src/Models/Customer.php +64 -0
  194. package/server/src/Models/Driver.php +14 -3
  195. package/server/src/Models/Entity.php +2 -2
  196. package/server/src/Models/Order.php +29 -4
  197. package/server/src/Models/OrderConfig.php +14 -11
  198. package/server/src/Models/Payload.php +17 -1
  199. package/server/src/Models/ServiceRate.php +273 -1
  200. package/server/src/Models/ServiceRateFee.php +39 -9
  201. package/server/src/Models/TrackingNumber.php +8 -2
  202. package/server/src/Models/Vehicle.php +18 -2
  203. package/server/src/Models/Waypoint.php +2 -2
  204. package/server/src/Notifications/LateDeparture.php +48 -0
  205. package/server/src/Notifications/ProlongedStoppage.php +48 -0
  206. package/server/src/Notifications/RouteDeviation.php +48 -0
  207. package/server/src/Observers/ContactObserver.php +2 -0
  208. package/server/src/Observers/ServiceRateObserver.php +6 -6
  209. package/server/src/Orchestration/Engines/VroomOrchestrationEngine.php +29 -2
  210. package/server/src/Providers/FleetOpsServiceProvider.php +6 -0
  211. package/server/src/Providers/NotificationServiceProvider.php +3 -0
  212. package/server/src/Support/Analytics/AbstractAnalytics.php +77 -0
  213. package/server/src/Support/Analytics/FuelEfficiency.php +83 -0
  214. package/server/src/Support/Analytics/GeofenceViolations.php +63 -0
  215. package/server/src/Support/Analytics/IssuesInsights.php +67 -0
  216. package/server/src/Support/Analytics/LiveFleet.php +127 -0
  217. package/server/src/Support/Analytics/MaintenanceOverview.php +70 -0
  218. package/server/src/Support/Analytics/OnTimeDelivery.php +75 -0
  219. package/server/src/Support/Analytics/OperationsPulse.php +86 -0
  220. package/server/src/Support/Analytics/OrdersByStatus.php +64 -0
  221. package/server/src/Support/Analytics/RevenueTrend.php +78 -0
  222. package/server/src/Support/Analytics/TopDrivers.php +92 -0
  223. package/server/src/Support/CustomerAuth.php +98 -0
  224. package/server/src/Support/LabelPdf.php +52 -0
  225. package/server/src/Support/Metrics/AbstractMetric.php +170 -0
  226. package/server/src/Support/Metrics/ActiveLiveOrdersMetric.php +28 -0
  227. package/server/src/Support/Metrics/AvgOrderValueMetric.php +53 -0
  228. package/server/src/Support/Metrics/DriversOnlineMetric.php +30 -0
  229. package/server/src/Support/Metrics/EarningsMetric.php +30 -0
  230. package/server/src/Support/Metrics/FuelCostsMetric.php +30 -0
  231. package/server/src/Support/Metrics/MoneyMetric.php +30 -0
  232. package/server/src/Support/Metrics/OpenIssuesMetric.php +35 -0
  233. package/server/src/Support/Metrics/OrdersCanceledMetric.php +35 -0
  234. package/server/src/Support/Metrics/OrdersCompletedMetric.php +35 -0
  235. package/server/src/Support/Metrics/OrdersInProgressMetric.php +51 -0
  236. package/server/src/Support/Metrics/OrdersScheduledMetric.php +37 -0
  237. package/server/src/Support/Metrics/Registry.php +46 -0
  238. package/server/src/Support/Metrics/ResolvedIssuesMetric.php +35 -0
  239. package/server/src/Support/Metrics/TotalCustomersMetric.php +29 -0
  240. package/server/src/Support/Metrics/TotalDistanceTraveledMetric.php +35 -0
  241. package/server/src/Support/Metrics/TotalDriversMetric.php +28 -0
  242. package/server/src/Support/Metrics/TotalTimeTraveledMetric.php +35 -0
  243. package/server/src/Support/Metrics.php +46 -249
  244. package/server/src/Support/ResolvesOrderServiceStops.php +512 -0
  245. package/server/src/Support/Telematics/Providers/AfaqyProvider.php +290 -0
  246. package/server/src/Support/Telematics/Providers/SafeeProvider.php +333 -0
  247. package/server/src/Tracking/TrackingContextBuilder.php +41 -38
  248. package/server/src/Tracking/TrackingIntelligenceService.php +109 -9
  249. package/server/src/Tracking/TrackingStop.php +13 -11
  250. package/server/src/routes.php +50 -0
  251. package/server/tests/AnalyticsRoutesTest.php +41 -0
  252. package/server/tests/CapacityAllocationEngineTest.php +10 -10
  253. package/server/tests/CustomerContactIdentitySafetyTest.php +54 -0
  254. package/server/tests/CustomerEndpointTest.php +199 -0
  255. package/server/tests/DriverVehicleStatusDefaultTest.php +39 -0
  256. package/server/tests/LabelPdfRenderingTest.php +23 -0
  257. package/server/tests/LiveControllerViewportTest.php +60 -0
  258. package/server/tests/MetricsRegistryTest.php +84 -0
  259. package/server/tests/OrchestrationPayloadBuilderTest.php +2 -2
  260. package/server/tests/OrderConfigEndpointTest.php +86 -0
  261. package/server/tests/OrderFileAttachmentTest.php +20 -0
  262. package/server/tests/ServiceAreaZoneApiShapeTest.php +34 -0
  263. package/server/tests/VehicleDriverAssignmentTest.php +18 -0
  264. package/server/tests/VroomOrchestrationEngineTest.php +91 -0
  265. package/translations/en-us.yaml +38 -0
  266. package/server/migrations/2026_05_13_000001_repair_customer_contact_user_types.php +0 -86
@@ -4,11 +4,29 @@ import { action } from '@ember/object';
4
4
 
5
5
  export default class ServiceRateFormComponent extends Component {
6
6
  @service orderConfigActions;
7
+ @service serviceAreaActions;
7
8
  @service serviceRateActions;
8
9
 
9
10
  constructor() {
10
11
  super(...arguments);
11
12
  this.orderConfigActions.loadAll.perform();
13
+ this.serviceAreaActions.loadAll.perform();
14
+ }
15
+
16
+ get serviceAreas() {
17
+ return this.serviceAreaActions.serviceAreas ?? [];
18
+ }
19
+
20
+ get zones() {
21
+ return Array.from(this.serviceAreas).flatMap((serviceArea) => serviceArea.zones?.toArray?.() ?? serviceArea.zones ?? []);
22
+ }
23
+
24
+ get geographyTypes() {
25
+ return [
26
+ { label: 'Service Area', value: 'service_area' },
27
+ { label: 'Zone', value: 'zone' },
28
+ { label: 'Fallback', value: 'fallback' },
29
+ ];
12
30
  }
13
31
 
14
32
  @action selectOrderConfig(orderConfig) {
@@ -24,10 +42,47 @@ export default class ServiceRateFormComponent extends Component {
24
42
  this.args.resource.resetPerDropFees();
25
43
  } else if (rateCalculationMethod === 'fixed_meter' || rateCalculationMethod === 'fixed_rate') {
26
44
  this.serviceRateActions.generateFixedRateFees(this.args.resource);
45
+ } else if (rateCalculationMethod === 'multi_zone_distance' && !this.args.resource.rateFees.length) {
46
+ this.args.resource.addMultiZoneDistanceRule();
47
+ this.args.resource.addMultiZoneDistanceFallbackRule();
27
48
  }
28
49
  }
29
50
 
30
51
  @action onMaxDistanceChange() {
31
52
  this.serviceRateActions.generateFixedRateFees(this.args.resource);
32
53
  }
54
+
55
+ @action selectRuleGeographyType(rule, { value }) {
56
+ rule.set('zone', null);
57
+ rule.set('zone_uuid', null);
58
+ rule.set('service_area', null);
59
+ rule.set('service_area_uuid', null);
60
+ rule.set('is_fallback', value === 'fallback');
61
+ }
62
+
63
+ @action selectRuleServiceArea(rule, serviceArea) {
64
+ rule.set('service_area', serviceArea);
65
+ rule.set('service_area_uuid', serviceArea?.id);
66
+ rule.set('zone', null);
67
+ rule.set('zone_uuid', null);
68
+ rule.set('is_fallback', false);
69
+ }
70
+
71
+ @action selectRuleZone(rule, zone) {
72
+ rule.set('zone', zone);
73
+ rule.set('zone_uuid', zone?.id);
74
+ rule.set('service_area', null);
75
+ rule.set('service_area_uuid', null);
76
+ rule.set('is_fallback', false);
77
+ }
78
+
79
+ @action setRuleFallback(rule, isFallback) {
80
+ rule.set('is_fallback', Boolean(isFallback));
81
+ if (isFallback) {
82
+ rule.set('zone', null);
83
+ rule.set('zone_uuid', null);
84
+ rule.set('service_area', null);
85
+ rule.set('service_area_uuid', null);
86
+ }
87
+ }
33
88
  }
@@ -10,7 +10,7 @@ export default class VehicleFormComponent extends Component {
10
10
  @service currentUser;
11
11
  @service notifications;
12
12
  @service modalsManager;
13
- @tracked statusOptions = ['active', 'pending'];
13
+ @tracked statusOptions = ['available', 'pending'];
14
14
 
15
15
  @action updateAvatarUrl(option) {
16
16
  this.args.resource.avatar_url = option.key === 'custom_avatar' ? option.value : [option.value];
@@ -20,7 +20,7 @@
20
20
  <:tooltip>
21
21
  <div>
22
22
  <div class="text-xs font-semibold">{{this.resource.name this.resource.yearMakeModel}}</div>
23
- <div class="text-xs">{{t "this.resource.driver"}}: {{n-a this.resource.driver_name}}</div>
23
+ <div class="text-xs">{{t "resource.driver"}}: {{n-a this.resource.driver_name}}</div>
24
24
  <div class="text-xs">
25
25
  <span>{{t "common.status"}}:</span>
26
26
  <span class="{{if this.resource.online 'text-green-500' 'text-red-400'}}">
@@ -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>
@@ -0,0 +1,43 @@
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 WidgetFuelEfficiencyComponent extends Component {
7
+ static widgetId = 'fleet-ops-fuel-efficiency-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 chartOptions() {
20
+ return {
21
+ responsive: true,
22
+ maintainAspectRatio: false,
23
+ interaction: { mode: 'index', intersect: false },
24
+ plugins: {
25
+ legend: { position: 'bottom', labels: { boxWidth: 12, padding: 8 } },
26
+ },
27
+ scales: {
28
+ x: { grid: { display: false } },
29
+ y1: { position: 'left', beginAtZero: true, title: { display: false } },
30
+ y2: { position: 'right', beginAtZero: true, grid: { drawOnChartArea: false } },
31
+ },
32
+ };
33
+ }
34
+
35
+ @task *load() {
36
+ try {
37
+ this.data = yield this.fetch.get('fleet-ops/analytics/fuel-efficiency', { period: '90d' });
38
+ this.error = null;
39
+ } catch (e) {
40
+ this.error = e?.message ?? 'Failed to load fuel data';
41
+ }
42
+ }
43
+ }
@@ -0,0 +1,44 @@
1
+ <div class="fleet-ops-widget fleet-ops-geofence-violations 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.geofence-violations.title"}}</span>
4
+ <FaIcon @icon="location-crosshairs" 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-2 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">Today</div>
16
+ <div class="text-lg font-bold leading-tight mt-0.5 {{if this.data.violations_today 'text-amber-500' 'text-gray-400'}}">{{this.data.violations_today}}</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">Last 7d</div>
20
+ <div class="text-lg font-bold text-black dark:text-gray-100 leading-tight mt-0.5">{{this.data.violations_period}}</div>
21
+ </div>
22
+ </div>
23
+ <div class="grid grid-cols-2 gap-2 flex-1 min-h-0">
24
+ <div class="min-h-0">
25
+ {{#if this.zoneDatasets}}
26
+ <Chart @type="bar" @labels={{this.zoneLabels}} @datasets={{this.zoneDatasets}} @options={{this.zoneOptions}} />
27
+ {{else}}
28
+ <div class="flex items-center justify-center h-full text-[10px] text-gray-500">No zone activity.</div>
29
+ {{/if}}
30
+ </div>
31
+ <div class="min-h-0 overflow-auto divide-y dark:divide-gray-700">
32
+ {{#each this.data.top_dwells as |dwell|}}
33
+ <div class="py-1">
34
+ <div class="text-[11px] font-medium text-black dark:text-gray-100 truncate">{{or dwell.driver_name "Driver"}}</div>
35
+ <div class="text-[10px] text-gray-500 dark:text-gray-400 truncate">{{or dwell.zone_name "Zone"}} &middot; {{dwell.duration_minutes}}m</div>
36
+ </div>
37
+ {{else}}
38
+ <div class="text-center text-[10px] text-gray-500 py-3">No dwell outliers.</div>
39
+ {{/each}}
40
+ </div>
41
+ </div>
42
+ {{/if}}
43
+ </div>
44
+ </div>
@@ -0,0 +1,55 @@
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 WidgetGeofenceViolationsComponent extends Component {
7
+ static widgetId = 'fleet-ops-geofence-violations-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 zoneDatasets() {
20
+ if (!this.data?.by_zone?.data?.length) return null;
21
+ return [
22
+ {
23
+ label: 'Events',
24
+ data: this.data.by_zone.data,
25
+ backgroundColor: '#f59e0b',
26
+ },
27
+ ];
28
+ }
29
+
30
+ get zoneLabels() {
31
+ return this.data?.by_zone?.labels ?? [];
32
+ }
33
+
34
+ get zoneOptions() {
35
+ return {
36
+ responsive: true,
37
+ maintainAspectRatio: false,
38
+ indexAxis: 'y',
39
+ plugins: { legend: { display: false } },
40
+ scales: {
41
+ x: { beginAtZero: true, ticks: { precision: 0 } },
42
+ y: { grid: { display: false } },
43
+ },
44
+ };
45
+ }
46
+
47
+ @task *load() {
48
+ try {
49
+ this.data = yield this.fetch.get('fleet-ops/analytics/geofence-violations', { period: '7d' });
50
+ this.error = null;
51
+ } catch (e) {
52
+ this.error = e?.message ?? 'Failed to load geofence data';
53
+ }
54
+ }
55
+ }
@@ -0,0 +1,43 @@
1
+ <div class="fleet-ops-widget fleet-ops-issues-insights 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.issues-insights.title"}}</span>
4
+ <FaIcon @icon="triangle-exclamation" class="text-gray-400" />
5
+ </div>
6
+
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.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-2 gap-2 h-full">
14
+ <div class="min-h-0">
15
+ {{#if this.categoryDatasets}}
16
+ <Chart @type="doughnut" @labels={{this.categoryLabels}} @datasets={{this.categoryDatasets}} @options={{this.categoryOptions}} />
17
+ {{else}}
18
+ <div class="flex items-center justify-center h-full text-[10px] text-gray-500">No issues yet.</div>
19
+ {{/if}}
20
+ </div>
21
+ <div class="flex flex-col justify-around gap-1.5">
22
+ <div>
23
+ <div class="text-[9px] uppercase text-gray-500 dark:text-gray-400 leading-none">Open</div>
24
+ <div class="text-lg font-bold text-amber-500 leading-tight">{{this.data.open}}</div>
25
+ </div>
26
+ <div>
27
+ <div class="text-[9px] uppercase text-gray-500 dark:text-gray-400 leading-none">Resolved (30d)</div>
28
+ <div class="text-lg font-bold text-green-500 leading-tight">{{this.data.resolved_this_period}}</div>
29
+ </div>
30
+ <div>
31
+ <div class="text-[9px] uppercase text-gray-500 dark:text-gray-400 leading-none">Avg Resolve</div>
32
+ <div class="text-sm font-bold text-black dark:text-gray-100 leading-tight">{{this.resolutionDisplay}}</div>
33
+ </div>
34
+ <div class="flex flex-wrap gap-1">
35
+ <Badge @status="danger" @text={{concat "H " this.data.by_priority.high}} @hideIcon={{true}} @disableHumanize={{true}} @wrapperClass="text-[9px] px-1 py-0" />
36
+ <Badge @status="warning" @text={{concat "M " this.data.by_priority.medium}} @hideIcon={{true}} @disableHumanize={{true}} @wrapperClass="text-[9px] px-1 py-0" />
37
+ <Badge @status="info" @text={{concat "L " this.data.by_priority.low}} @hideIcon={{true}} @disableHumanize={{true}} @wrapperClass="text-[9px] px-1 py-0" />
38
+ </div>
39
+ </div>
40
+ </div>
41
+ {{/if}}
42
+ </div>
43
+ </div>
@@ -0,0 +1,62 @@
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
+ const CATEGORY_PALETTE = ['#3485e2', '#f59e0b', '#22c55e', '#ef4444', '#8b5cf6', '#a3a3a3', '#0ea5e9', '#ec4899'];
7
+
8
+ export default class WidgetIssuesInsightsComponent extends Component {
9
+ static widgetId = 'fleet-ops-issues-insights-widget';
10
+
11
+ @service fetch;
12
+
13
+ @tracked data = null;
14
+ @tracked error = null;
15
+
16
+ constructor() {
17
+ super(...arguments);
18
+ this.load.perform();
19
+ }
20
+
21
+ get categoryDatasets() {
22
+ if (!this.data?.by_category?.data?.length) return null;
23
+ return [
24
+ {
25
+ data: this.data.by_category.data,
26
+ backgroundColor: this.data.by_category.data.map((_, i) => CATEGORY_PALETTE[i % CATEGORY_PALETTE.length]),
27
+ borderWidth: 0,
28
+ },
29
+ ];
30
+ }
31
+
32
+ get categoryLabels() {
33
+ return this.data?.by_category?.labels ?? [];
34
+ }
35
+
36
+ get categoryOptions() {
37
+ return {
38
+ responsive: true,
39
+ maintainAspectRatio: false,
40
+ plugins: {
41
+ legend: { position: 'bottom', labels: { boxWidth: 10, padding: 6, font: { size: 10 } } },
42
+ },
43
+ };
44
+ }
45
+
46
+ get resolutionDisplay() {
47
+ const hours = this.data?.avg_resolution_hours;
48
+ if (typeof hours !== 'number') return '—';
49
+ if (hours < 1) return `${Math.round(hours * 60)}m`;
50
+ if (hours < 48) return `${hours}h`;
51
+ return `${(hours / 24).toFixed(1)}d`;
52
+ }
53
+
54
+ @task *load() {
55
+ try {
56
+ this.data = yield this.fetch.get('fleet-ops/analytics/issues-insights', { period: '30d' });
57
+ this.error = null;
58
+ } catch (e) {
59
+ this.error = e?.message ?? 'Failed to load issues';
60
+ }
61
+ }
62
+ }
@@ -0,0 +1,10 @@
1
+ <Widget::KpiTile
2
+ @slug="active_live_orders"
3
+ @title={{t "widget.kpi-active-orders.title"}}
4
+ @icon="bolt"
5
+ @format="count"
6
+ @period="30d"
7
+ @showDelta={{false}}
8
+ @showSparkline={{false}}
9
+ ...attributes
10
+ />
@@ -0,0 +1,5 @@
1
+ import Component from '@glimmer/component';
2
+
3
+ export default class WidgetKpiActiveOrdersComponent extends Component {
4
+ static widgetId = 'fleet-ops-kpi-active-orders-widget';
5
+ }
@@ -0,0 +1,10 @@
1
+ <Widget::KpiTile
2
+ @slug="avg_order_value"
3
+ @title={{t "widget.kpi-aov.title"}}
4
+ @icon="receipt"
5
+ @format="money"
6
+ @period="30d"
7
+ @showDelta={{true}}
8
+ @showSparkline={{true}}
9
+ ...attributes
10
+ />
@@ -0,0 +1,5 @@
1
+ import Component from '@glimmer/component';
2
+
3
+ export default class WidgetKpiAovComponent extends Component {
4
+ static widgetId = 'fleet-ops-kpi-aov-widget';
5
+ }
@@ -0,0 +1,10 @@
1
+ <Widget::KpiTile
2
+ @slug="total_distance_traveled"
3
+ @title={{t "widget.kpi-distance.title"}}
4
+ @icon="route"
5
+ @format="meters"
6
+ @period="30d"
7
+ @showDelta={{true}}
8
+ @showSparkline={{true}}
9
+ ...attributes
10
+ />
@@ -0,0 +1,5 @@
1
+ import Component from '@glimmer/component';
2
+
3
+ export default class WidgetKpiDistanceComponent extends Component {
4
+ static widgetId = 'fleet-ops-kpi-distance-widget';
5
+ }
@@ -0,0 +1,10 @@
1
+ <Widget::KpiTile
2
+ @slug="drivers_online"
3
+ @title={{t "widget.kpi-drivers-online.title"}}
4
+ @icon="id-card"
5
+ @format="count"
6
+ @period="30d"
7
+ @showDelta={{false}}
8
+ @showSparkline={{false}}
9
+ ...attributes
10
+ />
@@ -0,0 +1,5 @@
1
+ import Component from '@glimmer/component';
2
+
3
+ export default class WidgetKpiDriversOnlineComponent extends Component {
4
+ static widgetId = 'fleet-ops-kpi-drivers-online-widget';
5
+ }
@@ -0,0 +1,10 @@
1
+ <Widget::KpiTile
2
+ @slug="earnings"
3
+ @title={{t "widget.kpi-earnings.title"}}
4
+ @icon="sack-dollar"
5
+ @format="money"
6
+ @period="30d"
7
+ @showDelta={{true}}
8
+ @showSparkline={{true}}
9
+ ...attributes
10
+ />
@@ -0,0 +1,5 @@
1
+ import Component from '@glimmer/component';
2
+
3
+ export default class WidgetKpiEarningsComponent extends Component {
4
+ static widgetId = 'fleet-ops-kpi-earnings-widget';
5
+ }
@@ -0,0 +1,11 @@
1
+ <Widget::KpiTile
2
+ @slug="open_issues"
3
+ @title={{t "widget.kpi-open-issues.title"}}
4
+ @icon="triangle-exclamation"
5
+ @format="count"
6
+ @period="30d"
7
+ @showDelta={{true}}
8
+ @deltaInverse={{true}}
9
+ @showSparkline={{true}}
10
+ ...attributes
11
+ />
@@ -0,0 +1,5 @@
1
+ import Component from '@glimmer/component';
2
+
3
+ export default class WidgetKpiOpenIssuesComponent extends Component {
4
+ static widgetId = 'fleet-ops-kpi-open-issues-widget';
5
+ }
@@ -0,0 +1,59 @@
1
+ {{!--
2
+ KPI tile. Layout strategy:
3
+ - Title + icon at the top.
4
+ - Value + delta directly under the title.
5
+ - Sparkline absolutely positioned at the bottom edge of the card so every
6
+ tile lines up regardless of value-string length, and a flat-zero
7
+ sparkline (all-zero data, fill:'origin', y.min=0) sits flush along the
8
+ bottom rather than floating in the middle.
9
+ --}}
10
+ <div class="fleet-ops-widget fleet-ops-kpi-tile fleet-ops-kpi-tile-pop {{this.cardAccentClass}} relative flex flex-col h-full rounded-md border shadow-sm overflow-hidden {{@class}}" ...attributes>
11
+ <div class="flex items-center justify-between px-2.5 pt-2 relative z-10">
12
+ <span class="text-[10px] uppercase tracking-wider text-gray-500 dark:text-gray-400 font-bold truncate">
13
+ {{@title}}
14
+ </span>
15
+ {{#if @icon}}
16
+ <FaIcon @icon={{@icon}} @prefix={{or @iconPrefix "fas"}} class="kpi-tile-icon text-gray-400 dark:text-gray-500 text-[11px]" />
17
+ {{/if}}
18
+ </div>
19
+
20
+ <div class="flex-1 flex flex-col px-2.5 pt-1 min-h-0 relative z-10">
21
+ {{#if this.load.isRunning}}
22
+ <div class="flex-1 flex items-center justify-center">
23
+ <Spinner />
24
+ </div>
25
+ {{else if this.error}}
26
+ <div class="flex-1 flex items-center justify-center text-[10px] text-red-500 dark:text-red-400">
27
+ {{this.error}}
28
+ </div>
29
+ {{else if this.data}}
30
+ <div class="flex items-baseline gap-1.5 kpi-tile-value-row">
31
+ <span class="text-2xl font-bold leading-none truncate {{this.valueTextClass}}">
32
+ {{#if (eq this.format "money")}}
33
+ {{format-currency this.data.value this.currency}}
34
+ {{else if (eq this.format "meters")}}
35
+ {{format-meters this.data.value}}
36
+ {{else if (eq this.format "duration")}}
37
+ {{format-duration this.data.value}}
38
+ {{else}}
39
+ {{this.data.value}}
40
+ {{/if}}
41
+ </span>
42
+ {{#if this.hasDelta}}
43
+ <Badge @status={{this.deltaStatus}} @text={{this.deltaText}} @hideIcon={{true}} @disableHumanize={{true}} @wrapperClass="text-[9px] px-1 py-0 font-bold" />
44
+ {{/if}}
45
+ </div>
46
+ {{/if}}
47
+ </div>
48
+
49
+ {{#if this.sparklineDatasets}}
50
+ <div class="kpi-tile-sparkline absolute inset-x-0 bottom-0">
51
+ <Chart
52
+ @type="line"
53
+ @labels={{this.sparklineLabels}}
54
+ @datasets={{this.sparklineDatasets}}
55
+ @options={{this.sparklineOptions}}
56
+ />
57
+ </div>
58
+ {{/if}}
59
+ </div>