@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
@@ -0,0 +1,45 @@
1
+ <div class="fleet-ops-widget fleet-ops-maintenance-overview flex flex-col h-full {{@class}}" ...attributes>
2
+ <div class="flex items-center justify-between px-3 py-1.5 border dark:border-gray-700 border-gray-200 dark:bg-gray-800 bg-gray-50 rounded-t-md shadow-sm">
3
+ <span class="text-xs font-bold text-black dark:text-gray-100">{{t "widget.maintenance-overview.title"}}</span>
4
+ <FaIcon @icon="wrench" class="text-gray-400" />
5
+ </div>
6
+
7
+ <div class="flex-1 flex flex-col p-2.5 border border-t-0 dark:border-gray-700 border-gray-200 rounded-b-md bg-white dark:bg-gray-900 min-h-0">
8
+ {{#if this.load.isRunning}}
9
+ <div class="flex items-center justify-center h-full"><Spinner /></div>
10
+ {{else if this.error}}
11
+ <div class="flex items-center justify-center h-full text-xs text-red-500">{{this.error}}</div>
12
+ {{else if this.data}}
13
+ <div class="grid grid-cols-3 gap-1.5 mb-2">
14
+ <div class="rounded-md border dark:border-gray-700 border-gray-200 p-1.5 text-center">
15
+ <div class="text-[9px] uppercase text-gray-500 dark:text-gray-400 leading-none">Overdue</div>
16
+ <div class="text-lg font-bold leading-tight mt-0.5 {{if this.data.overdue 'text-red-500' 'text-gray-400'}}">{{this.data.overdue}}</div>
17
+ </div>
18
+ <div class="rounded-md border dark:border-gray-700 border-gray-200 p-1.5 text-center">
19
+ <div class="text-[9px] uppercase text-gray-500 dark:text-gray-400 leading-none">Next 7d</div>
20
+ <div class="text-lg font-bold text-amber-500 leading-tight mt-0.5">{{this.data.scheduled_next_7d}}</div>
21
+ </div>
22
+ <div class="rounded-md border dark:border-gray-700 border-gray-200 p-1.5 text-center">
23
+ <div class="text-[9px] uppercase text-gray-500 dark:text-gray-400 leading-none">MTD</div>
24
+ <div class="text-sm font-bold text-black dark:text-gray-100 leading-tight mt-0.5 truncate">{{format-currency this.data.cost_this_month this.data.currency}}</div>
25
+ </div>
26
+ </div>
27
+
28
+ <div class="flex-1 min-h-0 overflow-auto divide-y dark:divide-gray-700 border-t dark:border-gray-700 pt-1">
29
+ {{#each this.data.upcoming as |item|}}
30
+ <div class="flex items-center justify-between py-1 hover:bg-gray-50 dark:hover:bg-gray-800 px-1">
31
+ <div class="min-w-0 flex-1">
32
+ <div class="text-[11px] font-medium text-black dark:text-gray-100 truncate">{{or item.type "Maintenance"}}</div>
33
+ <div class="text-[10px] text-gray-500 dark:text-gray-400">{{format-date-fns item.scheduled_at "MMM d"}}</div>
34
+ </div>
35
+ {{#if item.priority}}
36
+ <Badge @status={{this.priorityStatus item.priority}} @text={{item.priority}} @hideIcon={{true}} @wrapperClass="text-[9px] px-1 py-0 ml-2" />
37
+ {{/if}}
38
+ </div>
39
+ {{else}}
40
+ <div class="text-[10px] text-gray-500 text-center py-3">No upcoming maintenance.</div>
41
+ {{/each}}
42
+ </div>
43
+ {{/if}}
44
+ </div>
45
+ </div>
@@ -0,0 +1,40 @@
1
+ import Component from '@glimmer/component';
2
+ import { tracked } from '@glimmer/tracking';
3
+ import { inject as service } from '@ember/service';
4
+ import { task } from 'ember-concurrency';
5
+
6
+ export default class WidgetMaintenanceOverviewComponent extends Component {
7
+ static widgetId = 'fleet-ops-maintenance-overview-widget';
8
+
9
+ @service fetch;
10
+
11
+ @tracked data = null;
12
+ @tracked error = null;
13
+
14
+ constructor() {
15
+ super(...arguments);
16
+ this.load.perform();
17
+ }
18
+
19
+ priorityStatus(priority) {
20
+ switch (priority) {
21
+ case 'high':
22
+ case 'urgent':
23
+ return 'danger';
24
+ case 'medium':
25
+ case 'normal':
26
+ return 'warning';
27
+ default:
28
+ return 'info';
29
+ }
30
+ }
31
+
32
+ @task *load() {
33
+ try {
34
+ this.data = yield this.fetch.get('fleet-ops/analytics/maintenance-overview');
35
+ this.error = null;
36
+ } catch (e) {
37
+ this.error = e?.message ?? 'Failed to load maintenance overview';
38
+ }
39
+ }
40
+ }
@@ -0,0 +1,27 @@
1
+ <div class="fleet-ops-widget fleet-ops-on-time-delivery flex flex-col h-full rounded-md border dark:border-gray-700 border-gray-200 dark:bg-gray-800 bg-white shadow-sm {{@class}}" ...attributes>
2
+ <div class="flex items-center justify-between px-2.5 pt-2">
3
+ <span class="text-[10px] uppercase tracking-wide text-gray-500 dark:text-gray-400 font-semibold">
4
+ {{t "widget.on-time-delivery.title"}}
5
+ </span>
6
+ <FaIcon @icon="clock" class="text-gray-400 text-[11px]" />
7
+ </div>
8
+
9
+ <div class="flex-1 flex flex-col items-center justify-center px-2.5 pb-2 min-h-0">
10
+ {{#if this.load.isRunning}}
11
+ <Spinner />
12
+ {{else if this.error}}
13
+ <div class="text-[10px] text-red-500">{{this.error}}</div>
14
+ {{else if this.data}}
15
+ <div class="relative w-full max-w-24 aspect-square">
16
+ <Chart @type="doughnut" @labels={{this.chartLabels}} @datasets={{this.chartDatasets}} @options={{this.chartOptions}} />
17
+ <div class="absolute inset-0 flex flex-col items-center justify-center pointer-events-none">
18
+ <span class="text-base font-bold text-black dark:text-gray-100 leading-none">{{this.data.on_time_pct}}%</span>
19
+ <span class="text-[9px] text-gray-500 dark:text-gray-400 mt-0.5">{{this.data.on_time}}/{{this.data.total}}</span>
20
+ </div>
21
+ </div>
22
+ {{#if this.deltaStatus}}
23
+ <Badge @status={{this.deltaStatus}} @text={{this.deltaText}} @hideIcon={{true}} @disableHumanize={{true}} @wrapperClass="mt-1 text-[9px] px-1 py-0" />
24
+ {{/if}}
25
+ {{/if}}
26
+ </div>
27
+ </div>
@@ -0,0 +1,73 @@
1
+ import Component from '@glimmer/component';
2
+ import { tracked } from '@glimmer/tracking';
3
+ import { inject as service } from '@ember/service';
4
+ import { task } from 'ember-concurrency';
5
+
6
+ export default class WidgetOnTimeDeliveryComponent extends Component {
7
+ static widgetId = 'fleet-ops-on-time-delivery-widget';
8
+
9
+ @service fetch;
10
+
11
+ @tracked data = null;
12
+ @tracked error = null;
13
+
14
+ constructor() {
15
+ super(...arguments);
16
+ this.load.perform();
17
+ }
18
+
19
+ get gaugeColor() {
20
+ const pct = this.data?.on_time_pct ?? 0;
21
+ if (pct >= 95) return '#22c55e';
22
+ if (pct >= 85) return '#84cc16';
23
+ if (pct >= 70) return '#f59e0b';
24
+ return '#ef4444';
25
+ }
26
+
27
+ get chartLabels() {
28
+ return ['On Time', 'Late'];
29
+ }
30
+
31
+ get chartDatasets() {
32
+ return [
33
+ {
34
+ data: [this.data?.on_time ?? 0, this.data?.late ?? 0],
35
+ backgroundColor: [this.gaugeColor, 'rgba(148,163,184,0.25)'],
36
+ borderWidth: 0,
37
+ },
38
+ ];
39
+ }
40
+
41
+ get chartOptions() {
42
+ return {
43
+ responsive: true,
44
+ maintainAspectRatio: false,
45
+ cutout: '75%',
46
+ plugins: {
47
+ legend: { display: false },
48
+ tooltip: { enabled: true },
49
+ },
50
+ };
51
+ }
52
+
53
+ get deltaStatus() {
54
+ const d = this.data?.delta_pct;
55
+ if (typeof d !== 'number' || d === 0) return null;
56
+ return d > 0 ? 'success' : 'danger';
57
+ }
58
+
59
+ get deltaText() {
60
+ const d = this.data?.delta_pct;
61
+ if (typeof d !== 'number') return null;
62
+ return `${d > 0 ? '+' : ''}${d} pp`;
63
+ }
64
+
65
+ @task *load() {
66
+ try {
67
+ this.data = yield this.fetch.get('fleet-ops/analytics/on-time-delivery', { period: '30d' });
68
+ this.error = null;
69
+ } catch (e) {
70
+ this.error = e?.message ?? 'Failed to load on-time delivery';
71
+ }
72
+ }
73
+ }
@@ -0,0 +1,51 @@
1
+ <div class="fleet-ops-widget fleet-ops-operations-pulse flex flex-col h-full {{@class}}" ...attributes>
2
+ <div class="flex items-center justify-between px-3 py-1.5 border dark:border-gray-700 border-gray-200 dark:bg-gray-800 bg-gray-50 rounded-t-md shadow-sm">
3
+ <span class="text-xs font-bold text-black dark:text-gray-100">
4
+ {{t "widget.operations-pulse.title"}}
5
+ </span>
6
+ <button type="button" class="text-gray-400 hover:text-gray-600 dark:hover:text-gray-200 text-[10px]" {{on "click" this.refresh}} title={{t "widget.refresh"}}>
7
+ <FaIcon @icon="rotate" class={{if this.load.isRunning "animate-spin"}} />
8
+ </button>
9
+ </div>
10
+
11
+ <div class="flex-1 p-2.5 border border-t-0 dark:border-gray-700 border-gray-200 rounded-b-md bg-white dark:bg-gray-900">
12
+ {{#if this.load.isRunning}}
13
+ <div class="flex items-center justify-center h-full">
14
+ <Spinner />
15
+ </div>
16
+ {{else if this.error}}
17
+ <div class="flex items-center justify-center h-full text-xs text-red-500">{{this.error}}</div>
18
+ {{else}}
19
+ <div class="grid grid-cols-2 lg:grid-cols-5 gap-2 h-full">
20
+ {{#each this.tiles as |tile|}}
21
+ <div class="fleet-ops-pulse-tile {{tile.meta.accent}} relative rounded-md border p-2 flex flex-col justify-between min-h-0 overflow-hidden">
22
+ {{!-- Accent stripe along the left edge for instant status read --}}
23
+ <span class="fleet-ops-pulse-stripe absolute inset-y-0 left-0"></span>
24
+
25
+ <div class="flex items-center justify-between relative z-10">
26
+ <span class="text-[9px] uppercase tracking-wider text-gray-600 dark:text-gray-300 font-bold truncate">{{tile.title}}</span>
27
+ {{#if tile.meta.icon}}
28
+ <div class="fleet-ops-pulse-icon flex-shrink-0">
29
+ <FaIcon @icon={{tile.meta.icon}} class="text-[10px]" />
30
+ </div>
31
+ {{/if}}
32
+ </div>
33
+
34
+ <div class="flex items-baseline gap-1 mt-1 relative z-10 fleet-ops-pulse-value-row">
35
+ <span class="fleet-ops-pulse-value text-2xl font-bold leading-none tabular-nums">{{tile.value}}</span>
36
+ {{#if tile.of}}
37
+ <span class="text-[10px] text-gray-500 dark:text-gray-400">/ {{tile.of}}</span>
38
+ {{/if}}
39
+ </div>
40
+
41
+ {{#let (this.deltaStatus tile) as |status|}}
42
+ {{#if status}}
43
+ <Badge @status={{status}} @text={{this.deltaText tile}} @hideIcon={{true}} @disableHumanize={{true}} @wrapperClass="text-[9px] py-0 px-1 leading-tight mt-1 self-start font-bold relative z-10" />
44
+ {{/if}}
45
+ {{/let}}
46
+ </div>
47
+ {{/each}}
48
+ </div>
49
+ {{/if}}
50
+ </div>
51
+ </div>
@@ -0,0 +1,173 @@
1
+ import Component from '@glimmer/component';
2
+ import { tracked } from '@glimmer/tracking';
3
+ import { action } from '@ember/object';
4
+ import { inject as service } from '@ember/service';
5
+ import { debug } from '@ember/debug';
6
+ import { task } from 'ember-concurrency';
7
+
8
+ const TILE_ORDER = ['active_orders', 'completed_today', 'drivers_online', 'vehicles_deployed', 'issues_open'];
9
+
10
+ /**
11
+ * Each tile gets:
12
+ * - icon: FA icon name + colored chip
13
+ * - accent: CSS class on the tile root for the gradient + accent stripe palette
14
+ * - deltaInverse: flip success/danger semantics (for "lower-is-better" metrics)
15
+ */
16
+ const TILE_META = {
17
+ active_orders: { icon: 'bolt', accent: 'pulse-accent-blue' },
18
+ completed_today: { icon: 'circle-check', accent: 'pulse-accent-green' },
19
+ drivers_online: { icon: 'id-card', accent: 'pulse-accent-sky' },
20
+ vehicles_deployed: { icon: 'truck', accent: 'pulse-accent-indigo' },
21
+ issues_open: { icon: 'triangle-exclamation', accent: 'pulse-accent-amber', deltaInverse: true },
22
+ };
23
+
24
+ /** Refetch the full snapshot this often as a reconciliation guard against
25
+ * drift between incrementally-applied socket deltas and authoritative state. */
26
+ const RECONCILE_INTERVAL_MS = 60_000;
27
+
28
+ /**
29
+ * Live operational snapshot. Initial GET seeds the tiles, then subscribes to
30
+ * `company.{uuid}` and increments/decrements counters on order.* events
31
+ * without polling. Reconciles via a full refetch every 60s.
32
+ */
33
+ export default class WidgetOperationsPulseComponent extends Component {
34
+ static widgetId = 'fleet-ops-operations-pulse-widget';
35
+
36
+ @service fetch;
37
+ @service socket;
38
+ @service currentUser;
39
+
40
+ @tracked data = null;
41
+ @tracked error = null;
42
+
43
+ socketChannel = null;
44
+ socketActive = false;
45
+ reconcileTimer = null;
46
+
47
+ constructor() {
48
+ super(...arguments);
49
+ this.load.perform();
50
+ this.subscribe();
51
+ this.reconcileTimer = setInterval(() => this.load.perform(), RECONCILE_INTERVAL_MS);
52
+ }
53
+
54
+ willDestroy() {
55
+ super.willDestroy(...arguments);
56
+ this.unsubscribe();
57
+ if (this.reconcileTimer) clearInterval(this.reconcileTimer);
58
+ }
59
+
60
+ get tiles() {
61
+ if (!this.data) return [];
62
+ return TILE_ORDER.filter((key) => this.data[key]).map((key) => ({
63
+ key,
64
+ title: this.titleFor(key),
65
+ ...this.data[key],
66
+ meta: TILE_META[key] ?? {},
67
+ }));
68
+ }
69
+
70
+ titleFor(key) {
71
+ return key
72
+ .split('_')
73
+ .map((p) => p.charAt(0).toUpperCase() + p.slice(1))
74
+ .join(' ');
75
+ }
76
+
77
+ deltaStatus(tile) {
78
+ if (typeof tile.delta_pct !== 'number' || tile.delta_pct === 0) return null;
79
+ const positive = tile.delta_pct > 0;
80
+ const isGood = tile.meta?.deltaInverse ? !positive : positive;
81
+ return isGood ? 'success' : 'danger';
82
+ }
83
+
84
+ deltaText(tile) {
85
+ if (typeof tile.delta_pct !== 'number') return null;
86
+ const sign = tile.delta_pct > 0 ? '+' : '';
87
+ return `${sign}${tile.delta_pct}%`;
88
+ }
89
+
90
+ @task *load() {
91
+ try {
92
+ this.data = yield this.fetch.get('fleet-ops/analytics/operations-pulse');
93
+ this.error = null;
94
+ } catch (e) {
95
+ this.error = e?.message ?? 'Failed to load operations pulse';
96
+ }
97
+ }
98
+
99
+ @action
100
+ refresh() {
101
+ this.load.perform();
102
+ }
103
+
104
+ async subscribe() {
105
+ const companyId = this.currentUser?.companyId;
106
+ if (!companyId) return;
107
+
108
+ try {
109
+ const sc = this.socket.instance();
110
+ const channel = sc.subscribe(`company.${companyId}`);
111
+ this.socketChannel = channel;
112
+ this.socketActive = true;
113
+
114
+ if (channel.state !== 'subscribed') {
115
+ await channel.subscribe();
116
+ }
117
+
118
+ for await (const msg of channel) {
119
+ if (!this.socketActive) break;
120
+ this.applyEvent(msg);
121
+ }
122
+ } catch (e) {
123
+ debug(`[operations-pulse] socket error: ${e?.message ?? e}`);
124
+ }
125
+ }
126
+
127
+ async unsubscribe() {
128
+ this.socketActive = false;
129
+ try {
130
+ await this.socketChannel?.unsubscribe();
131
+ } catch (e) {
132
+ debug(`[operations-pulse] unsubscribe error: ${e?.message ?? e}`);
133
+ }
134
+ this.socketChannel = null;
135
+ }
136
+
137
+ applyEvent(msg) {
138
+ if (!this.data || !msg) return;
139
+ const event = msg.event;
140
+ const next = { ...this.data };
141
+
142
+ const adjust = (key, delta) => {
143
+ if (!next[key]) return;
144
+ next[key] = { ...next[key], value: Math.max(0, (next[key].value ?? 0) + delta) };
145
+ };
146
+
147
+ switch (event) {
148
+ case 'order.created':
149
+ case 'order.dispatched':
150
+ case 'order.driver_assigned':
151
+ adjust('active_orders', +1);
152
+ break;
153
+ case 'order.completed':
154
+ adjust('active_orders', -1);
155
+ adjust('completed_today', +1);
156
+ break;
157
+ case 'order.canceled':
158
+ case 'order.failed':
159
+ adjust('active_orders', -1);
160
+ break;
161
+ case 'issue.created':
162
+ adjust('issues_open', +1);
163
+ break;
164
+ case 'issue.resolved':
165
+ adjust('issues_open', -1);
166
+ break;
167
+ default:
168
+ return;
169
+ }
170
+
171
+ this.data = next;
172
+ }
173
+ }
@@ -0,0 +1,26 @@
1
+ <div class="fleet-ops-widget fleet-ops-orders-by-status 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.orders-by-status.title"}}</span>
4
+ <div class="flex gap-0.5">
5
+ {{#each this.periods as |p|}}
6
+ <button type="button"
7
+ class="text-[10px] px-1.5 py-0.5 rounded {{if (eq this.period p) 'bg-sky-600 text-white' 'text-gray-500 hover:text-gray-700 dark:hover:text-gray-200'}}"
8
+ {{on "click" (fn this.setPeriod p)}}>
9
+ {{p}}
10
+ </button>
11
+ {{/each}}
12
+ </div>
13
+ </div>
14
+
15
+ <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">
16
+ {{#if this.load.isRunning}}
17
+ <div class="flex items-center justify-center h-full"><Spinner /></div>
18
+ {{else if this.error}}
19
+ <div class="flex items-center justify-center h-full text-xs text-red-500">{{this.error}}</div>
20
+ {{else if this.data}}
21
+ <div class="h-full">
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,54 @@
1
+ import Component from '@glimmer/component';
2
+ import { tracked } from '@glimmer/tracking';
3
+ import { action } from '@ember/object';
4
+ import { inject as service } from '@ember/service';
5
+ import { task } from 'ember-concurrency';
6
+
7
+ const PERIODS = ['7d', '14d', '30d'];
8
+
9
+ export default class WidgetOrdersByStatusComponent extends Component {
10
+ static widgetId = 'fleet-ops-orders-by-status-widget';
11
+
12
+ @service fetch;
13
+
14
+ @tracked period = '14d';
15
+ @tracked data = null;
16
+ @tracked error = null;
17
+
18
+ periods = PERIODS;
19
+
20
+ constructor() {
21
+ super(...arguments);
22
+ this.load.perform();
23
+ }
24
+
25
+ get chartOptions() {
26
+ return {
27
+ responsive: true,
28
+ maintainAspectRatio: false,
29
+ plugins: {
30
+ legend: { position: 'bottom', labels: { boxWidth: 12, padding: 8 } },
31
+ tooltip: { mode: 'index', intersect: false },
32
+ },
33
+ scales: {
34
+ x: { stacked: true, grid: { display: false } },
35
+ y: { stacked: true, beginAtZero: true, ticks: { precision: 0 } },
36
+ },
37
+ };
38
+ }
39
+
40
+ @task *load() {
41
+ try {
42
+ this.data = yield this.fetch.get('fleet-ops/analytics/orders-by-status', { period: this.period });
43
+ this.error = null;
44
+ } catch (e) {
45
+ this.error = e?.message ?? 'Failed to load orders breakdown';
46
+ }
47
+ }
48
+
49
+ @action
50
+ setPeriod(period) {
51
+ this.period = period;
52
+ this.load.perform();
53
+ }
54
+ }
@@ -0,0 +1,34 @@
1
+ <div class="fleet-ops-widget fleet-ops-revenue-trend 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.revenue-trend.title"}}</span>
4
+ <div class="flex gap-0.5">
5
+ {{#each this.periods as |p|}}
6
+ <button type="button"
7
+ class="text-[10px] px-1.5 py-0.5 rounded {{if (eq this.period p) 'bg-sky-600 text-white' 'text-gray-500 hover:text-gray-700 dark:hover:text-gray-200'}}"
8
+ {{on "click" (fn this.setPeriod p)}}>
9
+ {{p}}
10
+ </button>
11
+ {{/each}}
12
+ </div>
13
+ </div>
14
+
15
+ <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">
16
+ {{#if this.load.isRunning}}
17
+ <div class="flex items-center justify-center h-full"><Spinner /></div>
18
+ {{else if this.error}}
19
+ <div class="flex items-center justify-center h-full text-xs text-red-500">{{this.error}}</div>
20
+ {{else if this.data}}
21
+ <div class="flex items-baseline gap-2 mb-1.5">
22
+ <span class="text-base font-bold text-black dark:text-gray-100 leading-none">
23
+ {{format-currency this.data.summary.total this.data.summary.currency}}
24
+ </span>
25
+ {{#if this.deltaStatus}}
26
+ <Badge @status={{this.deltaStatus}} @text={{this.deltaText}} @hideIcon={{true}} @disableHumanize={{true}} @wrapperClass="text-[9px] px-1 py-0" />
27
+ {{/if}}
28
+ </div>
29
+ <div class="flex-1 min-h-0">
30
+ <Chart @type="line" @labels={{this.data.labels}} @datasets={{this.data.datasets}} @options={{this.chartOptions}} />
31
+ </div>
32
+ {{/if}}
33
+ </div>
34
+ </div>
@@ -0,0 +1,67 @@
1
+ import Component from '@glimmer/component';
2
+ import { tracked } from '@glimmer/tracking';
3
+ import { action } from '@ember/object';
4
+ import { inject as service } from '@ember/service';
5
+ import { task } from 'ember-concurrency';
6
+
7
+ const PERIODS = ['7d', '30d', '90d'];
8
+
9
+ export default class WidgetRevenueTrendComponent extends Component {
10
+ static widgetId = 'fleet-ops-revenue-trend-widget';
11
+
12
+ @service fetch;
13
+
14
+ @tracked period = '30d';
15
+ @tracked data = null;
16
+ @tracked error = null;
17
+
18
+ periods = PERIODS;
19
+
20
+ constructor() {
21
+ super(...arguments);
22
+ this.load.perform();
23
+ }
24
+
25
+ get chartOptions() {
26
+ return {
27
+ responsive: true,
28
+ maintainAspectRatio: false,
29
+ plugins: {
30
+ legend: { display: false },
31
+ tooltip: { mode: 'index', intersect: false },
32
+ },
33
+ scales: {
34
+ x: { grid: { display: false } },
35
+ y: { beginAtZero: true, ticks: { precision: 0 } },
36
+ },
37
+ elements: { point: { radius: 0, hoverRadius: 4 } },
38
+ };
39
+ }
40
+
41
+ get deltaStatus() {
42
+ const pct = this.data?.summary?.delta_pct;
43
+ if (typeof pct !== 'number' || pct === 0) return null;
44
+ return pct > 0 ? 'success' : 'danger';
45
+ }
46
+
47
+ get deltaText() {
48
+ const pct = this.data?.summary?.delta_pct;
49
+ if (typeof pct !== 'number') return null;
50
+ return `${pct > 0 ? '+' : ''}${pct}%`;
51
+ }
52
+
53
+ @task *load() {
54
+ try {
55
+ this.data = yield this.fetch.get('fleet-ops/analytics/revenue-trend', { period: this.period });
56
+ this.error = null;
57
+ } catch (e) {
58
+ this.error = e?.message ?? 'Failed to load revenue trend';
59
+ }
60
+ }
61
+
62
+ @action
63
+ setPeriod(period) {
64
+ this.period = period;
65
+ this.load.perform();
66
+ }
67
+ }
@@ -0,0 +1,44 @@
1
+ <div class="fleet-ops-widget fleet-ops-top-drivers 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.top-drivers.title"}}</span>
4
+ <div class="flex gap-0.5">
5
+ {{#each this.sorts as |s|}}
6
+ <button type="button"
7
+ class="text-[10px] px-1.5 py-0.5 rounded {{if (eq this.sortBy s.key) 'bg-sky-600 text-white' 'text-gray-500 hover:text-gray-700 dark:hover:text-gray-200'}}"
8
+ {{on "click" (fn this.setSort s.key)}}>
9
+ {{s.label}}
10
+ </button>
11
+ {{/each}}
12
+ </div>
13
+ </div>
14
+
15
+ <div class="flex-1 border border-t-0 dark:border-gray-700 border-gray-200 rounded-b-md bg-white dark:bg-gray-900 overflow-auto">
16
+ {{#if this.load.isRunning}}
17
+ <div class="flex items-center justify-center h-full"><Spinner /></div>
18
+ {{else if this.error}}
19
+ <div class="flex items-center justify-center h-full text-xs text-red-500">{{this.error}}</div>
20
+ {{else if this.data.rows.length}}
21
+ <div class="divide-y dark:divide-gray-700">
22
+ {{#each this.data.rows as |row index|}}
23
+ <div class="flex items-center gap-2 px-2 py-1.5 hover:bg-gray-50 dark:hover:bg-gray-800">
24
+ <span class="text-[11px] font-bold text-gray-400 w-3 text-right">{{add index 1}}</span>
25
+ <div class="w-5 h-5 rounded-full bg-gray-200 dark:bg-gray-700 flex items-center justify-center text-[9px] font-semibold text-gray-600 dark:text-gray-300 flex-shrink-0">
26
+ {{first-char row.name}}
27
+ </div>
28
+ <div class="flex-1 min-w-0">
29
+ <div class="text-[11px] font-medium text-black dark:text-gray-100 truncate leading-tight">{{row.name}}</div>
30
+ <div class="text-[10px] text-gray-500 dark:text-gray-400 truncate leading-tight">
31
+ {{row.orders_completed}} orders &middot; {{format-meters row.distance_m}}
32
+ </div>
33
+ </div>
34
+ {{#if row.on_time_pct}}
35
+ <Badge @status={{this.onTimeStatus row.on_time_pct}} @text={{concat row.on_time_pct "%"}} @hideIcon={{true}} @disableHumanize={{true}} @wrapperClass="text-[9px] px-1 py-0" />
36
+ {{/if}}
37
+ </div>
38
+ {{/each}}
39
+ </div>
40
+ {{else}}
41
+ <div class="flex items-center justify-center h-full text-[10px] text-gray-500">No driver activity in this period.</div>
42
+ {{/if}}
43
+ </div>
44
+ </div>