@fleetbase/fleetops-engine 0.6.47 → 0.6.49

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (223) hide show
  1. package/addon/components/admin/routing-settings.hbs +71 -0
  2. package/addon/components/admin/routing-settings.js +124 -0
  3. package/addon/components/customer/orders.js +1 -1
  4. package/addon/components/display-place.hbs +1 -1
  5. package/addon/components/driver/details.hbs +5 -1
  6. package/addon/components/driver/form.hbs +11 -1
  7. package/addon/components/fleet-listing-panel.hbs +2 -2
  8. package/addon/components/fleet-listing-panel.js +10 -0
  9. package/addon/components/map/google-live-map.js +12 -73
  10. package/addon/components/map/leaflet-live-map.hbs +149 -33
  11. package/addon/components/map/order-list-overlay/order.hbs +18 -3
  12. package/addon/components/map/toolbar.hbs +2 -2
  13. package/addon/components/map/toolbar.js +10 -2
  14. package/addon/components/orchestrator/phase-builder.hbs +49 -5
  15. package/addon/components/orchestrator/phase-builder.js +34 -1
  16. package/addon/components/orchestrator/resource-panel.hbs +19 -1
  17. package/addon/components/orchestrator/resource-panel.js +4 -0
  18. package/addon/components/orchestrator-workbench.js +4 -1
  19. package/addon/components/order/details/purchase-rate.hbs +19 -22
  20. package/addon/components/order/details/route.hbs +26 -1
  21. package/addon/components/order/details/route.js +50 -0
  22. package/addon/components/order/details/tracking.hbs +171 -27
  23. package/addon/components/order/details/tracking.js +457 -0
  24. package/addon/components/order/form.hbs +2 -2
  25. package/addon/components/order/kanban-card.hbs +18 -3
  26. package/addon/components/order-list-overlay/order.hbs +18 -3
  27. package/addon/components/order-progress-card.hbs +21 -6
  28. package/addon/components/order-tracking-lookup.hbs +7 -7
  29. package/addon/components/order-tracking-lookup.js +1 -1
  30. package/addon/components/route-list.hbs +78 -8
  31. package/addon/components/route-list.js +48 -6
  32. package/addon/components/tracking-stop-progress.hbs +45 -0
  33. package/addon/components/tracking-stop-progress.js +79 -0
  34. package/addon/components/widget/fleet-ops-key-metrics.hbs +19 -17
  35. package/addon/components/widget/fleet-ops-key-metrics.js +26 -0
  36. package/addon/components/widget/fuel-efficiency.hbs +26 -0
  37. package/addon/components/widget/fuel-efficiency.js +43 -0
  38. package/addon/components/widget/geofence-violations.hbs +44 -0
  39. package/addon/components/widget/geofence-violations.js +55 -0
  40. package/addon/components/widget/issues-insights.hbs +43 -0
  41. package/addon/components/widget/issues-insights.js +62 -0
  42. package/addon/components/widget/kpi-active-orders.hbs +10 -0
  43. package/addon/components/widget/kpi-active-orders.js +5 -0
  44. package/addon/components/widget/kpi-aov.hbs +10 -0
  45. package/addon/components/widget/kpi-aov.js +5 -0
  46. package/addon/components/widget/kpi-distance.hbs +10 -0
  47. package/addon/components/widget/kpi-distance.js +5 -0
  48. package/addon/components/widget/kpi-drivers-online.hbs +10 -0
  49. package/addon/components/widget/kpi-drivers-online.js +5 -0
  50. package/addon/components/widget/kpi-earnings.hbs +10 -0
  51. package/addon/components/widget/kpi-earnings.js +5 -0
  52. package/addon/components/widget/kpi-open-issues.hbs +11 -0
  53. package/addon/components/widget/kpi-open-issues.js +5 -0
  54. package/addon/components/widget/kpi-tile.hbs +59 -0
  55. package/addon/components/widget/kpi-tile.js +168 -0
  56. package/addon/components/widget/live-fleet.hbs +122 -0
  57. package/addon/components/widget/live-fleet.js +365 -0
  58. package/addon/components/widget/maintenance-overview.hbs +45 -0
  59. package/addon/components/widget/maintenance-overview.js +40 -0
  60. package/addon/components/widget/on-time-delivery.hbs +27 -0
  61. package/addon/components/widget/on-time-delivery.js +73 -0
  62. package/addon/components/widget/operations-pulse.hbs +51 -0
  63. package/addon/components/widget/operations-pulse.js +173 -0
  64. package/addon/components/widget/orders-by-status.hbs +26 -0
  65. package/addon/components/widget/orders-by-status.js +54 -0
  66. package/addon/components/widget/revenue-trend.hbs +34 -0
  67. package/addon/components/widget/revenue-trend.js +67 -0
  68. package/addon/components/widget/top-drivers.hbs +44 -0
  69. package/addon/components/widget/top-drivers.js +54 -0
  70. package/addon/controllers/settings/routing.js +124 -1
  71. package/addon/extension.js +172 -3
  72. package/addon/helpers/format-duration.js +38 -0
  73. package/addon/routes/application.js +6 -0
  74. package/addon/services/map-adapter/google.js +13 -2
  75. package/addon/services/order-list-overlay.js +2 -2
  76. package/addon/styles/fleetops-engine.css +806 -46
  77. package/addon/templates/maintenance/equipment/index.hbs +1 -1
  78. package/addon/templates/maintenance/parts/index.hbs +1 -1
  79. package/addon/templates/maintenance/schedules/index.hbs +1 -1
  80. package/addon/templates/management/vehicles/index.hbs +1 -1
  81. package/addon/templates/settings/avatars.hbs +1 -1
  82. package/addon/templates/settings/custom-fields.hbs +1 -1
  83. package/addon/templates/settings/map.hbs +1 -1
  84. package/addon/templates/settings/navigator-app.hbs +1 -1
  85. package/addon/templates/settings/notifications.hbs +1 -1
  86. package/addon/templates/settings/orchestrator.hbs +1 -1
  87. package/addon/templates/settings/payments/index.hbs +1 -1
  88. package/addon/templates/settings/payments/onboard.hbs +1 -1
  89. package/addon/templates/settings/routing.hbs +82 -4
  90. package/addon/templates/settings/scheduling.hbs +1 -1
  91. package/addon/utils/live-map-card-content.js +171 -0
  92. package/app/components/tracking-stop-progress.js +1 -0
  93. package/app/components/widget/fuel-efficiency.js +1 -0
  94. package/app/components/widget/geofence-violations.js +1 -0
  95. package/app/components/widget/issues-insights.js +1 -0
  96. package/app/components/widget/kpi-active-orders.js +1 -0
  97. package/app/components/widget/kpi-aov.js +1 -0
  98. package/app/components/widget/kpi-distance.js +1 -0
  99. package/app/components/widget/kpi-drivers-online.js +1 -0
  100. package/app/components/widget/kpi-earnings.js +1 -0
  101. package/app/components/widget/kpi-open-issues.js +1 -0
  102. package/app/components/widget/kpi-tile.js +1 -0
  103. package/app/components/widget/live-fleet.js +1 -0
  104. package/app/components/widget/maintenance-overview.js +1 -0
  105. package/app/components/widget/on-time-delivery.js +1 -0
  106. package/app/components/widget/operations-pulse.js +1 -0
  107. package/app/components/widget/orders-by-status.js +1 -0
  108. package/app/components/widget/revenue-trend.js +1 -0
  109. package/app/components/widget/top-drivers.js +1 -0
  110. package/app/helpers/format-duration.js +1 -0
  111. package/app/utils/live-map-card-content.js +1 -0
  112. package/composer.json +1 -1
  113. package/extension.json +1 -1
  114. package/package.json +3 -3
  115. package/pnpm-workspace.yaml +1 -0
  116. package/server/config/fleetops.php +32 -11
  117. package/server/config/telematics.php +111 -1
  118. package/server/migrations/2026_05_13_000001_repair_customer_contact_user_types.php +86 -0
  119. package/server/migrations/2026_05_18_000001_add_license_expiry_to_drivers_table.php +25 -0
  120. package/server/seeders/Testing/Concerns/SeedsTestingData.php +183 -0
  121. package/server/seeders/Testing/ConnectivitySeeder.php +212 -0
  122. package/server/seeders/Testing/FleetSeeder.php +341 -0
  123. package/server/seeders/Testing/MaintenanceSeeder.php +238 -0
  124. package/server/seeders/Testing/NetworkSeeder.php +250 -0
  125. package/server/seeders/Testing/OrdersSeeder.php +214 -0
  126. package/server/seeders/Testing/TestingSeeder.php +40 -0
  127. package/server/seeders/Testing/VroomOrchestrationSeeder.php +409 -0
  128. package/server/src/Console/Commands/ProcessOperationalAlerts.php +279 -0
  129. package/server/src/Http/Controllers/Api/v1/OrchestrationController.php +64 -0
  130. package/server/src/Http/Controllers/Api/v1/OrderController.php +26 -26
  131. package/server/src/Http/Controllers/Api/v1/PayloadController.php +10 -10
  132. package/server/src/Http/Controllers/Api/v1/ServiceQuoteController.php +1 -1
  133. package/server/src/Http/Controllers/Internal/v1/AnalyticsController.php +119 -0
  134. package/server/src/Http/Controllers/Internal/v1/ContactController.php +41 -0
  135. package/server/src/Http/Controllers/Internal/v1/LiveController.php +6 -42
  136. package/server/src/Http/Controllers/Internal/v1/MetricsController.php +82 -3
  137. package/server/src/Http/Controllers/Internal/v1/OrchestrationController.php +1 -1
  138. package/server/src/Http/Controllers/Internal/v1/OrderController.php +52 -21
  139. package/server/src/Http/Controllers/Internal/v1/ServiceQuoteController.php +1 -1
  140. package/server/src/Http/Controllers/Internal/v1/SettingController.php +164 -12
  141. package/server/src/Http/Resources/v1/Driver.php +2 -0
  142. package/server/src/Http/Resources/v1/Fleet.php +4 -0
  143. package/server/src/Http/Resources/v1/Index/Driver.php +48 -0
  144. package/server/src/Http/Resources/v1/Index/Payload.php +2 -2
  145. package/server/src/Http/Resources/v1/Index/Vehicle.php +74 -1
  146. package/server/src/Models/Contact.php +122 -13
  147. package/server/src/Models/Driver.php +2 -0
  148. package/server/src/Models/Order.php +1 -1
  149. package/server/src/Models/OrderConfig.php +14 -11
  150. package/server/src/Models/Payload.php +8 -8
  151. package/server/src/Models/Place.php +0 -3
  152. package/server/src/Models/ServiceRate.php +1 -1
  153. package/server/src/Notifications/LateDeparture.php +48 -0
  154. package/server/src/Notifications/ProlongedStoppage.php +48 -0
  155. package/server/src/Notifications/RouteDeviation.php +48 -0
  156. package/server/src/Observers/ContactObserver.php +8 -0
  157. package/server/src/Orchestration/Engines/CapacityAllocationEngine.php +220 -0
  158. package/server/src/Orchestration/Engines/VroomOrchestrationEngine.php +357 -38
  159. package/server/src/Orchestration/Support/OrchestrationPayloadBuilder.php +259 -51
  160. package/server/src/Providers/FleetOpsServiceProvider.php +32 -0
  161. package/server/src/Providers/NotificationServiceProvider.php +3 -0
  162. package/server/src/Support/Analytics/AbstractAnalytics.php +77 -0
  163. package/server/src/Support/Analytics/FuelEfficiency.php +83 -0
  164. package/server/src/Support/Analytics/GeofenceViolations.php +63 -0
  165. package/server/src/Support/Analytics/IssuesInsights.php +67 -0
  166. package/server/src/Support/Analytics/LiveFleet.php +127 -0
  167. package/server/src/Support/Analytics/MaintenanceOverview.php +70 -0
  168. package/server/src/Support/Analytics/OnTimeDelivery.php +75 -0
  169. package/server/src/Support/Analytics/OperationsPulse.php +86 -0
  170. package/server/src/Support/Analytics/OrdersByStatus.php +64 -0
  171. package/server/src/Support/Analytics/RevenueTrend.php +79 -0
  172. package/server/src/Support/Analytics/TopDrivers.php +92 -0
  173. package/server/src/Support/LiveOrderQuery.php +75 -0
  174. package/server/src/Support/Metrics/AbstractMetric.php +170 -0
  175. package/server/src/Support/Metrics/ActiveLiveOrdersMetric.php +28 -0
  176. package/server/src/Support/Metrics/AvgOrderValueMetric.php +53 -0
  177. package/server/src/Support/Metrics/DriversOnlineMetric.php +30 -0
  178. package/server/src/Support/Metrics/EarningsMetric.php +30 -0
  179. package/server/src/Support/Metrics/FuelCostsMetric.php +30 -0
  180. package/server/src/Support/Metrics/MoneyMetric.php +30 -0
  181. package/server/src/Support/Metrics/OpenIssuesMetric.php +35 -0
  182. package/server/src/Support/Metrics/OrdersCanceledMetric.php +35 -0
  183. package/server/src/Support/Metrics/OrdersCompletedMetric.php +35 -0
  184. package/server/src/Support/Metrics/OrdersInProgressMetric.php +51 -0
  185. package/server/src/Support/Metrics/OrdersScheduledMetric.php +37 -0
  186. package/server/src/Support/Metrics/Registry.php +46 -0
  187. package/server/src/Support/Metrics/ResolvedIssuesMetric.php +35 -0
  188. package/server/src/Support/Metrics/TotalCustomersMetric.php +29 -0
  189. package/server/src/Support/Metrics/TotalDistanceTraveledMetric.php +35 -0
  190. package/server/src/Support/Metrics/TotalDriversMetric.php +28 -0
  191. package/server/src/Support/Metrics/TotalTimeTraveledMetric.php +35 -0
  192. package/server/src/Support/Metrics.php +46 -234
  193. package/server/src/Support/OSRM.php +11 -6
  194. package/server/src/Support/OrderTracker.php +7 -582
  195. package/server/src/Support/Telematics/Providers/AfaqyProvider.php +290 -0
  196. package/server/src/Support/Telematics/Providers/SafeeProvider.php +333 -0
  197. package/server/src/Support/Utils.php +1 -1
  198. package/server/src/Tracking/Contracts/TrackingProviderInterface.php +19 -0
  199. package/server/src/Tracking/Providers/CalculatedTrackingProvider.php +65 -0
  200. package/server/src/Tracking/Providers/GoogleRoutesTrackingProvider.php +118 -0
  201. package/server/src/Tracking/Providers/OsrmTrackingProvider.php +71 -0
  202. package/server/src/Tracking/README.md +25 -0
  203. package/server/src/Tracking/Support/FakeTrackingProvider.php +42 -0
  204. package/server/src/Tracking/TrackingContext.php +63 -0
  205. package/server/src/Tracking/TrackingContextBuilder.php +204 -0
  206. package/server/src/Tracking/TrackingIntelligenceService.php +180 -0
  207. package/server/src/Tracking/TrackingOptions.php +49 -0
  208. package/server/src/Tracking/TrackingProviderCapabilities.php +30 -0
  209. package/server/src/Tracking/TrackingProviderManager.php +89 -0
  210. package/server/src/Tracking/TrackingProviderRegistry.php +35 -0
  211. package/server/src/Tracking/TrackingProviderResult.php +20 -0
  212. package/server/src/Tracking/TrackingStop.php +59 -0
  213. package/server/src/routes.php +27 -0
  214. package/server/tests/AnalyticsRoutesTest.php +41 -0
  215. package/server/tests/CapacityAllocationEngineTest.php +108 -0
  216. package/server/tests/LiveOrderQueryTest.php +34 -0
  217. package/server/tests/MetricsRegistryTest.php +88 -0
  218. package/server/tests/OrchestrationPayloadBuilderTest.php +132 -0
  219. package/server/tests/OrchestratorConsumableApiTest.php +74 -0
  220. package/server/tests/PingDriverEndpointTest.php +19 -0
  221. package/server/tests/TrackingIntelligenceTest.php +176 -0
  222. package/server/tests/VroomOrchestrationEngineTest.php +250 -0
  223. package/translations/en-us.yaml +41 -0
@@ -2,10 +2,29 @@
2
2
  {{#if this.firstStop}}
3
3
  <div class="order-route-stop {{@routeStopClass}}">
4
4
  <div class="order-route-stop-index {{@routeStopIndexClass}}">
5
- <div class="index-count fleetops-route-stop-badge" style={{this.firstStop.badgeStyle}}>{{this.firstStop.label}}</div>
5
+ <div
6
+ class="index-count fleetops-route-stop-badge {{if this.firstStop.active 'fleetops-route-stop-badge--active'}}"
7
+ style={{this.firstStop.badgeStyle}}
8
+ >{{this.firstStop.label}}</div>
6
9
  </div>
7
10
  <div class="order-route-location {{@routeLocationClass}} dark:text-gray-100">
8
- <DisplayPlace @place={{this.firstStop.place}} @eta={{get @eta this.firstStop.place.id}} @waypointActions={{@waypointActions}} />
11
+ <div class="mb-2 flex flex-wrap items-center gap-1.5">
12
+ {{#if this.firstStop.place.status_code}}
13
+ <Badge @status={{this.firstStop.place.status_code}} />
14
+ {{/if}}
15
+ {{#if this.firstStop.completed}}
16
+ <Badge @status="success" @hideStatusDot={{true}}>Completed</Badge>
17
+ {{else if this.firstStop.active}}
18
+ <Badge @status="indigo" @hideStatusDot={{true}}>Current Stop</Badge>
19
+ {{/if}}
20
+ {{#if this.firstStop.etaSeconds}}
21
+ <Badge @status="pending" @hideStatusDot={{true}}><span class="font-bold">ETA:</span> {{format-duration this.firstStop.etaSeconds}}</Badge>
22
+ {{/if}}
23
+ {{#if this.firstStop.etaAt}}
24
+ <Badge @status="pending" @hideStatusDot={{true}}>{{format-date-fns this.firstStop.etaAt "d MMM HH:mm"}}</Badge>
25
+ {{/if}}
26
+ </div>
27
+ <DisplayPlace @place={{this.firstStop.place}} @waypointActions={{@waypointActions}} @hideBadges={{true}} />
9
28
  </div>
10
29
  </div>
11
30
  {{/if}}
@@ -24,10 +43,26 @@
24
43
  {{#each this.middleStops as |stop|}}
25
44
  <div class="order-route-stop {{@routeStopClass}}">
26
45
  <div class="order-route-stop-index {{@routeStopIndexClass}}">
27
- <div class="index-count fleetops-route-stop-badge" style={{stop.badgeStyle}}>{{stop.label}}</div>
46
+ <div class="index-count fleetops-route-stop-badge {{if stop.active 'fleetops-route-stop-badge--active'}}" style={{stop.badgeStyle}}>{{stop.label}}</div>
28
47
  </div>
29
48
  <div class="order-route-location {{@routeLocationClass}} dark:text-gray-100">
30
- <DisplayPlace @place={{stop.place}} @eta={{get @eta stop.place.id}} @waypointActions={{@waypointActions}} />
49
+ <div class="mb-2 flex flex-wrap items-center gap-1.5">
50
+ {{#if stop.place.status_code}}
51
+ <Badge @status={{stop.place.status_code}} />
52
+ {{/if}}
53
+ {{#if stop.completed}}
54
+ <Badge @status="success" @hideStatusDot={{true}}>Completed</Badge>
55
+ {{else if stop.active}}
56
+ <Badge @status="indigo" @hideStatusDot={{true}}>Current Stop</Badge>
57
+ {{/if}}
58
+ {{#if stop.etaSeconds}}
59
+ <Badge @status="pending" @hideStatusDot={{true}}><span class="font-bold">ETA:</span> {{format-duration stop.etaSeconds}}</Badge>
60
+ {{/if}}
61
+ {{#if stop.etaAt}}
62
+ <Badge @status="pending" @hideStatusDot={{true}}>{{format-date-fns stop.etaAt "d MMM HH:mm"}}</Badge>
63
+ {{/if}}
64
+ </div>
65
+ <DisplayPlace @place={{stop.place}} @waypointActions={{@waypointActions}} @hideBadges={{true}} />
31
66
  </div>
32
67
  </div>
33
68
  {{/each}}
@@ -36,10 +71,26 @@
36
71
  {{#each this.middleStops as |stop|}}
37
72
  <div class="order-route-stop {{@routeStopClass}}">
38
73
  <div class="order-route-stop-index {{@routeStopIndexClass}}">
39
- <div class="index-count fleetops-route-stop-badge" style={{stop.badgeStyle}}>{{stop.label}}</div>
74
+ <div class="index-count fleetops-route-stop-badge {{if stop.active 'fleetops-route-stop-badge--active'}}" style={{stop.badgeStyle}}>{{stop.label}}</div>
40
75
  </div>
41
76
  <div class="order-route-location {{@routeLocationClass}} dark:text-gray-100">
42
- <DisplayPlace @place={{stop.place}} @eta={{get @eta stop.place.id}} @waypointActions={{@waypointActions}} />
77
+ <div class="mb-2 flex flex-wrap items-center gap-1.5">
78
+ {{#if stop.place.status_code}}
79
+ <Badge @status={{stop.place.status_code}} />
80
+ {{/if}}
81
+ {{#if stop.completed}}
82
+ <Badge @status="success" @hideStatusDot={{true}}>Completed</Badge>
83
+ {{else if stop.active}}
84
+ <Badge @status="indigo" @hideStatusDot={{true}}>Current Stop</Badge>
85
+ {{/if}}
86
+ {{#if stop.etaSeconds}}
87
+ <Badge @status="pending" @hideStatusDot={{true}}><span class="font-bold">ETA:</span> {{format-duration stop.etaSeconds}}</Badge>
88
+ {{/if}}
89
+ {{#if stop.etaAt}}
90
+ <Badge @status="pending" @hideStatusDot={{true}}>{{format-date-fns stop.etaAt "d MMM HH:mm"}}</Badge>
91
+ {{/if}}
92
+ </div>
93
+ <DisplayPlace @place={{stop.place}} @waypointActions={{@waypointActions}} @hideBadges={{true}} />
43
94
  </div>
44
95
  </div>
45
96
  {{/each}}
@@ -48,10 +99,29 @@
48
99
  {{#if this.lastStop}}
49
100
  <div class="order-route-stop {{@routeStopClass}}">
50
101
  <div class="order-route-stop-index {{@routeStopIndexClass}}">
51
- <div class="index-count fleetops-route-stop-badge" style={{this.lastStop.badgeStyle}}>{{this.lastStop.label}}</div>
102
+ <div
103
+ class="index-count fleetops-route-stop-badge {{if this.lastStop.active 'fleetops-route-stop-badge--active'}}"
104
+ style={{this.lastStop.badgeStyle}}
105
+ >{{this.lastStop.label}}</div>
52
106
  </div>
53
107
  <div class="order-route-location {{@routeLocationClass}} dark:text-gray-100">
54
- <DisplayPlace @place={{this.lastStop.place}} @eta={{get @eta this.lastStop.place.id}} @waypointActions={{@waypointActions}} />
108
+ <div class="mb-2 flex flex-wrap items-center gap-1.5">
109
+ {{#if this.lastStop.place.status_code}}
110
+ <Badge @status={{this.lastStop.place.status_code}} />
111
+ {{/if}}
112
+ {{#if this.lastStop.completed}}
113
+ <Badge @status="success" @hideStatusDot={{true}}>Completed</Badge>
114
+ {{else if this.lastStop.active}}
115
+ <Badge @status="indigo" @hideStatusDot={{true}}>Current Stop</Badge>
116
+ {{/if}}
117
+ {{#if this.lastStop.etaSeconds}}
118
+ <Badge @status="pending" @hideStatusDot={{true}}><span class="font-bold">ETA:</span> {{format-duration this.lastStop.etaSeconds}}</Badge>
119
+ {{/if}}
120
+ {{#if this.lastStop.etaAt}}
121
+ <Badge @status="pending" @hideStatusDot={{true}}>{{format-date-fns this.lastStop.etaAt "d MMM HH:mm"}}</Badge>
122
+ {{/if}}
123
+ </div>
124
+ <DisplayPlace @place={{this.lastStop.place}} @waypointActions={{@waypointActions}} @hideBadges={{true}} />
55
125
  </div>
56
126
  </div>
57
127
  {{/if}}
@@ -16,12 +16,22 @@ export default class RouteListComponent extends Component {
16
16
  get routeStops() {
17
17
  const routePoints = buildRoutePointsFromPayload(this.args.order?.payload);
18
18
 
19
- return routePoints.map((routePoint) => ({
20
- routePoint,
21
- place: routePoint.place,
22
- badgeStyle: this.badgeStyleForStop(routePoint),
23
- ...describeRoutePoint(routePoint, this.routeColor),
24
- }));
19
+ return routePoints
20
+ .map((routePoint) => ({
21
+ routePoint,
22
+ place: routePoint.place,
23
+ badgeStyle: this.badgeStyleForStop(routePoint),
24
+ trackingStop: this.trackingStopFor(routePoint.place),
25
+ routeLeg: this.routeLegFor(routePoint.place),
26
+ ...describeRoutePoint(routePoint, this.routeColor),
27
+ }))
28
+ .map((stop) => ({
29
+ ...stop,
30
+ etaSeconds: stop.routeLeg?.eta_seconds ?? stop.routeLeg?.duration_in_traffic_s ?? stop.routeLeg?.duration_s ?? this.legacyEtaFor(stop.place),
31
+ etaAt: stop.routeLeg?.eta_at,
32
+ completed: Boolean(stop.trackingStop?.completed),
33
+ active: this.matchesStop(stop.trackingStop, this.args.order?.tracker_data?.active_stop),
34
+ }));
25
35
  }
26
36
 
27
37
  get firstStop() {
@@ -52,6 +62,38 @@ export default class RouteListComponent extends Component {
52
62
  return `background-color: ${markerColor}; color: ${textColor};`;
53
63
  }
54
64
 
65
+ trackingStopFor(place) {
66
+ const stops = this.args.order?.tracker_data?.stops ?? [];
67
+
68
+ return stops.find((stop) => this.matchesPlace(stop, place)) ?? null;
69
+ }
70
+
71
+ routeLegFor(place) {
72
+ const legs = this.args.order?.tracker_data?.route?.legs ?? [];
73
+
74
+ return legs.find((leg) => this.matchesPlace(leg.stop, place)) ?? null;
75
+ }
76
+
77
+ legacyEtaFor(place) {
78
+ return this.args.eta?.[place?.id] ?? this.args.eta?.[place?.uuid] ?? this.args.eta?.[place?.public_id] ?? null;
79
+ }
80
+
81
+ matchesPlace(stop, place) {
82
+ if (!stop || !place) {
83
+ return false;
84
+ }
85
+
86
+ return stop.uuid === place.uuid || stop.public_id === place.public_id || stop.id === place.id;
87
+ }
88
+
89
+ matchesStop(stop, activeStop) {
90
+ if (!stop || !activeStop) {
91
+ return false;
92
+ }
93
+
94
+ return stop.uuid === activeStop.uuid || stop.public_id === activeStop.public_id;
95
+ }
96
+
55
97
  @action toggleWaypointsCollapse() {
56
98
  this.isWaypointsCollapsed = !this.isWaypointsCollapsed;
57
99
  }
@@ -0,0 +1,45 @@
1
+ <div class="tracking-stop-progress" ...attributes>
2
+ <div class="tracking-stop-progress__header">
3
+ <div class="tracking-stop-progress__title">Between Stops</div>
4
+ <div class="tracking-stop-progress__count">
5
+ <span class="font-semibold text-gray-800 dark:text-gray-100">{{this.completedCount}}</span>
6
+ /
7
+ {{this.totalCount}}
8
+ stops
9
+ </div>
10
+ </div>
11
+
12
+ <div class="tracking-stop-progress__scroller">
13
+ <div class="tracking-stop-progress__rail">
14
+ {{#each this.stops as |stop|}}
15
+ <div class="tracking-stop-progress__stop">
16
+ <div class="tracking-stop-progress__line">
17
+ {{#unless stop.isFirst}}
18
+ <div class="tracking-stop-progress__connector"></div>
19
+ {{/unless}}
20
+
21
+ <div
22
+ class="tracking-stop-progress__dot
23
+ {{if stop.completed 'tracking-stop-progress__dot--done'}}
24
+ {{if stop.active 'tracking-stop-progress__dot--active'}}
25
+ {{if stop.pending 'tracking-stop-progress__dot--pending'}}"
26
+ title={{stop.title}}
27
+ >
28
+ {{stop.label}}
29
+ </div>
30
+ <Attach::Tooltip @class="clean" @animation="scale" @placement="top">
31
+ <InputInfo class="max-w-xs">
32
+ <div class="text-xs font-semibold uppercase tracking-wide text-gray-500 dark:text-gray-400">{{stop.title}}</div>
33
+ <DisplayPlace @place={{stop.place}} @addressClass="mt-1 text-xs leading-4 text-gray-900 dark:text-gray-100 not-italic" @hideBadges={{true}} />
34
+ </InputInfo>
35
+ </Attach::Tooltip>
36
+
37
+ {{#unless stop.isLast}}
38
+ <div class="tracking-stop-progress__connector"></div>
39
+ {{/unless}}
40
+ </div>
41
+ </div>
42
+ {{/each}}
43
+ </div>
44
+ </div>
45
+ </div>
@@ -0,0 +1,79 @@
1
+ import Component from '@glimmer/component';
2
+
3
+ export default class TrackingStopProgressComponent extends Component {
4
+ get stops() {
5
+ const stops = this.args.stops ?? [];
6
+ const activeStop = this.args.activeStop;
7
+
8
+ return stops.map((stop, index) => {
9
+ const isActive = this.matches(stop, activeStop);
10
+ const completed = Boolean(stop.completed);
11
+
12
+ return {
13
+ ...stop,
14
+ index: index + 1,
15
+ isFirst: index === 0,
16
+ isLast: index === stops.length - 1,
17
+ label: this.labelFor(stop, index),
18
+ title: this.titleFor(stop, index),
19
+ locationLabel: this.locationLabelFor(stop, index),
20
+ place: this.placeFor(stop),
21
+ completed,
22
+ active: isActive,
23
+ pending: !completed && !isActive,
24
+ };
25
+ });
26
+ }
27
+
28
+ get completedCount() {
29
+ return this.stops.filter((stop) => stop.completed).length;
30
+ }
31
+
32
+ get totalCount() {
33
+ return this.stops.length;
34
+ }
35
+
36
+ labelFor(stop, index) {
37
+ if (stop.type === 'pickup') {
38
+ return 'P';
39
+ }
40
+
41
+ if (stop.type === 'dropoff') {
42
+ return 'D';
43
+ }
44
+
45
+ return String(index + 1);
46
+ }
47
+
48
+ titleFor(stop, index) {
49
+ if (stop.type === 'pickup') {
50
+ return 'Pickup';
51
+ }
52
+
53
+ if (stop.type === 'dropoff') {
54
+ return 'Dropoff';
55
+ }
56
+
57
+ return `Stop ${index + 1}`;
58
+ }
59
+
60
+ locationLabelFor(stop, index) {
61
+ return stop.city || stop.name || stop.address || this.titleFor(stop, index);
62
+ }
63
+
64
+ placeFor(stop) {
65
+ return {
66
+ ...stop,
67
+ street1: stop.street1 || stop.address || stop.name,
68
+ country_name: stop.country_name || stop.country,
69
+ };
70
+ }
71
+
72
+ matches(stop, activeStop) {
73
+ if (!stop || !activeStop) {
74
+ return false;
75
+ }
76
+
77
+ return stop.uuid === activeStop.uuid || stop.public_id === activeStop.public_id;
78
+ }
79
+ }
@@ -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>