@fleetbase/fleetops-engine 0.6.52 → 0.6.54

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (151) hide show
  1. package/addon/components/device/details.hbs +106 -135
  2. package/addon/components/device/details.js +174 -1
  3. package/addon/components/device/panel-header.hbs +2 -2
  4. package/addon/components/layout/fleet-ops-sidebar/operations-monitor.hbs +201 -0
  5. package/addon/components/layout/fleet-ops-sidebar/operations-monitor.js +654 -0
  6. package/addon/components/layout/fleet-ops-sidebar.hbs +19 -233
  7. package/addon/components/layout/fleet-ops-sidebar.js +364 -395
  8. package/addon/components/maintenance-schedule/form.js +1 -0
  9. package/addon/components/order/form/details.hbs +1 -43
  10. package/addon/components/order/form/details.js +18 -53
  11. package/addon/components/order/form/orchestrator-constraints.hbs +40 -0
  12. package/addon/components/order/form/orchestrator-constraints.js +61 -0
  13. package/addon/components/order/form/payload.js +50 -2
  14. package/addon/components/order/form/route.js +15 -0
  15. package/addon/components/order/form/service-rate.hbs +62 -11
  16. package/addon/components/order/form/service-rate.js +124 -2
  17. package/addon/components/order/form.hbs +2 -0
  18. package/addon/components/telematic/details.hbs +7 -1
  19. package/addon/components/telematic/form.js +16 -8
  20. package/addon/components/work-order/details.hbs +5 -1
  21. package/addon/components/work-order/form.hbs +26 -4
  22. package/addon/components/work-order/form.js +5 -1
  23. package/addon/controllers/connectivity/devices/index/details/events.js +244 -1
  24. package/addon/controllers/connectivity/devices/index/details/sensors.js +199 -0
  25. package/addon/controllers/connectivity/devices/index/details/vehicle.js +74 -0
  26. package/addon/controllers/connectivity/devices/index/details.js +12 -0
  27. package/addon/controllers/connectivity/devices/index.js +4 -1
  28. package/addon/controllers/connectivity/telematics/details/attachments.js +220 -54
  29. package/addon/controllers/connectivity/telematics/details/devices.js +90 -41
  30. package/addon/controllers/connectivity/telematics/details/events.js +39 -11
  31. package/addon/controllers/connectivity/telematics/details/sensors.js +39 -19
  32. package/addon/controllers/maintenance/index.js +100 -0
  33. package/addon/controllers/maintenance/work-orders/index.js +18 -1
  34. package/addon/controllers/management/fuel-transactions/index.js +1 -1
  35. package/addon/controllers/management/index.js +88 -0
  36. package/addon/controllers/settings/index.js +142 -0
  37. package/addon/extension.js +21 -0
  38. package/addon/routes/analytics/index.js +3 -0
  39. package/addon/routes/connectivity/devices/index/details/events.js +37 -1
  40. package/addon/routes/connectivity/devices/index/details/sensors.js +35 -0
  41. package/addon/routes/connectivity/devices/index/details/vehicle.js +7 -0
  42. package/addon/routes/connectivity/devices/index.js +3 -0
  43. package/addon/routes/connectivity/telematics/details/attachments.js +72 -9
  44. package/addon/routes/connectivity/telematics/details/devices.js +4 -0
  45. package/addon/routes/connectivity/telematics/details/sensors.js +1 -0
  46. package/addon/routes/connectivity/telematics/details.js +1 -1
  47. package/addon/routes/connectivity/telematics/edit.js +1 -1
  48. package/addon/routes/maintenance/index.js +3 -0
  49. package/addon/routes/maintenance/work-orders/index.js +3 -0
  50. package/addon/routes/settings/index.js +3 -0
  51. package/addon/routes.js +2 -0
  52. package/addon/services/order-creation.js +54 -1
  53. package/addon/services/order-validation.js +3 -2
  54. package/addon/services/service-rate-actions.js +1 -0
  55. package/addon/styles/fleetops-engine.css +1821 -6
  56. package/addon/templates/analytics/index.hbs +14 -0
  57. package/addon/templates/analytics/reports/index.hbs +4 -4
  58. package/addon/templates/connectivity/devices/index/details/events.hbs +85 -1
  59. package/addon/templates/connectivity/devices/index/details/sensors.hbs +73 -0
  60. package/addon/templates/connectivity/devices/index/details/vehicle.hbs +84 -0
  61. package/addon/templates/connectivity/telematics/details/attachments.hbs +198 -121
  62. package/addon/templates/connectivity/telematics/details/devices.hbs +4 -1
  63. package/addon/templates/connectivity/telematics/details/events.hbs +4 -1
  64. package/addon/templates/connectivity/telematics/details/sensors.hbs +3 -0
  65. package/addon/templates/maintenance/index.hbs +113 -0
  66. package/addon/templates/management/fuel-transactions/index/details.hbs +1 -1
  67. package/addon/templates/management/index.hbs +109 -73
  68. package/addon/templates/management/vehicles/index/edit.hbs +1 -1
  69. package/addon/templates/operations/orders/index.hbs +1 -1
  70. package/addon/templates/operations/service-rates/index.hbs +1 -1
  71. package/addon/templates/settings/index.hbs +83 -0
  72. package/addon/templates/settings/routing.hbs +2 -2
  73. package/addon/utils/fleet-ops-options.js +38 -0
  74. package/app/components/order/form/orchestrator-constraints.js +1 -0
  75. package/app/controllers/connectivity/devices/index/details/sensors.js +1 -0
  76. package/app/controllers/connectivity/devices/index/details/vehicle.js +1 -0
  77. package/app/controllers/maintenance/index.js +1 -0
  78. package/app/controllers/management/index.js +1 -0
  79. package/app/controllers/settings/index.js +1 -0
  80. package/app/routes/analytics/index.js +1 -0
  81. package/app/routes/connectivity/devices/index/details/sensors.js +1 -0
  82. package/app/routes/connectivity/devices/index/details/vehicle.js +1 -0
  83. package/app/routes/maintenance/index.js +1 -0
  84. package/app/routes/settings/index.js +1 -0
  85. package/app/templates/analytics/index.js +1 -0
  86. package/app/templates/connectivity/devices/index/details/sensors.js +1 -0
  87. package/app/templates/connectivity/devices/index/details/vehicle.js +1 -0
  88. package/app/templates/maintenance/index.js +1 -0
  89. package/app/templates/management/index.js +1 -0
  90. package/app/templates/settings/index.js +1 -0
  91. package/composer.json +1 -1
  92. package/extension.json +1 -1
  93. package/package.json +3 -3
  94. package/server/migrations/2025_08_28_054930_create_work_orders_table.php +1 -0
  95. package/server/migrations/2026_06_15_000001_normalize_device_attachable_vehicle_morph_types.php +30 -0
  96. package/server/migrations/2026_06_18_000001_add_category_to_work_orders_table.php +32 -0
  97. package/server/seeders/Testing/MaintenanceSeeder.php +5 -4
  98. package/server/src/Console/Commands/FixInvalidPolymorphicRelationTypeNamespaces.php +1 -0
  99. package/server/src/Console/Commands/ProcessMaintenanceTriggers.php +1 -0
  100. package/server/src/Console/Commands/SyncTelematics.php +89 -0
  101. package/server/src/Exceptions/TelematicProviderException.php +13 -2
  102. package/server/src/Http/Controllers/Api/v1/OrderController.php +6 -21
  103. package/server/src/Http/Controllers/Internal/v1/DeviceController.php +112 -11
  104. package/server/src/Http/Controllers/Internal/v1/DeviceEventController.php +16 -5
  105. package/server/src/Http/Controllers/Internal/v1/FleetController.php +9 -0
  106. package/server/src/Http/Controllers/Internal/v1/HubController.php +300 -0
  107. package/server/src/Http/Controllers/Internal/v1/LiveController.php +177 -0
  108. package/server/src/Http/Controllers/Internal/v1/MaintenanceScheduleController.php +1 -0
  109. package/server/src/Http/Controllers/Internal/v1/OrderController.php +4 -32
  110. package/server/src/Http/Controllers/Internal/v1/SearchController.php +506 -0
  111. package/server/src/Http/Controllers/Internal/v1/ServiceQuoteController.php +4 -4
  112. package/server/src/Http/Controllers/Internal/v1/TelematicController.php +1 -1
  113. package/server/src/Http/Controllers/Internal/v1/VehicleController.php +94 -10
  114. package/server/src/Http/Filter/DeviceEventFilter.php +44 -0
  115. package/server/src/Http/Filter/DeviceFilter.php +131 -0
  116. package/server/src/Http/Filter/DriverFilter.php +6 -0
  117. package/server/src/Http/Filter/SensorFilter.php +125 -0
  118. package/server/src/Http/Filter/VehicleFilter.php +19 -0
  119. package/server/src/Http/Requests/CreateDriverRequest.php +14 -13
  120. package/server/src/Http/Requests/Internal/CreateDriverRequest.php +1 -0
  121. package/server/src/Http/Resources/v1/Driver.php +11 -2
  122. package/server/src/Http/Resources/v1/WorkOrder.php +1 -0
  123. package/server/src/Jobs/FinalizeApiOrderCreation.php +47 -0
  124. package/server/src/Jobs/FinalizeInternalOrderCreation.php +35 -0
  125. package/server/src/Jobs/NotifyBulkAssignedDriver.php +47 -0
  126. package/server/src/Jobs/SyncTelematicDevicesJob.php +181 -66
  127. package/server/src/Models/Device.php +15 -1
  128. package/server/src/Models/DeviceEvent.php +1 -1
  129. package/server/src/Models/Driver.php +17 -1
  130. package/server/src/Models/Sensor.php +15 -2
  131. package/server/src/Models/Telematic.php +1 -1
  132. package/server/src/Models/Vehicle.php +5 -1
  133. package/server/src/Models/WorkOrder.php +5 -2
  134. package/server/src/Observers/DriverObserver.php +3 -3
  135. package/server/src/Observers/FleetObserver.php +23 -0
  136. package/server/src/Observers/VehicleObserver.php +3 -3
  137. package/server/src/Providers/FleetOpsServiceProvider.php +12 -0
  138. package/server/src/Support/LiveCacheService.php +1 -1
  139. package/server/src/Support/Telematics/Providers/AfaqyProvider.php +188 -28
  140. package/server/src/Support/Telematics/Providers/SafeeProvider.php +44 -3
  141. package/server/src/Support/Telematics/TelematicService.php +330 -30
  142. package/server/src/routes.php +10 -0
  143. package/server/tests/DeviceFilterTest.php +36 -0
  144. package/server/tests/HubRoutesTest.php +62 -0
  145. package/server/tests/OperationsMonitorEndpointTest.php +38 -0
  146. package/server/tests/OrderQueuedClosureRegressionTest.php +36 -0
  147. package/server/tests/TelematicsHardeningTest.php +455 -3
  148. package/server/tests/VehicleDriverAssignmentTest.php +27 -0
  149. package/translations/ar-ae.yml +1 -1
  150. package/translations/en-us.yaml +1 -0
  151. package/translations/vi-vn.yaml +1 -1
@@ -1,55 +1,59 @@
1
1
  <div class="flex flex-col gap-4 p-4 md:p-6">
2
- <div class="flex flex-col gap-1">
3
- <h2 class="text-base font-bold text-gray-900 dark:text-gray-100">Vehicle Attachment Mapping</h2>
4
- <p class="text-sm text-gray-500 dark:text-gray-300">Map synced provider devices to vehicles so telemetry can power vehicle state, maintenance, and operations workflows.</p>
5
- </div>
6
-
7
- <section class="grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-4">
8
- {{#each this.metrics as |metric|}}
9
- <div class="fleet-ops-widget fleet-ops-kpi-tile fleetops-connectivity-kpi-tile {{metric.accentClass}} relative min-h-[102px] overflow-hidden rounded-md border bg-white shadow-sm dark:bg-gray-800">
10
- <div class="flex h-full items-start justify-between gap-3 p-3">
11
- <div class="min-w-0">
12
- <div class="truncate text-[10px] font-bold uppercase text-gray-500 dark:text-gray-400">{{metric.label}}</div>
13
- <div class="mt-2 text-2xl font-extrabold leading-none text-gray-900 dark:text-gray-50">{{metric.value}}</div>
14
- </div>
15
- <div class="fleetops-connectivity-kpi-icon flex h-8 w-8 flex-shrink-0 items-center justify-center rounded-md text-[13px]">
16
- <FaIcon @icon={{metric.icon}} />
2
+ <section class="overflow-hidden rounded-md border border-gray-200 bg-white shadow-sm dark:border-gray-700 dark:bg-gray-800">
3
+ <div class="border-b border-gray-200 px-4 py-3 dark:border-gray-700">
4
+ <div class="flex flex-col gap-3 xl:flex-row xl:items-center xl:justify-between">
5
+ <div class="min-w-0">
6
+ <h3 class="text-sm font-bold text-gray-900 dark:text-gray-100">Attachment Workspace</h3>
7
+ <p class="mt-1 truncate text-xs text-gray-500 dark:text-gray-300">
8
+ Loaded
9
+ {{this.syncedDevices.length}}
10
+ of
11
+ {{this.totalSyncedDevices}}
12
+ synced devices.
13
+ </p>
14
+ </div>
15
+ <div class="flex w-full min-w-0 items-start gap-2 xl:max-w-4xl">
16
+ <div class="grid min-w-0 flex-1 grid-cols-1 gap-2 sm:grid-cols-3">
17
+ <Input
18
+ @value={{this.query}}
19
+ @type="text"
20
+ class="form-input form-input-sm h-10 w-full text-sm"
21
+ placeholder="Search vehicles or devices"
22
+ {{on "input" this.updateQuery}}
23
+ />
24
+ <select class="form-select form-select-sm h-10 w-full text-sm" {{on "change" this.updateStatus}}>
25
+ <option value="" selected={{not this.status}}>Any status</option>
26
+ <option value="active" selected={{eq this.status "active"}}>Active</option>
27
+ <option value="online" selected={{eq this.status "online"}}>Online</option>
28
+ <option value="recently_offline" selected={{eq this.status "recently_offline"}}>Recently offline</option>
29
+ <option value="offline" selected={{eq this.status "offline"}}>Offline</option>
30
+ <option value="long_offline" selected={{eq this.status "long_offline"}}>Long offline</option>
31
+ <option value="never_connected" selected={{eq this.status "never_connected"}}>Never connected</option>
32
+ <option value="inactive" selected={{eq this.status "inactive"}}>Inactive</option>
33
+ </select>
34
+ <div class="min-w-0">
35
+ <ModelSelect
36
+ @modelName="vehicle"
37
+ @query={{hash telematic_uuid=this.telematic.id}}
38
+ @selectedModel={{this.selectedVehicle}}
39
+ @placeholder="Any vehicle"
40
+ @triggerClass="fleetops-attachments-vehicle-filter-trigger form-select form-select-sm h-10 w-full text-sm"
41
+ @infiniteScroll={{false}}
42
+ @renderInPlace={{false}}
43
+ @allowClear={{true}}
44
+ @onChange={{this.updateSelectedVehicle}}
45
+ as |vehicle|
46
+ >
47
+ {{vehicle.displayName}}
48
+ </ModelSelect>
49
+ </div>
17
50
  </div>
51
+ <Button @icon="refresh" @size="xs" @isLoading={{this.isRefreshing}} @disabled={{this.isRefreshing}} @onClick={{this.refresh}} />
18
52
  </div>
19
53
  </div>
20
- {{/each}}
21
- </section>
22
-
23
- <section class="overflow-hidden rounded-md border border-gray-200 bg-white shadow-sm dark:border-gray-700 dark:bg-gray-800">
24
- <div class="flex flex-col gap-3 border-b border-gray-200 px-4 py-3 dark:border-gray-700 lg:flex-row lg:items-start lg:justify-between">
25
- <div class="max-w-xl">
26
- <h3 class="text-sm font-bold text-gray-900 dark:text-gray-100">Vehicle Groups</h3>
27
- <p class="mt-1 text-xs leading-5 text-gray-500 dark:text-gray-300">Find a vehicle to review its attached devices, or resolve the unattached queue first.</p>
28
- </div>
29
- <div class="flex w-full min-w-0 flex-nowrap items-center justify-end gap-2 overflow-x-auto lg:w-auto lg:flex-shrink-0">
30
- <Input
31
- @value={{this.query}}
32
- @type="text"
33
- class="form-input h-9 min-w-[16rem] flex-shrink-0"
34
- placeholder="Search vehicles or devices"
35
- {{on "input" this.updateQuery}}
36
- />
37
- <select class="form-select h-9 flex-shrink-0" {{on "change" this.updateAttachmentState}}>
38
- <option value="" selected={{not this.attachment_state}}>All mappings</option>
39
- <option value="unattached" selected={{eq this.attachment_state "unattached"}}>Unattached</option>
40
- <option value="attached" selected={{eq this.attachment_state "attached"}}>Attached</option>
41
- </select>
42
- <select class="form-select h-9 flex-shrink-0" {{on "change" this.updateStatus}}>
43
- <option value="" selected={{not this.status}}>Any status</option>
44
- <option value="active" selected={{eq this.status "active"}}>Active</option>
45
- <option value="online" selected={{eq this.status "online"}}>Online</option>
46
- <option value="offline" selected={{eq this.status "offline"}}>Offline</option>
47
- <option value="inactive" selected={{eq this.status "inactive"}}>Inactive</option>
48
- </select>
49
- </div>
50
54
  </div>
51
55
 
52
- {{#if (or (eq this.emptyStateVariant "not_synced") (eq this.emptyStateVariant "filtered_empty"))}}
56
+ {{#if (or (eq this.emptyStateVariant "error") (eq this.emptyStateVariant "not_synced") (eq this.emptyStateVariant "filtered_empty"))}}
53
57
  <div class="p-4">
54
58
  <Telematic::TabEmptyState
55
59
  @tone={{this.emptyStateContent.tone}}
@@ -66,97 +70,170 @@
66
70
  />
67
71
  </div>
68
72
  {{else}}
69
- <div class="divide-y divide-gray-200 dark:divide-gray-700">
70
- {{#if (eq this.emptyStateVariant "empty")}}
71
- <div class="p-4">
72
- <Telematic::TabEmptyState
73
- @tone={{this.emptyStateContent.tone}}
74
- @icon={{this.emptyStateContent.icon}}
75
- @title={{this.emptyStateContent.title}}
76
- @message={{this.emptyStateContent.message}}
77
- @note={{this.emptyStateContent.note}}
78
- />
79
- </div>
80
- {{/if}}
81
-
82
- {{#if (eq this.emptyStateVariant "unattached_only")}}
83
- <div class="p-4">
84
- <Telematic::TabEmptyState
85
- @tone={{this.emptyStateContent.tone}}
86
- @icon={{this.emptyStateContent.icon}}
87
- @title={{this.emptyStateContent.title}}
88
- @message={{this.emptyStateContent.message}}
89
- @note={{this.emptyStateContent.note}}
90
- />
91
- </div>
92
- {{/if}}
93
-
94
- <div class="bg-yellow-50/70 px-4 py-3 dark:bg-yellow-900/10">
95
- <div class="mb-3 flex flex-col gap-2 md:flex-row md:items-center md:justify-between">
96
- <div>
97
- <h4 class="text-sm font-bold text-gray-900 dark:text-gray-100">Unattached Devices</h4>
98
- <p class="text-xs text-yellow-800 dark:text-yellow-300">These devices are synced, but not yet mapped to a vehicle. Attach devices deliberately one at a time.</p>
73
+ <div class="grid grid-cols-1 divide-y divide-gray-200 dark:divide-gray-700 md:grid-cols-2 md:divide-x md:divide-y-0">
74
+ <div class="min-w-0 bg-gray-50/70 dark:bg-gray-900/20">
75
+ <div class="sticky top-0 z-10 border-b border-gray-200 bg-gray-50/95 px-4 py-3 backdrop-blur dark:border-gray-700 dark:bg-gray-800/95">
76
+ <div class="flex items-start justify-between gap-3">
77
+ <div class="min-w-0">
78
+ <div class="flex items-center gap-2">
79
+ <h4 class="text-sm font-bold text-gray-900 dark:text-gray-100">Unattached</h4>
80
+ <span class="text-xs font-semibold text-gray-500 dark:text-gray-400">{{this.filteredUnattachedCount}} devices</span>
81
+ </div>
82
+ {{#if this.selectedDevice}}
83
+ <p class="mt-1 truncate text-xs text-blue-600 dark:text-blue-300">
84
+ Selected:
85
+ {{this.selectedDeviceName}}.
86
+ Choose a vehicle on the right.
87
+ </p>
88
+ {{else}}
89
+ <p class="mt-1 text-xs text-gray-500 dark:text-gray-400">Select a device, then choose a vehicle target.</p>
90
+ {{/if}}
91
+ </div>
92
+ {{#if this.selectedDevice}}
93
+ <Button @icon="times" @size="xs" @onClick={{this.clearSelectedDevice}} />
94
+ {{/if}}
95
+ </div>
99
96
  </div>
100
- <Badge @status={{if this.unattachedDevices.length "warning" "success"}} @hideStatusDot={{true}}>
101
- {{this.unattachedDevices.length}} devices
102
- </Badge>
103
- </div>
104
97
 
105
- {{#if this.unattachedDevices.length}}
106
- <div class="grid grid-cols-1 gap-2 xl:grid-cols-2">
107
- {{#each this.unattachedDevices as |device|}}
108
- <div class="flex flex-col gap-3 rounded-md border border-yellow-200 bg-white p-3 dark:border-yellow-700 dark:bg-gray-800 md:flex-row md:items-center md:justify-between">
109
- <div class="min-w-0">
110
- <Device::Pill @device={{device}} />
111
- </div>
112
- <div class="flex flex-shrink-0 items-center gap-2">
113
- <Button @icon="eye" @text="Open" @size="xs" @onClick={{fn this.deviceActions.transition.view device}} />
114
- <Button @icon="link" @text="Attach" @size="xs" @type="primary" @onClick={{fn this.openAttachDeviceModal device}} />
98
+ <div class="h-[calc(100vh-24rem)] min-h-[28rem] overflow-y-auto">
99
+ {{#if this.unattachedDevices.length}}
100
+ <div class="divide-y divide-gray-200 dark:divide-gray-700">
101
+ {{#each this.unattachedDevices as |device|}}
102
+ <div
103
+ class="group flex min-h-[56px] cursor-pointer items-center gap-3 border-l-2 px-3 py-2 transition {{if
104
+ (eq this.selectedDevice device)
105
+ "border-blue-500 bg-blue-50 dark:bg-blue-900/20"
106
+ "border-transparent bg-white hover:border-blue-300 hover:bg-gray-50 dark:bg-gray-800 dark:hover:bg-gray-700/50"
107
+ }}"
108
+ {{on "click" (fn this.selectUnattachedDevice device)}}
109
+ >
110
+ <div class="flex h-7 w-7 flex-shrink-0 items-center justify-center rounded-md bg-gray-100 text-gray-500 dark:bg-gray-700 dark:text-gray-300">
111
+ <FaIcon @icon="microchip" />
112
+ </div>
113
+ <div class="min-w-0 flex-1">
114
+ <div class="flex min-w-0 items-center gap-2">
115
+ <span class="truncate text-sm font-semibold text-gray-900 dark:text-gray-100">{{or device.displayName device.name device.device_id}}</span>
116
+ <Badge
117
+ @status={{or device.connection_status device.status "default"}}
118
+ @hideIcon={{true}}
119
+ @wrapperClass="text-[9px] px-1 py-0 leading-tight flex-shrink-0"
120
+ >
121
+ {{smart-humanize (or device.connection_status device.status "unknown")}}
122
+ </Badge>
123
+ </div>
124
+ <div class="mt-0.5 flex min-w-0 items-center gap-2 text-xs text-gray-500 dark:text-gray-400">
125
+ <span class="truncate">{{or device.public_id device.device_id device.serial_number device.imei device.id}}</span>
126
+ </div>
127
+ </div>
128
+ <div class="flex flex-shrink-0 items-center gap-1">
129
+ <Button @icon="eye" @size="xs" @onClick={{fn this.deviceActions.transition.view device}} />
130
+ {{#if (eq this.selectedDevice device)}}
131
+ <Button @icon="link" @text="Attach" @size="xs" @type="primary" @onClick={{fn this.openAttachDeviceModal device}} />
132
+ {{else}}
133
+ <Button @icon="link" @size="xs" @type="default" @onClick={{fn this.openAttachDeviceModal device}} />
134
+ {{/if}}
135
+ </div>
136
+ </div>
137
+ {{/each}}
138
+ </div>
139
+ {{else}}
140
+ <div class="p-4">
141
+ <div class="rounded-md border border-green-200 bg-green-50 p-3 text-sm text-green-800 dark:border-green-700 dark:bg-green-900/20 dark:text-green-300">
142
+ All visible synced devices are mapped to vehicles.
115
143
  </div>
116
144
  </div>
117
- {{/each}}
145
+ {{/if}}
118
146
  </div>
119
- {{else}}
120
- <div class="rounded-md border border-green-200 bg-green-50 p-3 text-sm text-green-800 dark:border-green-700 dark:bg-green-900/20 dark:text-green-300">
121
- All synced devices in this view are mapped to vehicles.
122
- </div>
123
- {{/if}}
124
147
  </div>
125
148
 
126
- {{#each this.vehicleGroups as |group|}}
127
- <div class="px-4 py-3">
128
- <div class="mb-3 flex flex-col gap-2 md:flex-row md:items-center md:justify-between">
149
+ <div class="min-w-0">
150
+ <div class="sticky top-0 z-10 border-b border-gray-200 bg-white/95 px-4 py-3 backdrop-blur dark:border-gray-700 dark:bg-gray-800/95">
151
+ <div class="flex items-start justify-between gap-3">
129
152
  <div class="min-w-0">
130
- <h4 class="truncate text-sm font-bold text-gray-900 dark:text-gray-100">{{group.name}}</h4>
131
- <p class="text-xs text-gray-500 dark:text-gray-300">{{group.devices.length}} attached provider devices</p>
153
+ <div class="flex items-center gap-2">
154
+ <h4 class="text-sm font-bold text-gray-900 dark:text-gray-100">Vehicles</h4>
155
+ <span class="text-xs font-semibold text-gray-500 dark:text-gray-400">{{this.vehicleGroups.length}} mapped</span>
156
+ </div>
157
+ {{#if this.selectedDevice}}
158
+ <p class="mt-1 truncate text-xs text-blue-600 dark:text-blue-300">
159
+ Choose a vehicle for
160
+ {{this.selectedDeviceName}}
161
+ </p>
162
+ {{else}}
163
+ <p class="mt-1 text-xs text-gray-500 dark:text-gray-400">Attached devices grouped by vehicle.</p>
164
+ {{/if}}
132
165
  </div>
133
- <Badge @status="info" @hideStatusDot={{true}}>Vehicle mapped</Badge>
134
166
  </div>
167
+ </div>
135
168
 
136
- <div class="overflow-hidden rounded-md border border-gray-200 dark:border-gray-700">
137
- {{#each group.devices as |device|}}
138
- <div class="grid grid-cols-1 gap-3 border-b border-gray-200 p-3 last:border-b-0 dark:border-gray-700 lg:grid-cols-[minmax(0,1.5fr)_minmax(0,1fr)_auto] lg:items-center">
139
- <Device::Pill @device={{device}} />
140
- <div class="grid grid-cols-2 gap-2 text-xs">
141
- <div>
142
- <div class="text-gray-400 dark:text-gray-500">Provider ID</div>
143
- <div class="truncate font-semibold text-gray-900 dark:text-gray-100">{{n-a device.device_id}}</div>
169
+ <div class="h-[calc(100vh-24rem)] min-h-[28rem] overflow-y-auto">
170
+ {{#if this.vehicleGroups.length}}
171
+ <div class="divide-y divide-gray-200 dark:divide-gray-700">
172
+ {{#each this.vehicleGroups as |group|}}
173
+ <div class="border-l-2 border-transparent px-4 py-3 transition hover:border-blue-300 hover:bg-blue-50/40 dark:hover:bg-blue-900/10">
174
+ <div class="mb-2 flex items-center justify-between gap-3">
175
+ <div class="flex min-w-0 items-center gap-3">
176
+ <Image
177
+ src={{or group.vehicle.photo_url group.vehicle.avatar_url}}
178
+ @fallbackSrc={{config "defaultValues.vehicleImage"}}
179
+ alt={{group.name}}
180
+ class="h-10 w-9 flex-shrink-0 rounded-md object-cover"
181
+ />
182
+ <div class="min-w-0">
183
+ <div class="truncate text-sm font-bold text-gray-900 dark:text-gray-100">{{or group.vehicle.displayName group.vehicle.display_name group.vehicle.name group.name}}</div>
184
+ <div class="mt-0.5 truncate text-xs text-gray-500 dark:text-gray-400">
185
+ {{n-a (or group.vehicle.plate_number group.vehicle.internal_id group.vehicle.call_sign group.vehicle.vin)}}
186
+ <span class="mx-1">·</span>
187
+ {{group.devices.length}}
188
+ devices
189
+ </div>
190
+ </div>
191
+ </div>
192
+ {{#if this.selectedDevice}}
193
+ <Button @icon="link" @text="Attach here" @size="xs" @type="primary" @onClick={{fn this.attachSelectedDeviceToGroup group}} />
194
+ {{/if}}
144
195
  </div>
145
- <div>
146
- <div class="text-gray-400 dark:text-gray-500">Last seen</div>
147
- <div class="truncate font-semibold text-gray-900 dark:text-gray-100">{{n-a (format-date-fns device.last_online_at "dd MMM HH:mm")}}</div>
196
+
197
+ <div class="overflow-hidden rounded-md border border-gray-200 dark:border-gray-700">
198
+ {{#each group.devices as |device|}}
199
+ <div class="flex min-h-[46px] items-center gap-3 border-b border-gray-200 px-3 py-2 last:border-b-0 dark:border-gray-700">
200
+ <div class="min-w-0 flex-1">
201
+ <div class="flex min-w-0 items-center gap-2">
202
+ <span class="truncate text-sm font-semibold text-gray-800 dark:text-gray-100">{{or device.displayName device.name device.device_id}}</span>
203
+ <Badge
204
+ @status={{or device.connection_status device.status "default"}}
205
+ @hideIcon={{true}}
206
+ @wrapperClass="text-[9px] px-1 py-0 leading-tight flex-shrink-0"
207
+ >
208
+ {{smart-humanize (or device.connection_status device.status "unknown")}}
209
+ </Badge>
210
+ </div>
211
+ <div class="mt-0.5 flex min-w-0 items-center gap-2 text-xs text-gray-500 dark:text-gray-400">
212
+ <span class="truncate">{{or device.public_id device.device_id device.serial_number device.imei device.id}}</span>
213
+ <span class="h-1 w-1 flex-shrink-0 rounded-full bg-gray-400"></span>
214
+ <span class="flex-shrink-0">{{n-a (format-date-fns device.last_online_at "dd MMM HH:mm")}}</span>
215
+ </div>
216
+ </div>
217
+ <div class="flex flex-shrink-0 items-center gap-1">
218
+ <Button @icon="eye" @size="xs" @onClick={{fn this.deviceActions.transition.view device}} />
219
+ <Button @icon="shuffle" @size="xs" @onClick={{fn this.openAttachDeviceModal device}} />
220
+ <Button @icon="unlink" @size="xs" @type="danger" @onClick={{fn this.detachDevice device}} />
221
+ </div>
222
+ </div>
223
+ {{/each}}
148
224
  </div>
149
225
  </div>
150
- <div class="flex items-center gap-2 lg:justify-end">
151
- <Button @icon="eye" @text="Open" @size="xs" @onClick={{fn this.deviceActions.transition.view device}} />
152
- <Button @icon="shuffle" @text="Change" @size="xs" @onClick={{fn this.openAttachDeviceModal device}} />
153
- <Button @icon="unlink" @text="Detach" @size="xs" @type="danger" @onClick={{fn this.detachDevice device}} />
154
- </div>
226
+ {{/each}}
227
+ </div>
228
+ {{else}}
229
+ <div class="p-4">
230
+ <div class="rounded-md border border-gray-200 bg-gray-50 p-4 text-sm text-gray-600 dark:border-gray-700 dark:bg-gray-900 dark:text-gray-300">
231
+ No visible vehicle groups. Attach a device from the left pane or adjust the filters.
155
232
  </div>
156
- {{/each}}
157
- </div>
233
+ </div>
234
+ {{/if}}
158
235
  </div>
159
- {{/each}}
236
+ </div>
160
237
  </div>
161
238
  {{/if}}
162
239
  </section>
@@ -26,7 +26,7 @@
26
26
  <h3 class="text-sm font-bold text-gray-900 dark:text-gray-100">Synced Devices</h3>
27
27
  <p class="mt-1 text-xs leading-5 text-gray-500 dark:text-gray-300">Search provider identifiers, filter attachment state, and open devices for troubleshooting.</p>
28
28
  </div>
29
- <div class="fleetops-provider-connections-actions flex min-w-0 flex-wrap items-center gap-2 lg:ml-auto lg:flex-nowrap lg:justify-end">
29
+ <div class="fleetops-provider-connections-actions flex min-w-0 flex-wrap items-center gap-1 lg:ml-auto lg:flex-nowrap lg:justify-end">
30
30
  <Layout::Resource::TabularActions
31
31
  @title="Synced Devices"
32
32
  @searchQuery={{this.query}}
@@ -40,6 +40,8 @@
40
40
  @searchInputClass="fleetops-provider-connections-search"
41
41
  @filterPickerButtonClass="fleetops-provider-connections-action-button"
42
42
  @columnPickerButtonClass="fleetops-provider-connections-action-button"
43
+ @filterPickerSize="sm"
44
+ @columnPickerSize="sm"
43
45
  />
44
46
  </div>
45
47
  </div>
@@ -70,6 +72,7 @@
70
72
  @onPageChange={{fn (mut this.page)}}
71
73
  @controller={{this}}
72
74
  @withoutHeader={{true}}
75
+ @tableWrapperClass="no-table-extra-spacing"
73
76
  @emptyStateComponent={{component
74
77
  "table/empty-state"
75
78
  variant="compact"
@@ -26,7 +26,7 @@
26
26
  <h3 class="text-sm font-bold text-gray-900 dark:text-gray-100">Telemetry Events</h3>
27
27
  <p class="mt-1 text-xs leading-5 text-gray-500 dark:text-gray-300">Filter events by severity, device, type, and processed state before opening details.</p>
28
28
  </div>
29
- <div class="fleetops-provider-connections-actions flex min-w-0 flex-wrap items-center gap-2 lg:ml-auto lg:flex-nowrap lg:justify-end">
29
+ <div class="fleetops-provider-connections-actions flex min-w-0 flex-wrap items-center gap-1 lg:ml-auto lg:flex-nowrap lg:justify-end">
30
30
  <Layout::Resource::TabularActions
31
31
  @title="Telemetry Events"
32
32
  @searchQuery={{this.query}}
@@ -40,6 +40,8 @@
40
40
  @searchInputClass="fleetops-provider-connections-search"
41
41
  @filterPickerButtonClass="fleetops-provider-connections-action-button"
42
42
  @columnPickerButtonClass="fleetops-provider-connections-action-button"
43
+ @filterPickerSize="sm"
44
+ @columnPickerSize="sm"
43
45
  />
44
46
  </div>
45
47
  </div>
@@ -83,6 +85,7 @@
83
85
  @onPageChange={{fn (mut this.page)}}
84
86
  @controller={{this}}
85
87
  @withoutHeader={{true}}
88
+ @tableWrapperClass="no-table-extra-spacing"
86
89
  @emptyStateComponent={{component
87
90
  "table/empty-state"
88
91
  variant="compact"
@@ -40,6 +40,8 @@
40
40
  @searchInputClass="fleetops-provider-connections-search"
41
41
  @filterPickerButtonClass="fleetops-provider-connections-action-button"
42
42
  @columnPickerButtonClass="fleetops-provider-connections-action-button"
43
+ @filterPickerSize="sm"
44
+ @columnPickerSize="sm"
43
45
  />
44
46
  </div>
45
47
  </div>
@@ -70,6 +72,7 @@
70
72
  @onPageChange={{fn (mut this.page)}}
71
73
  @controller={{this}}
72
74
  @withoutHeader={{true}}
75
+ @tableWrapperClass="no-table-extra-spacing"
73
76
  @emptyStateComponent={{component
74
77
  "table/empty-state"
75
78
  variant="compact"
@@ -0,0 +1,113 @@
1
+ <div class="fleet-ops-control-hub" data-test-fleet-ops-maintenance-hub>
2
+ <section class="fleet-ops-control-hub-header">
3
+ <div class="fleet-ops-control-hub-header-copy">
4
+ <div class="fleet-ops-control-hub-eyebrow">Maintenance</div>
5
+ <h1 class="fleet-ops-control-hub-title">Maintenance Hub</h1>
6
+ <p class="fleet-ops-control-hub-subtitle">Plan recurring service, coordinate work orders, and keep assets ready for daily operations.</p>
7
+ </div>
8
+ <div class="fleet-ops-control-hub-header-actions">
9
+ <Button @icon="refresh" @size="sm" @helpText={{t "common.reload"}} @onClick={{perform this.loadHub}} />
10
+ </div>
11
+ </section>
12
+
13
+ <div class="fleet-ops-control-hub-body">
14
+ <section class="fleet-ops-control-hub-kpis">
15
+ {{#each this.kpis as |kpi|}}
16
+ <LinkTo @route={{kpi.route}} class="fleet-ops-control-hub-kpi fleet-ops-control-hub-tone-{{kpi.tone}}" data-test-fleet-ops-maintenance-kpi={{kpi.key}}>
17
+ <div class="fleet-ops-control-hub-kpi-copy">
18
+ <div class="fleet-ops-control-hub-kpi-label">{{kpi.label}}</div>
19
+ <div class="fleet-ops-control-hub-kpi-value">{{kpi.value}}</div>
20
+ <div class="fleet-ops-control-hub-kpi-caption">{{kpi.caption}}</div>
21
+ <div class="fleet-ops-control-hub-kpi-action">
22
+ <span>Open {{kpi.label}}</span>
23
+ <FaIcon @icon="arrow-right" />
24
+ </div>
25
+ </div>
26
+ <div class="fleet-ops-control-hub-kpi-icon"><FaIcon @icon={{kpi.icon}} /></div>
27
+ </LinkTo>
28
+ {{/each}}
29
+ </section>
30
+
31
+ <section class="fleet-ops-control-hub-grid">
32
+ <div class="fleet-ops-control-hub-main">
33
+ {{#each this.sections as |section|}}
34
+ <div class="fleet-ops-control-hub-panel" data-test-fleet-ops-maintenance-section={{section.key}}>
35
+ <div class="fleet-ops-control-hub-panel-header">
36
+ <div>
37
+ <h2>{{section.title}}</h2>
38
+ <p>{{section.description}}</p>
39
+ </div>
40
+ </div>
41
+
42
+ <div class="fleet-ops-control-hub-link-grid">
43
+ {{#each section.links as |link|}}
44
+ <LinkTo @route={{link.route}} class="fleet-ops-control-hub-link" data-test-fleet-ops-hub-card={{link.label}}>
45
+ <span class="fleet-ops-control-hub-link-icon"><FaIcon @icon={{link.icon}} /></span>
46
+ <span class="fleet-ops-control-hub-link-copy">
47
+ <span class="fleet-ops-control-hub-link-title">{{link.label}}</span>
48
+ <span class="fleet-ops-control-hub-link-description">{{link.description}}</span>
49
+ </span>
50
+ <span class="fleet-ops-control-hub-link-count">{{link.count}}</span>
51
+ </LinkTo>
52
+ {{/each}}
53
+ </div>
54
+ </div>
55
+ {{/each}}
56
+ </div>
57
+
58
+ <aside class="fleet-ops-control-hub-side">
59
+ <div class="fleet-ops-control-hub-panel">
60
+ <div class="fleet-ops-control-hub-panel-header">
61
+ <div>
62
+ <h2>Action Queue</h2>
63
+ <p>Signals from schedules, work orders, parts, and equipment.</p>
64
+ </div>
65
+ </div>
66
+
67
+ <div class="fleet-ops-control-hub-action-list">
68
+ {{#each this.actions as |action|}}
69
+ {{#if action.route}}
70
+ <LinkTo @route={{action.route}} @query={{action.query}} class="fleet-ops-control-hub-action fleet-ops-control-hub-tone-{{action.tone}}">
71
+ <FaIcon @icon={{action.icon}} />
72
+ <span>
73
+ <strong>{{action.label}}</strong>
74
+ <small>{{action.description}}</small>
75
+ </span>
76
+ </LinkTo>
77
+ {{else}}
78
+ <div class="fleet-ops-control-hub-action fleet-ops-control-hub-tone-{{action.tone}}">
79
+ <FaIcon @icon={{action.icon}} />
80
+ <span>
81
+ <strong>{{action.label}}</strong>
82
+ <small>{{action.description}}</small>
83
+ </span>
84
+ </div>
85
+ {{/if}}
86
+ {{/each}}
87
+ </div>
88
+ </div>
89
+
90
+ <div class="fleet-ops-control-hub-doc-note">
91
+ <div class="fleet-ops-control-hub-doc-note-heading">
92
+ <FaIcon @icon="lightbulb" />
93
+ <span>Guides</span>
94
+ </div>
95
+ <p class="fleet-ops-control-hub-doc-note-copy">Read the guide before changing maintenance workflows.</p>
96
+ <div class="fleet-ops-control-hub-doc-grid">
97
+ {{#each this.docs as |doc|}}
98
+ <button type="button" class="fleet-ops-control-hub-doc-link" {{on "click" (fn this.openDocs doc)}}>
99
+ <FaIcon @icon={{doc.icon}} />
100
+ <span class="fleet-ops-control-hub-doc-copy">
101
+ <strong>Read the guide on {{doc.label}}</strong>
102
+ <small>{{doc.description}}</small>
103
+ </span>
104
+ </button>
105
+ {{/each}}
106
+ </div>
107
+ </div>
108
+ </aside>
109
+ </section>
110
+ </div>
111
+ </div>
112
+
113
+ {{outlet}}
@@ -46,7 +46,7 @@
46
46
  <aside class="space-y-4">
47
47
  <section class="rounded-md border border-gray-200 bg-white p-4 shadow-sm dark:border-gray-700 dark:bg-gray-800">
48
48
  <h2 class="text-sm font-bold text-gray-900 dark:text-gray-100">Manual Match</h2>
49
- <p class="mt-1 text-xs leading-5 text-gray-500 dark:text-gray-300">Use a FleetOps UUID or public ID when automatic matching cannot identify the vehicle or order.</p>
49
+ <p class="mt-1 text-xs leading-5 text-gray-500 dark:text-gray-300">Use a Fleet-Ops UUID or public ID when automatic matching cannot identify the vehicle or order.</p>
50
50
  <div class="mt-3 space-y-3 no-input-group-padding">
51
51
  <InputGroup @name="Vehicle ID">
52
52
  <Input class="form-input w-full" @value={{this.vehicleId}} {{on "input" this.setVehicleId}} />