@fleetbase/fleetops-engine 0.6.53 → 0.6.55

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 (212) hide show
  1. package/addon/components/admin/routing-settings.hbs +2 -1
  2. package/addon/components/cell/attached-vehicle.hbs +10 -0
  3. package/addon/components/cell/attached-vehicle.js +58 -0
  4. package/addon/components/cell/device-identity.hbs +24 -0
  5. package/addon/components/cell/device-identity.js +129 -0
  6. package/addon/components/cell/driver-identity.hbs +33 -0
  7. package/addon/components/cell/driver-identity.js +143 -0
  8. package/addon/components/cell/equipment-identity.hbs +5 -0
  9. package/addon/components/cell/equipment-identity.js +48 -0
  10. package/addon/components/cell/part-identity.hbs +5 -0
  11. package/addon/components/cell/part-identity.js +63 -0
  12. package/addon/components/cell/telematic-device.hbs +38 -0
  13. package/addon/components/cell/telematic-device.js +44 -0
  14. package/addon/components/cell/telematic-provider.hbs +33 -16
  15. package/addon/components/cell/telematic-provider.js +50 -8
  16. package/addon/components/cell/vehicle-identity.hbs +33 -0
  17. package/addon/components/cell/vehicle-identity.js +138 -0
  18. package/addon/components/device/details.hbs +106 -135
  19. package/addon/components/device/details.js +174 -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 +46 -21
  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/equipment/form.js +8 -1
  31. package/addon/components/layout/fleet-ops-sidebar/operations-monitor.js +76 -11
  32. package/addon/components/layout/fleet-ops-sidebar.hbs +1 -1
  33. package/addon/components/layout/fleet-ops-sidebar.js +101 -17
  34. package/addon/components/map/drawer/device-event-listing.js +2 -0
  35. package/addon/components/map/drawer/driver-listing.js +2 -0
  36. package/addon/components/map/drawer/place-listing.js +2 -0
  37. package/addon/components/map/drawer/position-listing.hbs +11 -6
  38. package/addon/components/map/drawer/position-listing.js +17 -4
  39. package/addon/components/map/drawer/vehicle-listing.js +2 -0
  40. package/addon/components/order/form/details.hbs +1 -43
  41. package/addon/components/order/form/details.js +18 -53
  42. package/addon/components/order/form/orchestrator-constraints.hbs +40 -0
  43. package/addon/components/order/form/orchestrator-constraints.js +61 -0
  44. package/addon/components/order/form/payload.js +48 -2
  45. package/addon/components/order/form/route.js +15 -0
  46. package/addon/components/order/form/service-rate.hbs +62 -11
  47. package/addon/components/order/form/service-rate.js +124 -2
  48. package/addon/components/order/form.hbs +2 -0
  49. package/addon/components/sensor/panel-header.hbs +47 -15
  50. package/addon/components/sensor/panel-header.js +59 -1
  51. package/addon/components/table/cell/dropdown/action-item.hbs +31 -0
  52. package/addon/components/table/cell/dropdown/action-item.js +62 -0
  53. package/addon/components/table/cell/dropdown.hbs +28 -0
  54. package/addon/components/table/cell/dropdown.js +91 -0
  55. package/addon/components/telematic/details.hbs +7 -1
  56. package/addon/components/telematic/form.js +16 -8
  57. package/addon/components/vendor/panel-header.hbs +82 -0
  58. package/addon/components/vendor/panel-header.js +48 -0
  59. package/addon/components/work-order/details.hbs +5 -1
  60. package/addon/components/work-order/form.hbs +26 -4
  61. package/addon/components/work-order/form.js +5 -1
  62. package/addon/controllers/connectivity/devices/index/details/events.js +244 -1
  63. package/addon/controllers/connectivity/devices/index/details/sensors.js +199 -0
  64. package/addon/controllers/connectivity/devices/index/details/vehicle.js +122 -0
  65. package/addon/controllers/connectivity/devices/index/details.js +12 -0
  66. package/addon/controllers/connectivity/devices/index.js +144 -129
  67. package/addon/controllers/connectivity/events/details.js +3 -0
  68. package/addon/controllers/connectivity/events/index.js +187 -11
  69. package/addon/controllers/connectivity/sensors/index.js +156 -14
  70. package/addon/controllers/connectivity/telematics/details/attachments.js +220 -54
  71. package/addon/controllers/connectivity/telematics/details/devices.js +170 -129
  72. package/addon/controllers/connectivity/telematics/details/events.js +39 -11
  73. package/addon/controllers/connectivity/telematics/details/sensors.js +39 -19
  74. package/addon/controllers/maintenance/equipment/index.js +1 -2
  75. package/addon/controllers/maintenance/parts/index.js +1 -2
  76. package/addon/controllers/maintenance/work-orders/index.js +18 -1
  77. package/addon/controllers/management/drivers/index.js +82 -42
  78. package/addon/controllers/management/fuel-reports/index.js +63 -4
  79. package/addon/controllers/management/issues/index.js +63 -4
  80. package/addon/controllers/management/vehicles/index.js +33 -6
  81. package/addon/routes/connectivity/devices/index/details/events.js +37 -1
  82. package/addon/routes/connectivity/devices/index/details/sensors.js +35 -0
  83. package/addon/routes/connectivity/devices/index/details/vehicle.js +29 -0
  84. package/addon/routes/connectivity/devices/index.js +7 -0
  85. package/addon/routes/connectivity/events/details.js +3 -0
  86. package/addon/routes/connectivity/events/index.js +6 -2
  87. package/addon/routes/connectivity/sensors/index.js +7 -2
  88. package/addon/routes/connectivity/telematics/details/attachments.js +72 -9
  89. package/addon/routes/connectivity/telematics/details/devices.js +6 -0
  90. package/addon/routes/connectivity/telematics/details/sensors.js +1 -0
  91. package/addon/routes/connectivity/telematics/details.js +1 -1
  92. package/addon/routes/connectivity/telematics/edit.js +1 -1
  93. package/addon/routes/maintenance/work-orders/index.js +1 -0
  94. package/addon/routes.js +2 -0
  95. package/addon/services/device-actions.js +30 -0
  96. package/addon/services/device-event-actions.js +1 -1
  97. package/addon/services/equipment-actions.js +1 -0
  98. package/addon/services/order-creation.js +54 -1
  99. package/addon/services/order-validation.js +3 -2
  100. package/addon/services/sensor-actions.js +3 -0
  101. package/addon/services/service-rate-actions.js +40 -1
  102. package/addon/services/vehicle-actions.js +24 -12
  103. package/addon/services/vendor-actions.js +3 -0
  104. package/addon/styles/fleetops-engine.css +28 -4
  105. package/addon/templates/connectivity/devices/index/details/events.hbs +85 -1
  106. package/addon/templates/connectivity/devices/index/details/sensors.hbs +73 -0
  107. package/addon/templates/connectivity/devices/index/details/vehicle.hbs +147 -0
  108. package/addon/templates/connectivity/telematics/details/attachments.hbs +198 -121
  109. package/addon/templates/connectivity/telematics/details/devices.hbs +4 -1
  110. package/addon/templates/connectivity/telematics/details/events.hbs +4 -1
  111. package/addon/templates/connectivity/telematics/details/sensors.hbs +3 -0
  112. package/addon/templates/management/vehicles/index/edit.hbs +1 -1
  113. package/addon/templates/management/vendors/index/details.hbs +2 -2
  114. package/addon/templates/settings/notifications.hbs +2 -1
  115. package/addon/templates/settings/routing.hbs +1 -0
  116. package/addon/utils/device-table-columns.js +288 -0
  117. package/addon/utils/fleet-ops-options.js +38 -0
  118. package/addon/utils/identity-cell-resource.js +20 -0
  119. package/addon/utils/map-drawer-dropdown-position.js +47 -0
  120. package/addon/utils/trackable-option.js +93 -0
  121. package/app/components/cell/attached-vehicle.js +1 -0
  122. package/app/components/cell/device-identity.js +1 -0
  123. package/app/components/cell/driver-identity.js +1 -0
  124. package/app/components/cell/equipment-identity.js +1 -0
  125. package/app/components/cell/part-identity.js +1 -0
  126. package/app/components/cell/telematic-device.js +1 -0
  127. package/app/components/cell/vehicle-identity.js +1 -0
  128. package/app/components/device/panel-tabs/events.hbs +30 -0
  129. package/app/components/device/panel-tabs/events.js +128 -0
  130. package/app/components/device/panel-tabs/sensors.hbs +30 -0
  131. package/app/components/device/panel-tabs/sensors.js +93 -0
  132. package/app/components/device/panel-tabs/vehicle.hbs +68 -0
  133. package/app/components/device/panel-tabs/vehicle.js +90 -0
  134. package/app/components/order/form/orchestrator-constraints.js +1 -0
  135. package/app/components/table/cell/dropdown/action-item.js +1 -0
  136. package/app/components/table/cell/dropdown.js +1 -0
  137. package/app/components/vendor/panel-header.js +1 -0
  138. package/app/controllers/connectivity/devices/index/details/sensors.js +1 -0
  139. package/app/controllers/connectivity/devices/index/details/vehicle.js +1 -0
  140. package/app/controllers/connectivity/events/{index/details.js → details.js} +1 -1
  141. package/app/routes/connectivity/devices/index/details/sensors.js +1 -0
  142. package/app/routes/connectivity/devices/index/details/vehicle.js +1 -0
  143. package/app/routes/connectivity/events/{index/details.js → details.js} +1 -1
  144. package/app/templates/connectivity/devices/index/details/sensors.js +1 -0
  145. package/app/templates/connectivity/devices/index/details/vehicle.js +1 -0
  146. package/app/templates/connectivity/events/{index/details.js → details.js} +1 -1
  147. package/app/utils/map-drawer-dropdown-position.js +1 -0
  148. package/app/utils/trackable-option.js +1 -0
  149. package/composer.json +1 -1
  150. package/extension.json +1 -1
  151. package/package.json +4 -4
  152. package/server/migrations/2025_08_28_054930_create_work_orders_table.php +1 -0
  153. package/server/migrations/2026_06_18_000001_add_category_to_work_orders_table.php +32 -0
  154. package/server/seeders/Testing/MaintenanceSeeder.php +5 -4
  155. package/server/src/Console/Commands/ProcessMaintenanceTriggers.php +1 -0
  156. package/server/src/Console/Commands/SyncTelematics.php +89 -0
  157. package/server/src/Exceptions/TelematicProviderException.php +13 -2
  158. package/server/src/Http/Controllers/Api/v1/OrderController.php +6 -21
  159. package/server/src/Http/Controllers/Internal/v1/DeviceController.php +55 -1
  160. package/server/src/Http/Controllers/Internal/v1/DeviceEventController.php +17 -6
  161. package/server/src/Http/Controllers/Internal/v1/FleetController.php +9 -0
  162. package/server/src/Http/Controllers/Internal/v1/LiveController.php +178 -1
  163. package/server/src/Http/Controllers/Internal/v1/MaintenanceScheduleController.php +1 -0
  164. package/server/src/Http/Controllers/Internal/v1/OrderController.php +4 -32
  165. package/server/src/Http/Controllers/Internal/v1/SearchController.php +2 -2
  166. package/server/src/Http/Controllers/Internal/v1/ServiceQuoteController.php +4 -4
  167. package/server/src/Http/Controllers/Internal/v1/ServiceRateController.php +82 -13
  168. package/server/src/Http/Filter/DeviceEventFilter.php +65 -0
  169. package/server/src/Http/Filter/DeviceFilter.php +80 -0
  170. package/server/src/Http/Filter/SensorFilter.php +125 -0
  171. package/server/src/Http/Filter/VehicleFilter.php +13 -0
  172. package/server/src/Http/Requests/CreateDriverRequest.php +14 -13
  173. package/server/src/Http/Requests/Internal/CreateDriverRequest.php +1 -0
  174. package/server/src/Http/Resources/v1/Driver.php +11 -2
  175. package/server/src/Http/Resources/v1/Index/Vehicle.php +19 -0
  176. package/server/src/Http/Resources/v1/WorkOrder.php +1 -0
  177. package/server/src/Jobs/FinalizeApiOrderCreation.php +47 -0
  178. package/server/src/Jobs/FinalizeInternalOrderCreation.php +35 -0
  179. package/server/src/Jobs/NotifyBulkAssignedDriver.php +47 -0
  180. package/server/src/Jobs/SyncTelematicDevicesJob.php +180 -123
  181. package/server/src/Models/Device.php +17 -1
  182. package/server/src/Models/DeviceEvent.php +82 -1
  183. package/server/src/Models/Driver.php +16 -0
  184. package/server/src/Models/Equipment.php +55 -0
  185. package/server/src/Models/Sensor.php +15 -2
  186. package/server/src/Models/Telematic.php +1 -1
  187. package/server/src/Models/Vehicle.php +5 -1
  188. package/server/src/Models/WorkOrder.php +5 -2
  189. package/server/src/Observers/DriverObserver.php +3 -3
  190. package/server/src/Observers/FleetObserver.php +23 -0
  191. package/server/src/Observers/VehicleObserver.php +3 -3
  192. package/server/src/Providers/FleetOpsServiceProvider.php +2 -0
  193. package/server/src/Support/LiveCacheService.php +1 -1
  194. package/server/src/Support/Telematics/Providers/AfaqyProvider.php +143 -20
  195. package/server/src/Support/Telematics/Providers/FlespiProvider.php +89 -15
  196. package/server/src/Support/Telematics/Providers/GeotabProvider.php +123 -35
  197. package/server/src/Support/Telematics/Providers/SafeeProvider.php +70 -5
  198. package/server/src/Support/Telematics/Providers/SamsaraProvider.php +122 -14
  199. package/server/src/Support/Telematics/TelematicService.php +330 -30
  200. package/server/src/routes.php +1 -1
  201. package/server/tests/DeviceFilterTest.php +45 -0
  202. package/server/tests/DriverVehicleStatusDefaultTest.php +19 -2
  203. package/server/tests/EquipmentAssignmentMorphTypeTest.php +35 -0
  204. package/server/tests/OperationsMonitorEndpointTest.php +38 -0
  205. package/server/tests/OrderQueuedClosureRegressionTest.php +36 -0
  206. package/server/tests/ServiceRateRouteValidationTest.php +101 -0
  207. package/server/tests/TelematicsHardeningTest.php +598 -6
  208. package/server/tests/VehicleDriverAssignmentTest.php +27 -0
  209. package/translations/en-us.yaml +1 -0
  210. package/addon/controllers/connectivity/events/index/details.js +0 -3
  211. package/addon/routes/connectivity/events/index/details.js +0 -3
  212. /package/addon/templates/connectivity/events/{index/details.hbs → details.hbs} +0 -0
@@ -12,7 +12,13 @@ export default class ConnectivityTelematicsDetailsDevicesRoute extends Route {
12
12
  status: { refreshModel: true },
13
13
  provider: { refreshModel: true },
14
14
  attachment_state: { refreshModel: true },
15
+ vehicle: { refreshModel: true },
16
+ connection_status: { refreshModel: true },
15
17
  device_id: { refreshModel: true },
18
+ type: { refreshModel: true },
19
+ serial_number: { refreshModel: true },
20
+ last_online_at: { refreshModel: true },
21
+ updated_at: { refreshModel: true },
16
22
  };
17
23
 
18
24
  model(params) {
@@ -12,6 +12,7 @@ export default class ConnectivityTelematicsDetailsSensorsRoute extends Route {
12
12
  status: { refreshModel: true },
13
13
  type: { refreshModel: true },
14
14
  device_uuid: { refreshModel: true },
15
+ last_reading_at: { refreshModel: true },
15
16
  };
16
17
 
17
18
  model(params) {
@@ -24,6 +24,6 @@ export default class ConnectivityTelematicsDetailsRoute extends Route {
24
24
  }
25
25
 
26
26
  model({ public_id }) {
27
- return this.store.findRecord('telematic', public_id);
27
+ return this.store.queryRecord('telematic', { public_id, single: true });
28
28
  }
29
29
  }
@@ -24,6 +24,6 @@ export default class ConnectivityTelematicsEditRoute extends Route {
24
24
  }
25
25
 
26
26
  model({ public_id }) {
27
- return this.store.findRecord('telematic', public_id);
27
+ return this.store.queryRecord('telematic', { public_id, single: true });
28
28
  }
29
29
  }
@@ -11,6 +11,7 @@ export default class MaintenanceWorkOrdersIndexRoute extends Route {
11
11
  query: { refreshModel: true },
12
12
  name: { refreshModel: true },
13
13
  public_id: { refreshModel: true },
14
+ category: { refreshModel: true },
14
15
  status: { refreshModel: true },
15
16
  priority: { refreshModel: true },
16
17
  created_at: { refreshModel: true },
package/addon/routes.js CHANGED
@@ -184,6 +184,8 @@ export default buildRoutes(function () {
184
184
  this.route('edit', { path: '/edit/:public_id' });
185
185
  this.route('details', { path: '/:public_id' }, function () {
186
186
  this.route('index', { path: '/' });
187
+ this.route('vehicle');
188
+ this.route('sensors');
187
189
  this.route('events');
188
190
  this.route('virtual', { path: '/:slug' });
189
191
  });
@@ -1,8 +1,10 @@
1
1
  import ResourceActionService, { inject as service } from '@fleetbase/ember-core/services/resource-action';
2
2
  import { action } from '@ember/object';
3
+ import { isArray } from '@ember/array';
3
4
 
4
5
  export default class DeviceActionsService extends ResourceActionService {
5
6
  @service fetch;
7
+ @service('universe/menu-service') menuService;
6
8
 
7
9
  constructor() {
8
10
  super(...arguments);
@@ -37,13 +39,41 @@ export default class DeviceActionsService extends ResourceActionService {
37
39
  });
38
40
  },
39
41
  view: (device) => {
42
+ if (!device?.id) {
43
+ return this.notifications?.warning?.(this.intl.t('common.invalid-resource'));
44
+ }
45
+
46
+ const registeredTabs = this.menuService?.getMenuItems?.('fleet-ops:component:device:details');
47
+
40
48
  return this.resourceContextPanel.open({
41
49
  device,
50
+ header: 'device/panel-header',
42
51
  tabs: [
43
52
  {
53
+ key: 'overview',
54
+ id: 'overview',
44
55
  label: this.intl.t('common.overview'),
45
56
  component: 'device/details',
46
57
  },
58
+ {
59
+ key: 'vehicle',
60
+ id: 'vehicle',
61
+ label: this.intl.t('resource.vehicle'),
62
+ component: 'device/panel-tabs/vehicle',
63
+ },
64
+ {
65
+ key: 'sensors',
66
+ id: 'sensors',
67
+ label: this.intl.t('resource.sensors'),
68
+ component: 'device/panel-tabs/sensors',
69
+ },
70
+ {
71
+ key: 'events',
72
+ id: 'events',
73
+ label: this.intl.t('resource.device-events'),
74
+ component: 'device/panel-tabs/events',
75
+ },
76
+ ...(isArray(registeredTabs) ? registeredTabs.filter((tab) => tab.component || tab.render) : []),
47
77
  ],
48
78
  });
49
79
  },
@@ -11,7 +11,7 @@ export default class DeviceEventActionsService extends ResourceActionService {
11
11
  }
12
12
 
13
13
  transition = {
14
- view: (deviceEvent) => this.transitionTo('connectivity.events.index.details', deviceEvent),
14
+ view: (deviceEvent) => this.transitionTo('connectivity.events.details', deviceEvent),
15
15
  };
16
16
 
17
17
  panel = {
@@ -9,6 +9,7 @@ export default class EquipmentActionsService extends ResourceActionService {
9
9
  super(...arguments);
10
10
  this.initialize('equipment', {
11
11
  defaultAttributes: {
12
+ status: 'available',
12
13
  currency: this.defaultCurrency,
13
14
  },
14
15
  });
@@ -1,12 +1,17 @@
1
1
  import Service, { inject as service } from '@ember/service';
2
2
  import { tracked } from '@glimmer/tracking';
3
3
  import { next } from '@ember/runloop';
4
+ import Evented from '@ember/object/evented';
4
5
 
5
- export default class OrderCreationService extends Service {
6
+ export const SERVICE_QUOTE_REFRESH_REQUESTED = 'service-quote-refresh-requested';
7
+
8
+ export default class OrderCreationService extends Service.extend(Evented) {
6
9
  @service orderActions;
7
10
  @tracked context;
8
11
  @tracked order;
9
12
  @tracked cfManager;
13
+ @tracked serviceQuoteOverrides = {};
14
+ orderValidationRules = {};
10
15
 
11
16
  newOrder(attrs = {}) {
12
17
  const order = this.orderActions.createNewInstance(attrs);
@@ -34,4 +39,52 @@ export default class OrderCreationService extends Service {
34
39
  removeContext(key) {
35
40
  delete this.context[key];
36
41
  }
42
+
43
+ requestServiceQuoteRefresh(reason, order = this.order) {
44
+ this.trigger(SERVICE_QUOTE_REFRESH_REQUESTED, {
45
+ reason,
46
+ order,
47
+ });
48
+ }
49
+
50
+ setServiceQuoteOverride(key, config = {}) {
51
+ this.serviceQuoteOverrides = {
52
+ ...this.serviceQuoteOverrides,
53
+ [key]: {
54
+ ...config,
55
+ key,
56
+ },
57
+ };
58
+ }
59
+
60
+ getServiceQuoteOverride() {
61
+ return Object.values(this.serviceQuoteOverrides ?? {}).find((override) => override?.mode === 'locked') ?? null;
62
+ }
63
+
64
+ clearServiceQuoteOverride(key) {
65
+ const overrides = { ...(this.serviceQuoteOverrides ?? {}) };
66
+ delete overrides[key];
67
+ this.serviceQuoteOverrides = overrides;
68
+ }
69
+
70
+ setOrderValidationRule(key, fn) {
71
+ if (typeof fn !== 'function') {
72
+ return;
73
+ }
74
+
75
+ this.orderValidationRules = {
76
+ ...(this.orderValidationRules ?? {}),
77
+ [key]: fn,
78
+ };
79
+ }
80
+
81
+ clearOrderValidationRule(key) {
82
+ const rules = { ...(this.orderValidationRules ?? {}) };
83
+ delete rules[key];
84
+ this.orderValidationRules = rules;
85
+ }
86
+
87
+ validateOrderRules(order, cfManager = null) {
88
+ return Object.values(this.orderValidationRules ?? {}).every((fn) => fn(order, cfManager) !== false);
89
+ }
37
90
  }
@@ -24,12 +24,13 @@ export default class OrderValidationService extends Service {
24
24
  const hasPickup = isNotEmpty(order.payload.pickup);
25
25
  const hasDropoff = isNotEmpty(order.payload.dropoff);
26
26
  const hasValidCustomFields = cfManager ? this.isCustomFieldsValid(cfManager) : true;
27
+ const hasValidExtensionRules = this.orderCreation.validateOrderRules(order, cfManager);
27
28
 
28
29
  if (hasWaypoints) {
29
- return hasOrderConfig && hasOrderType && hasValidCustomFields;
30
+ return hasOrderConfig && hasOrderType && hasValidCustomFields && hasValidExtensionRules;
30
31
  }
31
32
 
32
- return hasPickup && hasDropoff && hasOrderConfig && hasOrderType && hasValidCustomFields;
33
+ return hasPickup && hasDropoff && hasOrderConfig && hasOrderType && hasValidCustomFields && hasValidExtensionRules;
33
34
  }
34
35
 
35
36
  validationFails(order, cfManager) {
@@ -36,8 +36,11 @@ export default class SensorActionsService extends ResourceActionService {
36
36
  view: (sensor) => {
37
37
  return this.resourceContextPanel.open({
38
38
  sensor,
39
+ header: 'sensor/panel-header',
39
40
  tabs: [
40
41
  {
42
+ key: 'overview',
43
+ id: 'overview',
41
44
  label: this.intl.t('common.overview'),
42
45
  component: 'sensor/details',
43
46
  },
@@ -96,8 +96,15 @@ export default class ServiceRateActionsService extends ResourceActionService {
96
96
  };
97
97
 
98
98
  @task *queryServiceRatesForOrder(order, params = {}) {
99
+ const payloadCoordinates = order.payload?.payloadCoordinates ?? [];
100
+ const routeCoordinates = payloadCoordinates.map((coordinate) => this.#serializeRouteCoordinate(coordinate)).filter(Boolean);
101
+
102
+ if (routeCoordinates.length < 2) {
103
+ return [];
104
+ }
105
+
99
106
  const queryParams = {
100
- coordinates: order.payload.payloadCoordinates.join(';'),
107
+ coordinates: routeCoordinates.join(';'),
101
108
  facilitator: order.facilitator?.get('isIntegratedVendor') ? order.facilitator.get('public_id') : null,
102
109
  service_type: order.order_config?.get('key'),
103
110
  ...params,
@@ -117,6 +124,37 @@ export default class ServiceRateActionsService extends ResourceActionService {
117
124
  }
118
125
  }
119
126
 
127
+ #serializeRouteCoordinate(coordinate) {
128
+ if (Array.isArray(coordinate)) {
129
+ const [longitude, latitude] = coordinate;
130
+
131
+ if (!this.#isValidCoordinate(latitude) || !this.#isValidCoordinate(longitude)) {
132
+ return null;
133
+ }
134
+
135
+ return `${latitude},${longitude}`;
136
+ }
137
+
138
+ if (typeof coordinate === 'string') {
139
+ const parts = coordinate
140
+ .split(',')
141
+ .map((part) => part.trim())
142
+ .filter(Boolean);
143
+
144
+ if (parts.length !== 2 || !parts.every((part) => this.#isValidCoordinate(part))) {
145
+ return null;
146
+ }
147
+
148
+ return parts.join(',');
149
+ }
150
+
151
+ return null;
152
+ }
153
+
154
+ #isValidCoordinate(value) {
155
+ return value !== null && value !== undefined && value !== '' && Number.isFinite(Number(value));
156
+ }
157
+
120
158
  @task *getServiceQuotes(serviceRate, order) {
121
159
  if (order.payload.payloadCoordinates.length < 2) return;
122
160
 
@@ -134,6 +172,7 @@ export default class ServiceRateActionsService extends ResourceActionService {
134
172
  scheduled_at: order.scheduled_at,
135
173
  is_route_optimized: order.optimized,
136
174
  service: serviceRate.id,
175
+ meta: order.meta,
137
176
  });
138
177
 
139
178
  return serviceQuotes;
@@ -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
  },
@@ -235,8 +235,6 @@ body[data-theme='dark'] .fleetops-connectivity-kpi-accent-rose .fleetops-connect
235
235
 
236
236
  .fleetops-provider-connections-actions .fleetops-provider-connections-search {
237
237
  min-width: 16rem;
238
- height: 2.25rem;
239
- min-height: 2.25rem;
240
238
  }
241
239
 
242
240
  .fleetops-provider-connections-header {
@@ -257,8 +255,6 @@ body[data-theme='dark'] .fleetops-connectivity-kpi-accent-rose .fleetops-connect
257
255
  .fleetops-provider-connections-actions .fleetops-telematics-action-button,
258
256
  .fleetops-provider-connections-actions button {
259
257
  flex-shrink: 0;
260
- height: 2.25rem;
261
- min-height: 2.25rem;
262
258
  white-space: nowrap;
263
259
  }
264
260
 
@@ -266,6 +262,26 @@ body[data-theme='dark'] .fleetops-connectivity-kpi-accent-rose .fleetops-connect
266
262
  white-space: nowrap;
267
263
  }
268
264
 
265
+ .fleetops-attachments-vehicle-filter-trigger.ember-power-select-trigger {
266
+ display: flex;
267
+ align-items: center;
268
+ min-height: 2rem;
269
+ padding-right: 1.75rem;
270
+ }
271
+
272
+ .fleetops-attachments-vehicle-filter-trigger.ember-power-select-trigger .ember-power-select-placeholder,
273
+ .fleetops-attachments-vehicle-filter-trigger.ember-power-select-trigger .ember-power-select-selected-item {
274
+ min-width: 0;
275
+ overflow: hidden;
276
+ text-overflow: ellipsis;
277
+ white-space: nowrap;
278
+ }
279
+
280
+ .fleetops-attachments-vehicle-filter-trigger.ember-power-select-trigger .ember-power-select-status-icon {
281
+ display: block;
282
+ right: 0.65rem;
283
+ }
284
+
269
285
  .status-badge.long-offline-status-badge > span,
270
286
  .status-badge.long_offline-status-badge > span {
271
287
  background-color: #854d0e;
@@ -278,6 +294,14 @@ body[data-theme='dark'] .fleetops-connectivity-kpi-accent-rose .fleetops-connect
278
294
  color: #fde047;
279
295
  }
280
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
+
281
305
  .fleetops-provider-connections-panel .table-wrapper table tbody tr td,
282
306
  .fleetops-provider-connections-panel .next-table-wrapper table tbody tr td {
283
307
  vertical-align: top;
@@ -1 +1,85 @@
1
- {{outlet}}
1
+ <div class="flex flex-col gap-3 p-3">
2
+ <div class="flex flex-col gap-1">
3
+ <h2 class="text-base font-bold text-gray-900 dark:text-gray-100">Event Review + Triage</h2>
4
+ <p class="text-xs text-gray-500 dark:text-gray-300">Warning signals, diagnostics, and unprocessed telemetry.</p>
5
+ </div>
6
+
7
+ <section class="grid grid-cols-1 gap-2 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-[82px] overflow-hidden rounded-md border bg-white shadow-sm dark:bg-gray-800">
10
+ <div class="flex h-full items-start justify-between gap-2 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-1 text-xl 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}} />
17
+ </div>
18
+ </div>
19
+ </div>
20
+ {{/each}}
21
+ </section>
22
+
23
+ {{#if this.hasHealthyEventState}}
24
+ <Telematic::TabEmptyState
25
+ @tone="success"
26
+ @icon="check"
27
+ @title="No warning or unprocessed events"
28
+ @message="Recent telemetry is reviewed and no warning-level events are currently visible for this device."
29
+ @primaryText="Refresh"
30
+ @primaryIcon="refresh"
31
+ @primaryAction={{this.refresh}}
32
+ />
33
+ {{/if}}
34
+
35
+ <section class="overflow-hidden rounded-md border border-gray-200 bg-white shadow-sm dark:border-gray-700 dark:bg-gray-800">
36
+ <div class="fleetops-provider-connections-header border-b border-gray-200 px-3 py-2 dark:border-gray-700">
37
+ <div>
38
+ <h3 class="text-sm font-bold text-gray-900 dark:text-gray-100">Telemetry Events</h3>
39
+ <p class="mt-1 text-xs text-gray-500 dark:text-gray-300">Filter by type, severity, processed state, and date.</p>
40
+ </div>
41
+ <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">
42
+ <Layout::Resource::TabularActions
43
+ @title="Telemetry Events"
44
+ @searchQuery={{this.events_query}}
45
+ @onSearch={{perform this.searchTask}}
46
+ @columns={{this.columns}}
47
+ @page={{this.events_page}}
48
+ @onPageChange={{fn (mut this.events_page)}}
49
+ @actionButtons={{this.actionButtons}}
50
+ @bulkActions={{this.bulkActions}}
51
+ @controller={{this}}
52
+ @searchInputClass="fleetops-provider-connections-search"
53
+ @filterPickerButtonClass="fleetops-provider-connections-action-button"
54
+ @columnPickerButtonClass="fleetops-provider-connections-action-button"
55
+ @filterPickerSize="sm"
56
+ @columnPickerSize="sm"
57
+ />
58
+ </div>
59
+ </div>
60
+
61
+ <Layout::Resource::Tabular
62
+ @resource="device-event"
63
+ @title="Telemetry Events"
64
+ @data={{@model}}
65
+ @columns={{this.columns}}
66
+ @page={{this.events_page}}
67
+ @onPageChange={{fn (mut this.events_page)}}
68
+ @controller={{this}}
69
+ @withoutHeader={{true}}
70
+ @tableWrapperClass="no-table-extra-spacing"
71
+ @emptyStateComponent={{component
72
+ "table/empty-state"
73
+ variant="compact"
74
+ icon="bolt"
75
+ title="No telemetry events"
76
+ description="Events appear after this device reports provider activity."
77
+ filteredTitle="No events match your filters"
78
+ filteredDescription="Adjust the search, severity, or date filters to review other events."
79
+ primaryText="Refresh"
80
+ primaryIcon="refresh"
81
+ primaryAction=this.refresh
82
+ }}
83
+ />
84
+ </section>
85
+ </div>
@@ -0,0 +1,73 @@
1
+ <div class="flex flex-col gap-3 p-3">
2
+ <div class="flex flex-col gap-1">
3
+ <h2 class="text-base font-bold text-gray-900 dark:text-gray-100">Sensor Inventory + Health</h2>
4
+ <p class="text-xs text-gray-500 dark:text-gray-300">Readings, thresholds, and reporting health for this device.</p>
5
+ </div>
6
+
7
+ <section class="grid grid-cols-1 gap-2 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-[82px] overflow-hidden rounded-md border bg-white shadow-sm dark:bg-gray-800">
10
+ <div class="flex h-full items-start justify-between gap-2 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-1 text-xl 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}} />
17
+ </div>
18
+ </div>
19
+ </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="fleetops-provider-connections-header border-b border-gray-200 px-3 py-2 dark:border-gray-700">
25
+ <div>
26
+ <h3 class="text-sm font-bold text-gray-900 dark:text-gray-100">Device Sensors</h3>
27
+ <p class="mt-1 text-xs text-gray-500 dark:text-gray-300">Filter by type, status, and last reading.</p>
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">
30
+ <Layout::Resource::TabularActions
31
+ @title="Device Sensors"
32
+ @searchQuery={{this.sensors_query}}
33
+ @onSearch={{perform this.searchTask}}
34
+ @columns={{this.columns}}
35
+ @page={{this.sensors_page}}
36
+ @onPageChange={{fn (mut this.sensors_page)}}
37
+ @actionButtons={{this.actionButtons}}
38
+ @bulkActions={{this.bulkActions}}
39
+ @controller={{this}}
40
+ @searchInputClass="fleetops-provider-connections-search"
41
+ @filterPickerButtonClass="fleetops-provider-connections-action-button"
42
+ @columnPickerButtonClass="fleetops-provider-connections-action-button"
43
+ @filterPickerSize="sm"
44
+ @columnPickerSize="sm"
45
+ />
46
+ </div>
47
+ </div>
48
+
49
+ <Layout::Resource::Tabular
50
+ @resource="sensor"
51
+ @title="Device Sensors"
52
+ @data={{@model}}
53
+ @columns={{this.columns}}
54
+ @page={{this.sensors_page}}
55
+ @onPageChange={{fn (mut this.sensors_page)}}
56
+ @controller={{this}}
57
+ @withoutHeader={{true}}
58
+ @tableWrapperClass="no-table-extra-spacing"
59
+ @emptyStateComponent={{component
60
+ "table/empty-state"
61
+ variant="compact"
62
+ icon="gauge-high"
63
+ title="No sensors connected"
64
+ description="Sensors appear here after provider telemetry links them to this device."
65
+ filteredTitle="No sensors match your filters"
66
+ filteredDescription="Adjust the search or filters to find another sensor."
67
+ primaryText="Refresh"
68
+ primaryIcon="refresh"
69
+ primaryAction=this.refresh
70
+ }}
71
+ />
72
+ </section>
73
+ </div>