@fleetbase/fleetops-engine 0.6.54 → 0.6.56

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 (220) hide show
  1. package/addon/components/admin/map-settings.hbs +1 -1
  2. package/addon/components/admin/routing-settings.hbs +2 -1
  3. package/addon/components/cell/attached-vehicle.hbs +10 -0
  4. package/addon/components/cell/attached-vehicle.js +58 -0
  5. package/addon/components/cell/device-identity.hbs +24 -0
  6. package/addon/components/cell/device-identity.js +129 -0
  7. package/addon/components/cell/driver-identity.hbs +33 -0
  8. package/addon/components/cell/driver-identity.js +143 -0
  9. package/addon/components/cell/equipment-identity.hbs +5 -0
  10. package/addon/components/cell/equipment-identity.js +48 -0
  11. package/addon/components/cell/part-identity.hbs +5 -0
  12. package/addon/components/cell/part-identity.js +63 -0
  13. package/addon/components/cell/telematic-device.hbs +38 -0
  14. package/addon/components/cell/telematic-device.js +44 -0
  15. package/addon/components/cell/telematic-provider.hbs +33 -16
  16. package/addon/components/cell/telematic-provider.js +50 -8
  17. package/addon/components/cell/vehicle-identity.hbs +33 -0
  18. package/addon/components/cell/vehicle-identity.js +138 -0
  19. package/addon/components/device/details.js +1 -1
  20. package/addon/components/device/form.hbs +2 -2
  21. package/addon/components/device/form.js +7 -0
  22. package/addon/components/device/panel-header.hbs +45 -20
  23. package/addon/components/device/panel-header.js +49 -1
  24. package/addon/components/device/panel-tabs/events.hbs +30 -0
  25. package/addon/components/device/panel-tabs/events.js +128 -0
  26. package/addon/components/device/panel-tabs/sensors.hbs +30 -0
  27. package/addon/components/device/panel-tabs/sensors.js +93 -0
  28. package/addon/components/device/panel-tabs/vehicle.hbs +68 -0
  29. package/addon/components/device/panel-tabs/vehicle.js +90 -0
  30. package/addon/components/device-event/details.hbs +192 -0
  31. package/addon/components/device-event/details.js +151 -0
  32. package/addon/components/driver/schedule.js +1 -1
  33. package/addon/components/equipment/form.js +8 -1
  34. package/addon/components/layout/fleet-ops-sidebar.hbs +1 -1
  35. package/addon/components/layout/fleet-ops-sidebar.js +20 -6
  36. package/addon/components/map/drawer/device-event-listing.js +2 -0
  37. package/addon/components/map/drawer/driver-listing.js +2 -0
  38. package/addon/components/map/drawer/place-listing.js +2 -0
  39. package/addon/components/map/drawer/position-listing.hbs +11 -6
  40. package/addon/components/map/drawer/position-listing.js +17 -4
  41. package/addon/components/map/drawer/vehicle-listing.js +2 -0
  42. package/addon/components/sensor/panel-header.hbs +47 -15
  43. package/addon/components/sensor/panel-header.js +59 -1
  44. package/addon/components/table/cell/dropdown/action-item.hbs +31 -0
  45. package/addon/components/table/cell/dropdown/action-item.js +62 -0
  46. package/addon/components/table/cell/dropdown.hbs +28 -0
  47. package/addon/components/table/cell/dropdown.js +91 -0
  48. package/addon/components/telematic/details.hbs +1 -1
  49. package/addon/components/telematic/form.hbs +2 -2
  50. package/addon/components/telematic/settings.hbs +1 -1
  51. package/addon/components/vendor/panel-header.hbs +82 -0
  52. package/addon/components/vendor/panel-header.js +48 -0
  53. package/addon/controllers/connectivity/devices/index/details/events.js +1 -1
  54. package/addon/controllers/connectivity/devices/index/details/vehicle.js +50 -2
  55. package/addon/controllers/connectivity/devices/index.js +144 -129
  56. package/addon/controllers/connectivity/events/details.js +70 -0
  57. package/addon/controllers/connectivity/events/index.js +187 -11
  58. package/addon/controllers/connectivity/sensors/index.js +160 -14
  59. package/addon/controllers/connectivity/telematics/details/devices.js +126 -134
  60. package/addon/controllers/connectivity/telematics/details/events.js +1 -1
  61. package/addon/controllers/connectivity/telematics/details/sensors.js +4 -0
  62. package/addon/controllers/maintenance/equipment/index.js +1 -2
  63. package/addon/controllers/maintenance/parts/index.js +1 -2
  64. package/addon/controllers/management/drivers/index.js +82 -42
  65. package/addon/controllers/management/fuel-reports/index.js +63 -4
  66. package/addon/controllers/management/issues/index.js +63 -4
  67. package/addon/controllers/management/vehicles/index.js +33 -6
  68. package/addon/controllers/settings/map.js +15 -0
  69. package/addon/instance-initializers/register-vroom-allocation.js +1 -1
  70. package/addon/routes/connectivity/devices/index/details/vehicle.js +24 -2
  71. package/addon/routes/connectivity/devices/index.js +7 -0
  72. package/addon/routes/connectivity/events/details.js +28 -0
  73. package/addon/routes/connectivity/events/index.js +6 -2
  74. package/addon/routes/connectivity/sensors/index.js +7 -2
  75. package/addon/routes/connectivity/telematics/details/devices.js +2 -0
  76. package/addon/routes/management/vehicles/index/details.js +3 -1
  77. package/addon/services/device-actions.js +30 -0
  78. package/addon/services/device-event-actions.js +1 -1
  79. package/addon/services/equipment-actions.js +1 -0
  80. package/addon/services/map-adapter/google.js +25 -0
  81. package/addon/services/map-adapter/leaflet.js +1 -1
  82. package/addon/services/map-adapter-interface.js +1 -1
  83. package/addon/services/map-manager.js +4 -1
  84. package/addon/services/map-settings.js +15 -0
  85. package/addon/services/sensor-actions.js +3 -0
  86. package/addon/services/service-rate-actions.js +39 -1
  87. package/addon/services/vehicle-actions.js +24 -12
  88. package/addon/services/vendor-actions.js +3 -0
  89. package/addon/styles/fleetops-engine.css +9 -1
  90. package/addon/templates/connectivity/devices/index/details/vehicle.hbs +66 -2
  91. package/addon/templates/connectivity/events/details.hbs +40 -0
  92. package/addon/templates/management/vendors/index/details.hbs +2 -2
  93. package/addon/templates/settings/map.hbs +38 -9
  94. package/addon/templates/settings/notifications.hbs +2 -1
  95. package/addon/templates/settings/routing.hbs +1 -0
  96. package/addon/utils/device-table-columns.js +288 -0
  97. package/addon/utils/identity-cell-resource.js +20 -0
  98. package/addon/utils/leaflet-plugin-loader.js +5 -5
  99. package/addon/utils/map-drawer-dropdown-position.js +47 -0
  100. package/addon/utils/route-colors.js +1 -1
  101. package/addon/utils/trackable-option.js +93 -0
  102. package/app/components/cell/attached-vehicle.js +1 -0
  103. package/app/components/cell/device-identity.js +1 -0
  104. package/app/components/cell/driver-identity.js +1 -0
  105. package/app/components/cell/equipment-identity.js +1 -0
  106. package/app/components/cell/part-identity.js +1 -0
  107. package/app/components/cell/telematic-device.js +1 -0
  108. package/app/components/cell/vehicle-identity.js +1 -0
  109. package/app/components/device/panel-tabs/events.hbs +30 -0
  110. package/app/components/device/panel-tabs/events.js +128 -0
  111. package/app/components/device/panel-tabs/sensors.hbs +30 -0
  112. package/app/components/device/panel-tabs/sensors.js +93 -0
  113. package/app/components/device/panel-tabs/vehicle.hbs +68 -0
  114. package/app/components/device/panel-tabs/vehicle.js +90 -0
  115. package/app/components/device-event/details.hbs +192 -0
  116. package/app/components/device-event/details.js +1 -0
  117. package/app/components/table/cell/dropdown/action-item.js +1 -0
  118. package/app/components/table/cell/dropdown.js +1 -0
  119. package/app/components/vendor/panel-header.js +1 -0
  120. package/app/controllers/connectivity/events/{index/details.js → details.js} +1 -1
  121. package/app/routes/connectivity/events/{index/details.js → details.js} +1 -1
  122. package/app/templates/connectivity/events/{index/details.js → details.js} +1 -1
  123. package/app/utils/map-drawer-dropdown-position.js +1 -0
  124. package/app/utils/trackable-option.js +1 -0
  125. package/composer.json +1 -1
  126. package/extension.json +1 -1
  127. package/package.json +4 -4
  128. package/server/migrations/2026_06_23_000001_delete_bad_afaqy_telematics_sensors.php +34 -0
  129. package/server/src/Console/Commands/SyncTelematics.php +4 -4
  130. package/server/src/Exports/ContactExport.php +12 -5
  131. package/server/src/Exports/DeviceExport.php +92 -0
  132. package/server/src/Exports/DriverExport.php +28 -3
  133. package/server/src/Exports/EquipmentExport.php +94 -0
  134. package/server/src/Exports/FleetExport.php +22 -7
  135. package/server/src/Exports/FuelReportExport.php +16 -1
  136. package/server/src/Exports/IssueExport.php +25 -3
  137. package/server/src/Exports/MaintenanceExport.php +106 -0
  138. package/server/src/Exports/MaintenanceScheduleExport.php +99 -0
  139. package/server/src/Exports/OrderExport.php +24 -5
  140. package/server/src/Exports/PartExport.php +99 -0
  141. package/server/src/Exports/PlaceExport.php +33 -7
  142. package/server/src/Exports/SensorExport.php +94 -0
  143. package/server/src/Exports/ServiceAreaExport.php +31 -7
  144. package/server/src/Exports/ServiceRateExport.php +41 -1
  145. package/server/src/Exports/TelematicExport.php +90 -0
  146. package/server/src/Exports/VehicleExport.php +274 -8
  147. package/server/src/Exports/VendorExport.php +15 -2
  148. package/server/src/Exports/WorkOrderExport.php +92 -0
  149. package/server/src/Http/Controllers/Api/v1/Concerns/ResolvesFleetOpsApiResources.php +176 -0
  150. package/server/src/Http/Controllers/Api/v1/DeviceController.php +225 -0
  151. package/server/src/Http/Controllers/Api/v1/EquipmentController.php +114 -0
  152. package/server/src/Http/Controllers/Api/v1/FuelTransactionController.php +185 -0
  153. package/server/src/Http/Controllers/Api/v1/PartController.php +120 -0
  154. package/server/src/Http/Controllers/Api/v1/SensorController.php +132 -0
  155. package/server/src/Http/Controllers/Api/v1/WorkOrderController.php +161 -0
  156. package/server/src/Http/Controllers/Internal/v1/DeviceController.php +36 -1
  157. package/server/src/Http/Controllers/Internal/v1/DeviceEventController.php +1 -1
  158. package/server/src/Http/Controllers/Internal/v1/EquipmentController.php +17 -0
  159. package/server/src/Http/Controllers/Internal/v1/LiveController.php +1 -1
  160. package/server/src/Http/Controllers/Internal/v1/MaintenanceController.php +17 -0
  161. package/server/src/Http/Controllers/Internal/v1/MaintenanceScheduleController.php +17 -0
  162. package/server/src/Http/Controllers/Internal/v1/PartController.php +17 -0
  163. package/server/src/Http/Controllers/Internal/v1/SensorController.php +18 -0
  164. package/server/src/Http/Controllers/Internal/v1/ServiceRateController.php +82 -13
  165. package/server/src/Http/Controllers/Internal/v1/SettingController.php +29 -5
  166. package/server/src/Http/Controllers/Internal/v1/TelematicController.php +18 -0
  167. package/server/src/Http/Controllers/Internal/v1/WorkOrderController.php +17 -0
  168. package/server/src/Http/Filter/DeviceEventFilter.php +21 -0
  169. package/server/src/Http/Filter/DeviceFilter.php +50 -4
  170. package/server/src/Http/Filter/EquipmentFilter.php +23 -0
  171. package/server/src/Http/Filter/FuelProviderTransactionFilter.php +57 -2
  172. package/server/src/Http/Filter/PartFilter.php +47 -0
  173. package/server/src/Http/Filter/SensorFilter.php +34 -6
  174. package/server/src/Http/Filter/WorkOrderFilter.php +23 -0
  175. package/server/src/Http/Requests/CreateDeviceRequest.php +51 -0
  176. package/server/src/Http/Requests/CreateEquipmentRequest.php +35 -0
  177. package/server/src/Http/Requests/CreateFuelTransactionRequest.php +50 -0
  178. package/server/src/Http/Requests/CreatePartRequest.php +40 -0
  179. package/server/src/Http/Requests/CreateSensorRequest.php +47 -0
  180. package/server/src/Http/Requests/CreateWorkOrderRequest.php +42 -0
  181. package/server/src/Http/Requests/UpdateDeviceRequest.php +7 -0
  182. package/server/src/Http/Requests/UpdateEquipmentRequest.php +7 -0
  183. package/server/src/Http/Requests/UpdateFuelTransactionRequest.php +7 -0
  184. package/server/src/Http/Requests/UpdatePartRequest.php +7 -0
  185. package/server/src/Http/Requests/UpdateSensorRequest.php +7 -0
  186. package/server/src/Http/Requests/UpdateWorkOrderRequest.php +7 -0
  187. package/server/src/Http/Resources/v1/Device.php +72 -0
  188. package/server/src/Http/Resources/v1/Equipment.php +57 -0
  189. package/server/src/Http/Resources/v1/FuelTransaction.php +69 -0
  190. package/server/src/Http/Resources/v1/Index/Vehicle.php +19 -0
  191. package/server/src/Http/Resources/v1/Part.php +64 -0
  192. package/server/src/Http/Resources/v1/Sensor.php +68 -0
  193. package/server/src/Jobs/SyncTelematicDevicesJob.php +194 -38
  194. package/server/src/Models/Device.php +2 -0
  195. package/server/src/Models/DeviceEvent.php +81 -0
  196. package/server/src/Models/Equipment.php +55 -0
  197. package/server/src/Models/Order.php +2 -1
  198. package/server/src/Observers/PurchaseRateObserver.php +2 -1
  199. package/server/src/Support/Analytics/RevenueTrend.php +3 -8
  200. package/server/src/Support/Metrics/ActiveRevenueQuery.php +121 -0
  201. package/server/src/Support/Metrics/AvgOrderValueMetric.php +1 -7
  202. package/server/src/Support/Metrics/EarningsMetric.php +1 -10
  203. package/server/src/Support/Telematics/Providers/AfaqyProvider.php +105 -5
  204. package/server/src/Support/Telematics/Providers/FlespiProvider.php +89 -15
  205. package/server/src/Support/Telematics/Providers/GeotabProvider.php +123 -35
  206. package/server/src/Support/Telematics/Providers/SafeeProvider.php +567 -47
  207. package/server/src/Support/Telematics/Providers/SamsaraProvider.php +122 -14
  208. package/server/src/Support/Telematics/TelematicService.php +28 -9
  209. package/server/src/routes.php +67 -15
  210. package/server/tests/DeviceFilterTest.php +9 -0
  211. package/server/tests/DriverVehicleStatusDefaultTest.php +19 -2
  212. package/server/tests/EquipmentAssignmentMorphTypeTest.php +35 -0
  213. package/server/tests/ExportCoverageTest.php +79 -0
  214. package/server/tests/MetricsRegistryTest.php +45 -5
  215. package/server/tests/PublicFleetOpsRestEndpointsTest.php +191 -0
  216. package/server/tests/ServiceRateRouteValidationTest.php +101 -0
  217. package/server/tests/TelematicsHardeningTest.php +873 -7
  218. package/addon/controllers/connectivity/events/index/details.js +0 -3
  219. package/addon/routes/connectivity/events/index/details.js +0 -3
  220. package/addon/templates/connectivity/events/index/details.hbs +0 -1
@@ -66,6 +66,26 @@ export default class VehicleActionsService extends ResourceActionService {
66
66
  this.initialize('vehicle', { status: 'available' });
67
67
  }
68
68
 
69
+ async resolveVehicleResource(vehicle) {
70
+ if (typeof vehicle?.then === 'function') {
71
+ return await vehicle;
72
+ }
73
+
74
+ if (typeof vehicle?.loadResource === 'function') {
75
+ return (await vehicle.loadResource()) ?? vehicle;
76
+ }
77
+
78
+ return vehicle;
79
+ }
80
+
81
+ async reloadIndexResource(vehicle) {
82
+ if (vehicle?.meta?._index_resource) {
83
+ await vehicle.reload();
84
+ }
85
+
86
+ return vehicle;
87
+ }
88
+
69
89
  transition = {
70
90
  view: (vehicle) => this.transitionTo('management.vehicles.index.details', vehicle),
71
91
  edit: (vehicle) => this.transitionTo('management.vehicles.index.edit', vehicle),
@@ -87,9 +107,7 @@ export default class VehicleActionsService extends ResourceActionService {
87
107
  });
88
108
  },
89
109
  edit: async (vehicle, options = {}) => {
90
- if (vehicle?.meta?._index_resource) {
91
- await vehicle.reload();
92
- }
110
+ vehicle = await this.reloadIndexResource(await this.resolveVehicleResource(vehicle));
93
111
 
94
112
  return this.resourceContextPanel.open({
95
113
  content: 'vehicle/form',
@@ -109,9 +127,7 @@ export default class VehicleActionsService extends ResourceActionService {
109
127
  });
110
128
  },
111
129
  view: async (vehicle, options = {}) => {
112
- if (vehicle?.meta?._index_resource) {
113
- await vehicle.reload();
114
- }
130
+ vehicle = await this.reloadIndexResource(await this.resolveVehicleResource(vehicle));
115
131
 
116
132
  return this.resourceContextPanel.open({
117
133
  vehicle,
@@ -144,9 +160,7 @@ export default class VehicleActionsService extends ResourceActionService {
144
160
  });
145
161
  },
146
162
  edit: async (vehicle, options = {}, saveOptions = {}) => {
147
- if (vehicle?.meta?._index_resource) {
148
- await vehicle.reload();
149
- }
163
+ vehicle = await this.reloadIndexResource(await this.resolveVehicleResource(vehicle));
150
164
 
151
165
  return this.modalsManager.show('modals/resource', {
152
166
  resource: vehicle,
@@ -159,9 +173,7 @@ export default class VehicleActionsService extends ResourceActionService {
159
173
  });
160
174
  },
161
175
  view: async (vehicle, options = {}) => {
162
- if (vehicle?.meta?._index_resource) {
163
- await vehicle.reload();
164
- }
176
+ vehicle = await this.reloadIndexResource(await this.resolveVehicleResource(vehicle));
165
177
 
166
178
  return this.modalsManager.show('modals/resource', {
167
179
  resource: vehicle,
@@ -43,8 +43,11 @@ export default class VendorActionsService extends ResourceActionService {
43
43
  view: (vendor) => {
44
44
  return this.resourceContextPanel.open({
45
45
  vendor,
46
+ header: 'vendor/panel-header',
46
47
  tabs: [
47
48
  {
49
+ key: 'overview',
50
+ id: 'overview',
48
51
  label: this.intl.t('common.overview'),
49
52
  component: 'vendor/details',
50
53
  },
@@ -1,4 +1,4 @@
1
- /* stylelint-disable alpha-value-notation -- FleetOps lazy engine production CSS is minified by legacy clean-css, which corrupts comma-form rgba() percent alpha values. */
1
+ /* stylelint-disable alpha-value-notation -- Fleet-Ops lazy engine production CSS is minified by legacy clean-css, which corrupts comma-form rgba() percent alpha values. */
2
2
  .scheduler-left-sidebar {
3
3
  border-right: 1px rgb(229 231 235 / 100%) solid;
4
4
  height: 100vh;
@@ -294,6 +294,14 @@ body[data-theme='dark'] .fleetops-connectivity-kpi-accent-rose .fleetops-connect
294
294
  color: #fde047;
295
295
  }
296
296
 
297
+ .fleetops-device-status-badge.status-badge > span {
298
+ padding: 0 0.25rem !important;
299
+ border-radius: 0.125rem !important;
300
+ font-size: 0.5625rem !important;
301
+ line-height: 0.75rem !important;
302
+ font-weight: 700;
303
+ }
304
+
297
305
  .fleetops-provider-connections-panel .table-wrapper table tbody tr td,
298
306
  .fleetops-provider-connections-panel .next-table-wrapper table tbody tr td {
299
307
  vertical-align: top;
@@ -1,5 +1,5 @@
1
1
  <div class="flex flex-col gap-3 p-3">
2
- <div class="flex items-start justify-between gap-3">
2
+ <div class="flex flex-col items-start justify-between gap-3">
3
3
  <div class="min-w-0">
4
4
  <h2 class="text-base font-bold text-gray-900 dark:text-gray-100">Vehicle Attachment</h2>
5
5
  <p class="mt-0.5 text-xs text-gray-500 dark:text-gray-300">Fleet context for telemetry from this device.</p>
@@ -7,9 +7,12 @@
7
7
 
8
8
  {{#if this.hasVehicle}}
9
9
  <div class="flex flex-nowrap items-center gap-2 whitespace-nowrap">
10
- {{#if this.vehicle.id}}
10
+ {{#if this.canOpenVehicle}}
11
11
  <Button @icon="eye" @text="Open Vehicle" @size="xs" @onClick={{this.openVehicle}} />
12
12
  {{/if}}
13
+ {{#if this.canLocateVehicle}}
14
+ <Button @icon="location-dot" @text="Locate" @size="xs" @onClick={{this.locateVehicle}} />
15
+ {{/if}}
13
16
  <Button @icon="shuffle" @text="Change Vehicle" @size="xs" @onClick={{this.attachToVehicle}} />
14
17
  <Button @icon="unlink" @text="Detach" @size="xs" @type="danger" @onClick={{this.detachFromVehicle}} />
15
18
  </div>
@@ -70,6 +73,67 @@
70
73
  </div>
71
74
  </div>
72
75
  </section>
76
+
77
+ <section class="overflow-hidden rounded-md border border-gray-200 bg-white shadow-sm dark:border-gray-700 dark:bg-gray-800">
78
+ <div class="flex items-start justify-between gap-3 border-b border-gray-200 px-3 py-2 dark:border-gray-700">
79
+ <div class="min-w-0">
80
+ <h3 class="text-sm font-bold text-gray-900 dark:text-gray-100">Latest Positions</h3>
81
+ <p class="mt-0.5 text-xs text-gray-500 dark:text-gray-300">Recent vehicle coordinates reported through this device attachment.</p>
82
+ </div>
83
+ {{#if this.canOpenVehicle}}
84
+ <Button @icon="route" @text="Full History" @size="xs" @onClick={{this.openVehiclePositions}} />
85
+ {{/if}}
86
+ </div>
87
+
88
+ {{#if this.hasPositions}}
89
+ <div class="grid grid-cols-1 gap-0 divide-y divide-gray-100 dark:divide-gray-700">
90
+ {{#each this.positions as |position index|}}
91
+ <div class="flex flex-wrap items-center justify-between gap-3 px-3 py-2 text-xs">
92
+ <div class="flex min-w-0 items-center gap-2">
93
+ <div class="flex h-7 w-7 flex-shrink-0 items-center justify-center rounded-md bg-blue-50 text-blue-600 dark:bg-blue-900/30 dark:text-blue-300">
94
+ <span class="font-bold">{{add index 1}}</span>
95
+ </div>
96
+ <div class="min-w-0">
97
+ <div class="truncate font-semibold text-gray-900 dark:text-gray-100">{{n-a (format-date-fns position.created_at "dd MMM yyyy, HH:mm")}}</div>
98
+ <div class="truncate text-gray-500 dark:text-gray-400">{{n-a (point-coordinates position.coordinates)}}</div>
99
+ </div>
100
+ </div>
101
+ <div class="grid grid-cols-2 gap-2 text-right text-gray-500 dark:text-gray-400 sm:grid-cols-3">
102
+ <div>
103
+ <div class="font-semibold uppercase text-gray-400 dark:text-gray-500">Speed</div>
104
+ <div class="text-gray-800 dark:text-gray-100">{{n-a position.speed}}</div>
105
+ </div>
106
+ <div>
107
+ <div class="font-semibold uppercase text-gray-400 dark:text-gray-500">Heading</div>
108
+ <div class="text-gray-800 dark:text-gray-100">{{n-a position.heading position.bearing}}</div>
109
+ </div>
110
+ <div>
111
+ <div class="font-semibold uppercase text-gray-400 dark:text-gray-500">Altitude</div>
112
+ <div class="text-gray-800 dark:text-gray-100">{{n-a position.altitude}}</div>
113
+ </div>
114
+ </div>
115
+ </div>
116
+ {{/each}}
117
+ </div>
118
+
119
+ <div class="border-t border-gray-200 dark:border-gray-700">
120
+ <PositionsReplay @resource={{this.vehicle}} @mapHeight={{180}} class="max-h-[360px] overflow-hidden" />
121
+ </div>
122
+ <Spacer @height="150px" />
123
+ {{else}}
124
+ <div class="p-3">
125
+ <Telematic::TabEmptyState
126
+ @tone="info"
127
+ @icon="location-dot"
128
+ @title="No recent positions"
129
+ @message="This attached vehicle does not have recent position history available yet."
130
+ @primaryText="Locate Vehicle"
131
+ @primaryIcon="location-dot"
132
+ @primaryAction={{this.locateVehicle}}
133
+ />
134
+ </div>
135
+ {{/if}}
136
+ </section>
73
137
  {{else}}
74
138
  <Telematic::TabEmptyState
75
139
  @tone="warning"
@@ -0,0 +1,40 @@
1
+ <div class="h-full overflow-y-auto bg-gray-50 dark:bg-gray-900">
2
+ <section class="sticky top-0 z-40 border-b border-gray-200 bg-white shadow-sm dark:border-gray-700 dark:bg-gray-900">
3
+ <div class="mx-auto flex w-full max-w-7xl flex-col gap-3 px-4 py-3 md:px-6 lg:flex-row lg:items-start lg:justify-between">
4
+ <div class="min-w-0 flex-1">
5
+ <div class="flex flex-wrap items-center gap-2">
6
+ <h1 class="truncate text-lg font-bold text-gray-900 dark:text-gray-100">{{smart-humanize this.event.event_type}}</h1>
7
+ <Badge @status={{this.event.severity}} @hideStatusDot={{true}}>{{smart-humanize this.event.severity}}</Badge>
8
+ <Badge @status={{this.processedStatus}} @hideStatusDot={{true}}>{{this.processedLabel}}</Badge>
9
+ </div>
10
+ <div class="mt-1 flex flex-wrap gap-2 text-xs text-gray-500 dark:text-gray-300">
11
+ <span>{{n-a this.event.public_id}}</span>
12
+ <span>Device: {{n-a (or this.event.device_name this.event.device_id this.event.ident)}}</span>
13
+ <span>Provider: {{n-a (or this.event.telematic_name this.event.provider)}}</span>
14
+ <span>Occurred: {{n-a (format-date-fns this.event.occurred_at "dd MMM yyyy, HH:mm")}}</span>
15
+ </div>
16
+ </div>
17
+
18
+ <div class="flex shrink-0 flex-row flex-nowrap items-center gap-2 whitespace-nowrap">
19
+ <Button @icon="arrow-left" @text="Back" @size="xs" @wrapperClass="shrink-0" @onClick={{this.goBack}} />
20
+ <Button @icon="refresh" @text="Refresh" @size="xs" @wrapperClass="shrink-0" @onClick={{this.refresh}} />
21
+ {{#unless this.isProcessed}}
22
+ <Button
23
+ @icon="check"
24
+ @text="Mark Processed"
25
+ @type="primary"
26
+ @size="xs"
27
+ @wrapperClass="shrink-0"
28
+ @isLoading={{this.isMarkingProcessed}}
29
+ @disabled={{this.isMarkingProcessed}}
30
+ @onClick={{this.markProcessed}}
31
+ />
32
+ {{/unless}}
33
+ </div>
34
+ </div>
35
+ </section>
36
+
37
+ <section class="mx-auto w-full max-w-7xl">
38
+ <DeviceEvent::Details @resource={{this.event}} />
39
+ </section>
40
+ </div>
@@ -1,7 +1,7 @@
1
1
  <Layout::Resource::Panel
2
2
  @resource={{@model}}
3
3
  @controller={{this}}
4
- @headerTitle={{or @model.name}}
4
+ @headerComponent={{component "vendor/panel-header"}}
5
5
  @actionButtons={{this.actionButtons}}
6
6
  @onPressCancel={{transition-to "management.vendors.index"}}
7
7
  @onOverlayReady={{fn (mut this.overlay)}}
@@ -11,4 +11,4 @@
11
11
  <TabNavigation @tabs={{this.tabs}} @contentClass="scrollable" @tablistClass="pl-2">
12
12
  {{outlet}}
13
13
  </TabNavigation>
14
- </Layout::Resource::Panel>
14
+ </Layout::Resource::Panel>
@@ -14,15 +14,12 @@
14
14
  <div class="container mx-auto h-screen">
15
15
  <div class="max-w-3xl my-10 mx-auto space-y-6">
16
16
 
17
- {{!-- ─── Map Provider Panel ──────────────────────────────────────────── --}}
17
+ {{! ─── Map Provider Panel ──────────────────────────────────────────── }}
18
18
  <ContentPanel @title={{t "settings.map.configure-map"}} @open={{true}} @wrapperClass="bordered-classic">
19
19
  {{#if this.settingsLoaded}}
20
20
 
21
- {{!-- Provider selector --}}
22
- <InputGroup
23
- @name={{t "settings.map.map-provider"}}
24
- @helpText={{t "settings.map.map-provider-help-text"}}
25
- >
21
+ {{! Provider selector }}
22
+ <InputGroup @name={{t "settings.map.map-provider"}} @helpText={{t "settings.map.map-provider-help-text"}}>
26
23
  <Select
27
24
  class="w-full flex-1"
28
25
  @value={{this.mapProvider}}
@@ -34,15 +31,47 @@
34
31
  />
35
32
  </InputGroup>
36
33
 
34
+ {{#if this.isGoogleMapsSelected}}
35
+ <InputGroup @name={{t "settings.map.google-maps-map-type"}} @helpText={{t "settings.map.google-maps-map-type-help-text"}}>
36
+ <Select
37
+ class="w-full flex-1"
38
+ @value={{this.googleMapsMapType}}
39
+ @options={{this.googleMapsMapTypeOptions}}
40
+ @optionLabel="label"
41
+ @optionValue="value"
42
+ @placeholder={{t "settings.map.select-map-type"}}
43
+ @onSelect={{fn (mut this.googleMapsMapType)}}
44
+ />
45
+ </InputGroup>
46
+
47
+ <InputGroup>
48
+ <Toggle
49
+ @label={{t "settings.map.show-traffic-layer"}}
50
+ @isToggled={{this.showGoogleMapsTrafficLayer}}
51
+ @onToggle={{fn (mut this.showGoogleMapsTrafficLayer)}}
52
+ @helpText={{t "settings.map.show-traffic-layer-help-text"}}
53
+ />
54
+ </InputGroup>
55
+
56
+ <InputGroup>
57
+ <Toggle
58
+ @label={{t "settings.map.show-transit-layer"}}
59
+ @isToggled={{this.showGoogleMapsTransitLayer}}
60
+ @onToggle={{fn (mut this.showGoogleMapsTransitLayer)}}
61
+ @helpText={{t "settings.map.show-transit-layer-help-text"}}
62
+ />
63
+ </InputGroup>
64
+ {{/if}}
65
+
37
66
  {{else}}
38
- {{!-- Loading state --}}
67
+ {{! Loading state }}
39
68
  <div class="flex items-center justify-center py-8">
40
69
  <Spinner />
41
70
  </div>
42
71
  {{/if}}
43
72
  </ContentPanel>
44
73
 
45
- {{!-- Extension point for third-party panels --}}
74
+ {{! Extension point for third-party panels }}
46
75
  <RegistryYield @registry="fleet-ops:template:settings:map" as |RegistryComponent|>
47
76
  <RegistryComponent @controller={{this}} />
48
77
  </RegistryYield>
@@ -50,4 +79,4 @@
50
79
  </div>
51
80
  </div>
52
81
  <Spacer @height="600px" />
53
- </Layout::Section::Body>
82
+ </Layout::Section::Body>
@@ -19,6 +19,7 @@
19
19
  <div class="fleetbase-model-select fleetbase-power-select ember-model-select">
20
20
  <PowerSelectMultiple
21
21
  @searchEnabled={{true}}
22
+ @searchField="label"
22
23
  @options={{this.notifiables}}
23
24
  @selected={{get this.notificationSettings (concat (get-notification-key notification.definition notification.name) ".notifiables")}}
24
25
  @onChange={{fn this.onSelectNotifiable notification}}
@@ -35,4 +36,4 @@
35
36
  </div>
36
37
  </div>
37
38
  <Spacer @height="600px" />
38
- </Layout::Section::Body>
39
+ </Layout::Section::Body>
@@ -64,6 +64,7 @@
64
64
  <div class="fleetbase-model-select fleetbase-power-select ember-model-select">
65
65
  <PowerSelectMultiple
66
66
  @searchEnabled={{true}}
67
+ @searchField="label"
67
68
  @options={{this.trackingProviderOptions}}
68
69
  @selected={{this.selectedTrackingFallbackOptions}}
69
70
  @onChange={{this.setTrackingFallbacks}}
@@ -0,0 +1,288 @@
1
+ const connectionStatusOptions = [
2
+ { label: 'Online', value: 'online' },
3
+ { label: 'Recently Offline', value: 'recently_offline' },
4
+ { label: 'Offline', value: 'offline' },
5
+ { label: 'Long Offline', value: 'long_offline' },
6
+ { label: 'Never Connected', value: 'never_connected' },
7
+ ];
8
+
9
+ const attachmentStateOptions = [
10
+ { label: 'Attached', value: 'attached' },
11
+ { label: 'Unattached', value: 'unattached' },
12
+ ];
13
+
14
+ export { connectionStatusOptions, attachmentStateOptions };
15
+
16
+ export default function buildDeviceTableColumns(controller, options = {}) {
17
+ const { showProvider = true, showActions = true, deviceActionMode = 'route', showDeviceStatus = true } = options;
18
+ const viewDevice =
19
+ deviceActionMode === 'panel'
20
+ ? (controller.deviceActions.panel?.view ?? controller.deviceActions.transition?.view)
21
+ : (controller.deviceActions.transition?.view ?? controller.deviceActions.panel?.view);
22
+ const editDevice =
23
+ deviceActionMode === 'panel'
24
+ ? (controller.deviceActions.panel?.edit ?? controller.deviceActions.transition?.edit)
25
+ : (controller.deviceActions.transition?.edit ?? controller.deviceActions.panel?.edit);
26
+
27
+ const columns = [
28
+ {
29
+ sticky: true,
30
+ label: 'Telematic Device',
31
+ valuePath: 'displayName',
32
+ cellComponent: 'cell/device-identity',
33
+ action: viewDevice,
34
+ compact: true,
35
+ permission: 'fleet-ops view device',
36
+ resizable: true,
37
+ sortable: true,
38
+ filterable: true,
39
+ filterParam: 'query',
40
+ filterComponent: 'filter/string',
41
+ showStatus: showDeviceStatus,
42
+ },
43
+ {
44
+ label: 'Connection',
45
+ valuePath: 'connection_status',
46
+ cellComponent: 'table/cell/status',
47
+ resizable: true,
48
+ sortable: false,
49
+ filterable: true,
50
+ filterParam: 'connection_status',
51
+ filterComponent: 'filter/multi-option',
52
+ filterOptions: connectionStatusOptions,
53
+ filterOptionLabel: 'label',
54
+ filterOptionValue: 'value',
55
+ },
56
+ ];
57
+ const providerIdColumn = {
58
+ label: 'Provider ID / IMEI',
59
+ valuePath: 'device_id',
60
+ resizable: true,
61
+ sortable: true,
62
+ filterable: true,
63
+ filterParam: 'device_id',
64
+ filterComponent: 'filter/string',
65
+ };
66
+ const vehicleColumn = {
67
+ label: 'Vehicle',
68
+ valuePath: 'attached_to_name',
69
+ cellComponent: 'cell/vehicle-identity',
70
+ action: async (vehicle) => {
71
+ const resolvedVehicle = vehicle?.loadResource ? ((await vehicle.loadResource()) ?? vehicle) : vehicle;
72
+
73
+ if (resolvedVehicle?.id && controller.vehicleActions?.panel?.view) {
74
+ return controller.vehicleActions.panel.view(resolvedVehicle);
75
+ }
76
+ },
77
+ compact: true,
78
+ permission: 'fleet-ops view vehicle',
79
+ showStatusBadge: true,
80
+ emptyText: '-',
81
+ resourcePath: (device) => {
82
+ const attachableType = `${device?.attachable_type ?? ''}`.toLowerCase();
83
+
84
+ if (!device?.attachable_uuid || (attachableType && !attachableType.includes('vehicle'))) {
85
+ return null;
86
+ }
87
+
88
+ return (
89
+ device.attachable ?? {
90
+ id: device.attachable_uuid,
91
+ displayName: device.attached_to_name,
92
+ display_name: device.attached_to_name,
93
+ name: device.attached_to_name,
94
+ public_id: device.attachable_uuid,
95
+ vehicle_number: device.plate_number ?? device.call_sign ?? device.vehicle_number ?? device.attachable_uuid,
96
+ loadResource: () => controller.resolveAttachedVehicle?.(device),
97
+ }
98
+ );
99
+ },
100
+ resizable: true,
101
+ sortable: false,
102
+ filterable: true,
103
+ filterParam: 'vehicle',
104
+ filterComponent: 'filter/model',
105
+ filterComponentPlaceholder: 'Select vehicle',
106
+ model: 'vehicle',
107
+ modelNamePath: 'displayName',
108
+ };
109
+
110
+ if (showProvider) {
111
+ columns.push({
112
+ label: 'Telematic Provider',
113
+ valuePath: 'telematic_name',
114
+ cellComponent: 'cell/telematic-provider',
115
+ compact: true,
116
+ action: controller.openTelematic,
117
+ permission: 'fleet-ops view telematic',
118
+ resizable: true,
119
+ sortable: false,
120
+ filterable: true,
121
+ filterParam: 'telematic',
122
+ filterComponent: 'filter/model',
123
+ filterComponentPlaceholder: 'Select telematic',
124
+ model: 'telematic',
125
+ });
126
+ columns.push(vehicleColumn, providerIdColumn);
127
+ } else {
128
+ columns.push(providerIdColumn, vehicleColumn);
129
+ }
130
+
131
+ columns.push(
132
+ {
133
+ label: 'Sensors',
134
+ valuePath: 'sensors_count',
135
+ resizable: true,
136
+ sortable: false,
137
+ filterable: false,
138
+ },
139
+ {
140
+ label: 'Last Seen',
141
+ valuePath: 'lastOnlineAt',
142
+ sortParam: 'last_online_at',
143
+ resizable: true,
144
+ sortable: true,
145
+ filterable: true,
146
+ filterParam: 'last_online_at',
147
+ filterComponent: 'filter/date',
148
+ },
149
+ {
150
+ label: 'Provider',
151
+ valuePath: 'provider',
152
+ hidden: true,
153
+ resizable: true,
154
+ sortable: true,
155
+ filterable: true,
156
+ filterParam: 'provider',
157
+ filterComponent: 'filter/string',
158
+ },
159
+ {
160
+ label: 'Type',
161
+ valuePath: 'type',
162
+ hidden: true,
163
+ resizable: true,
164
+ sortable: true,
165
+ filterable: true,
166
+ filterParam: 'type',
167
+ filterComponent: 'filter/multi-option',
168
+ filterOptions: controller.deviceTypeOptions,
169
+ },
170
+ {
171
+ label: 'Serial Number',
172
+ valuePath: 'serial_number',
173
+ hidden: true,
174
+ resizable: true,
175
+ sortable: true,
176
+ filterable: true,
177
+ filterParam: 'serial_number',
178
+ filterComponent: 'filter/string',
179
+ },
180
+ {
181
+ label: controller.intl.t('column.status'),
182
+ valuePath: 'status',
183
+ cellComponent: 'table/cell/status',
184
+ hidden: true,
185
+ resizable: true,
186
+ sortable: true,
187
+ filterable: true,
188
+ filterParam: 'status',
189
+ filterComponent: 'filter/multi-option',
190
+ filterOptions: controller.deviceStatusOptions,
191
+ },
192
+ {
193
+ label: 'Attachment',
194
+ valuePath: 'attachable_uuid',
195
+ hidden: true,
196
+ resizable: true,
197
+ sortable: false,
198
+ filterable: true,
199
+ filterParam: 'attachment_state',
200
+ filterComponent: 'filter/select',
201
+ filterOptions: attachmentStateOptions,
202
+ filterOptionLabel: 'label',
203
+ filterOptionValue: 'value',
204
+ },
205
+ {
206
+ label: controller.intl.t('column.updated-at'),
207
+ valuePath: 'updatedAt',
208
+ sortParam: 'updated_at',
209
+ resizable: true,
210
+ sortable: true,
211
+ hidden: true,
212
+ filterable: true,
213
+ filterComponent: 'filter/date',
214
+ }
215
+ );
216
+
217
+ if (showActions) {
218
+ columns.push({
219
+ label: '',
220
+ cellComponent: 'table/cell/dropdown',
221
+ ddButtonText: false,
222
+ ddButtonIcon: 'ellipsis-h',
223
+ ddButtonIconPrefix: 'fas',
224
+ ddMenuLabel: controller.intl.t('common.resource-actions', { resource: controller.intl.t('resource.device') }),
225
+ cellClassNames: 'overflow-visible align-middle',
226
+ wrapperClass: 'flex items-center justify-end mx-2',
227
+ sticky: 'right',
228
+ width: 60,
229
+ actions: [
230
+ {
231
+ label: controller.intl.t('common.view-resource', { resource: controller.intl.t('resource.device') }),
232
+ fn: viewDevice,
233
+ permission: 'fleet-ops view device',
234
+ },
235
+ {
236
+ label: controller.intl.t('common.edit-resource', { resource: controller.intl.t('resource.device') }),
237
+ fn: editDevice,
238
+ permission: 'fleet-ops update device',
239
+ },
240
+ {
241
+ label: 'Attach or change vehicle',
242
+ fn: controller.openAttachDeviceModal ?? controller.deviceActions.attachToVehicle,
243
+ permission: 'fleet-ops update device',
244
+ },
245
+ {
246
+ label: controller.intl.t('device.actions.detach-from-vehicle'),
247
+ fn: controller.deviceActions.detachFromVehicle,
248
+ permission: 'fleet-ops update device',
249
+ isVisible: controller.hasAttachedVehicle,
250
+ },
251
+ {
252
+ separator: true,
253
+ isVisible: controller.hasAttachedVehicle,
254
+ },
255
+ {
256
+ label: 'View attached vehicle',
257
+ fn: controller.viewAttachedVehicle,
258
+ permission: 'fleet-ops view vehicle',
259
+ isVisible: controller.hasAttachedVehicle,
260
+ },
261
+ {
262
+ label: 'Locate attached vehicle on map',
263
+ fn: controller.locateAttachedVehicle,
264
+ permission: 'fleet-ops view vehicle',
265
+ isVisible: controller.hasAttachedVehicle,
266
+ },
267
+ ...(controller.openDeviceEvents
268
+ ? [
269
+ {
270
+ separator: true,
271
+ },
272
+ {
273
+ label: 'Review recent events',
274
+ fn: controller.openDeviceEvents,
275
+ permission: 'fleet-ops view device-event',
276
+ },
277
+ ]
278
+ : []),
279
+ ],
280
+ sortable: false,
281
+ filterable: false,
282
+ resizable: false,
283
+ searchable: false,
284
+ });
285
+ }
286
+
287
+ return columns;
288
+ }
@@ -0,0 +1,20 @@
1
+ import { get } from '@ember/object';
2
+
3
+ export function resolveIdentityCellResource(args) {
4
+ const column = args.column ?? {};
5
+ const resourcePath = column.resourcePath;
6
+
7
+ if (typeof resourcePath === 'function') {
8
+ return resourcePath(args.row, args.value, column) ?? null;
9
+ }
10
+
11
+ if (typeof resourcePath === 'string') {
12
+ return get(args.row, resourcePath) ?? null;
13
+ }
14
+
15
+ if (args.value && typeof args.value === 'object') {
16
+ return args.value;
17
+ }
18
+
19
+ return args.row;
20
+ }