@fleetbase/fleetops-engine 0.6.19 → 0.6.21

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 (130) hide show
  1. package/addon/components/custom-entity/form.hbs +14 -14
  2. package/addon/components/device/details.hbs +92 -43
  3. package/addon/components/device/form.hbs +108 -60
  4. package/addon/components/device/form.js +36 -8
  5. package/addon/components/device/panel-header.hbs +32 -0
  6. package/addon/components/device/panel-header.js +3 -0
  7. package/addon/components/driver/form.hbs +1 -1
  8. package/addon/components/driver/form.js +49 -47
  9. package/addon/components/entity/form.hbs +7 -5
  10. package/addon/components/layout/fleet-ops-sidebar.js +12 -12
  11. package/addon/components/map/drawer/device-event-listing.hbs +58 -0
  12. package/addon/components/map/drawer/device-event-listing.js +181 -0
  13. package/addon/components/map/drawer/position-listing.hbs +84 -0
  14. package/addon/components/map/drawer/position-listing.js +289 -0
  15. package/addon/components/map/drawer.js +2 -0
  16. package/addon/components/map/leaflet-live-map.hbs +7 -2
  17. package/addon/components/order/details/payload.hbs +6 -4
  18. package/addon/components/order/details/payload.js +2 -0
  19. package/addon/components/order/kanban.hbs +12 -10
  20. package/addon/components/order/kanban.js +27 -3
  21. package/addon/components/order-config-manager/custom-fields.js +1 -1
  22. package/addon/components/positions-replay.hbs +333 -0
  23. package/addon/components/positions-replay.js +372 -0
  24. package/addon/components/sensor/details.hbs +64 -38
  25. package/addon/components/sensor/form.hbs +112 -63
  26. package/addon/components/sensor/form.js +36 -24
  27. package/addon/components/sensor/panel-header.hbs +32 -0
  28. package/addon/components/sensor/panel-header.js +3 -0
  29. package/addon/components/telematic/details.hbs +40 -16
  30. package/addon/components/telematic/form.hbs +63 -64
  31. package/addon/components/telematic/form.js +73 -4
  32. package/addon/components/vehicle/card.hbs +1 -1
  33. package/addon/controllers/analytics/reports/index/edit.js +1 -1
  34. package/addon/controllers/connectivity/devices/index/details.js +22 -1
  35. package/addon/controllers/connectivity/devices/index/edit.js +66 -1
  36. package/addon/controllers/connectivity/devices/index.js +51 -9
  37. package/addon/controllers/connectivity/events/index.js +65 -16
  38. package/addon/controllers/connectivity/sensors/index/details.js +22 -1
  39. package/addon/controllers/connectivity/sensors/index/edit.js +66 -1
  40. package/addon/controllers/connectivity/sensors/index.js +66 -6
  41. package/addon/controllers/connectivity/telematics/index/details.js +22 -1
  42. package/addon/controllers/connectivity/telematics/index/edit.js +66 -1
  43. package/addon/controllers/connectivity/telematics/index.js +20 -11
  44. package/addon/controllers/management/fleets/index/details.js +26 -21
  45. package/addon/controllers/management/fleets/index/edit.js +9 -6
  46. package/addon/controllers/management/vehicles/index/details.js +21 -13
  47. package/addon/controllers/operations/orders/index/new.js +4 -2
  48. package/addon/controllers/operations/orders/index.js +50 -45
  49. package/addon/controllers/settings/custom-fields.js +6 -0
  50. package/addon/helpers/get-fleet-ops-option-label.js +11 -0
  51. package/addon/routes/connectivity/devices/index/details.js +27 -1
  52. package/addon/routes/connectivity/devices/index/edit.js +27 -1
  53. package/addon/routes/connectivity/sensors/index/details.js +27 -1
  54. package/addon/routes/connectivity/sensors/index/edit.js +27 -1
  55. package/addon/routes/connectivity/telematics/index/details.js +27 -1
  56. package/addon/routes/connectivity/telematics/index/edit.js +27 -1
  57. package/addon/routes/management/vehicles/index/details/positions.js +3 -0
  58. package/addon/routes/operations/orders/index.js +0 -3
  59. package/addon/routes.js +1 -0
  60. package/addon/services/movement-tracker.js +81 -30
  61. package/addon/services/order-creation.js +4 -8
  62. package/addon/services/order-validation.js +3 -3
  63. package/addon/styles/fleetops-engine.css +192 -0
  64. package/addon/templates/connectivity/devices/index/details/index.hbs +2 -2
  65. package/addon/templates/connectivity/devices/index/details.hbs +15 -2
  66. package/addon/templates/connectivity/devices/index/edit.hbs +1 -1
  67. package/addon/templates/connectivity/events/index.hbs +1 -1
  68. package/addon/templates/connectivity/sensors/index/details/index.hbs +2 -2
  69. package/addon/templates/connectivity/sensors/index/details.hbs +15 -2
  70. package/addon/templates/connectivity/sensors/index/edit.hbs +1 -1
  71. package/addon/templates/connectivity/telematics/index/details/index.hbs +2 -2
  72. package/addon/templates/connectivity/telematics/index/details.hbs +14 -2
  73. package/addon/templates/connectivity/telematics/index/edit.hbs +1 -1
  74. package/addon/templates/management/vehicles/index/details/positions.hbs +1 -0
  75. package/addon/templates/operations/orders/index.hbs +26 -2
  76. package/addon/utils/fleet-ops-options.js +95 -0
  77. package/addon/utils/setup-customer-portal.js +7 -0
  78. package/app/components/device/panel-header.js +1 -0
  79. package/app/components/map/drawer/device-event-listing.js +1 -0
  80. package/app/components/map/drawer/position-listing.js +1 -0
  81. package/app/components/positions-replay.js +1 -0
  82. package/app/components/sensor/panel-header.js +1 -0
  83. package/app/helpers/get-fleet-ops-option-label.js +1 -0
  84. package/app/routes/management/vehicles/index/details/positions.js +1 -0
  85. package/app/templates/management/vehicles/index/details/positions.js +1 -0
  86. package/composer.json +1 -1
  87. package/extension.json +1 -1
  88. package/package.json +4 -4
  89. package/server/config/telematics.php +111 -0
  90. package/server/migrations/2025_10_27_000001_add_telematics_integration_fields.php +70 -0
  91. package/server/migrations/2025_10_27_171322_fix_device_column_names.php +107 -0
  92. package/server/migrations/2025_10_27_203023_add_company_uuid_to_device_events_table.php +28 -0
  93. package/server/src/Console/Commands/ReplayVehicleLocations.php +225 -0
  94. package/server/src/Contracts/TelematicProviderDescriptor.php +72 -0
  95. package/server/src/Contracts/TelematicProviderInterface.php +119 -0
  96. package/server/src/Exceptions/TelematicProviderException.php +14 -0
  97. package/server/src/Exceptions/TelematicRateLimitExceededException.php +12 -0
  98. package/server/src/Http/Controllers/Api/v1/DriverController.php +24 -14
  99. package/server/src/Http/Controllers/Api/v1/VehicleController.php +27 -7
  100. package/server/src/Http/Controllers/Internal/v1/DeviceController.php +22 -0
  101. package/server/src/Http/Controllers/Internal/v1/OrderController.php +50 -68
  102. package/server/src/Http/Controllers/Internal/v1/PositionController.php +240 -0
  103. package/server/src/Http/Controllers/Internal/v1/SensorController.php +11 -0
  104. package/server/src/Http/Controllers/Internal/v1/TelematicController.php +141 -0
  105. package/server/src/Http/Controllers/Internal/v1/TelematicWebhookController.php +170 -0
  106. package/server/src/Http/Filter/DeviceEventFilter.php +68 -0
  107. package/server/src/Http/Filter/PositionFilter.php +35 -0
  108. package/server/src/Http/Resources/v1/Position.php +44 -0
  109. package/server/src/Jobs/ReplayPositions.php +64 -0
  110. package/server/src/Jobs/SendPositionReplay.php +65 -0
  111. package/server/src/Jobs/SyncTelematicDevicesJob.php +106 -0
  112. package/server/src/Jobs/TestTelematicConnectionJob.php +102 -0
  113. package/server/src/Models/Device.php +72 -10
  114. package/server/src/Models/DeviceEvent.php +7 -0
  115. package/server/src/Models/Driver.php +28 -1
  116. package/server/src/Models/Payload.php +11 -3
  117. package/server/src/Models/Place.php +9 -2
  118. package/server/src/Models/Position.php +17 -17
  119. package/server/src/Models/Sensor.php +78 -13
  120. package/server/src/Models/Telematic.php +116 -6
  121. package/server/src/Models/Vehicle.php +104 -1
  122. package/server/src/Providers/FleetOpsServiceProvider.php +2 -0
  123. package/server/src/Support/Telematics/Providers/AbstractProvider.php +151 -0
  124. package/server/src/Support/Telematics/Providers/FlespiProvider.php +182 -0
  125. package/server/src/Support/Telematics/Providers/GeotabProvider.php +181 -0
  126. package/server/src/Support/Telematics/Providers/SamsaraProvider.php +177 -0
  127. package/server/src/Support/Telematics/TelematicProviderRegistry.php +147 -0
  128. package/server/src/Support/Telematics/TelematicService.php +223 -0
  129. package/server/src/Support/Utils.php +1 -1
  130. package/server/src/routes.php +12 -1
@@ -10,17 +10,19 @@ import LeafletTrackingMarkerComponent from '../components/leaflet-tracking-marke
10
10
  export class EventBuffer {
11
11
  @tracked events = [];
12
12
  @tracked waitTime = 1000 * 3;
13
+ @tracked callback;
13
14
  @tracked intervalId;
14
15
  @tracked model;
15
16
 
16
- constructor(model, waitTime = 1000 * 3) {
17
+ constructor(model, { callback = null, waitTime = 1000 * 3 }) {
17
18
  this.model = model;
19
+ this.callback = callback;
18
20
  this.waitTime = waitTime;
19
21
  }
20
22
 
21
23
  start() {
22
24
  this.intervalId = setInterval(() => {
23
- const bufferReady = this.process.isIdle && this.events.length;
25
+ const bufferReady = this.process.isIdle && this.events.length > 0;
24
26
  if (bufferReady) {
25
27
  this.process.perform();
26
28
  }
@@ -32,11 +34,11 @@ export class EventBuffer {
32
34
  }
33
35
 
34
36
  clear() {
35
- this.events.length = 0;
37
+ this.events = [];
36
38
  }
37
39
 
38
40
  add(event) {
39
- this.events.pushObject(event);
41
+ this.events = [...this.events, event];
40
42
  }
41
43
 
42
44
  removeByIndex(index) {
@@ -44,65 +46,112 @@ export class EventBuffer {
44
46
  }
45
47
 
46
48
  remove(event) {
47
- this.events = this.events.removeObject(event);
49
+ this.events = this.events.filter((e) => e !== event);
48
50
  }
49
51
 
50
52
  @task *process() {
51
53
  debug('Processing movement tracker event buffer.');
54
+
55
+ // Take a snapshot of events to process and clear buffer immediately
56
+ // This prevents losing events that arrive during processing
57
+ const eventsToProcess = [...this.events];
58
+ this.events = []; // Clear immediately to accept new events
59
+
52
60
  // Sort events by created_at
53
- this.events = this.events.sort((a, b) => new Date(a.created_at) - new Date(b.created_at));
61
+ eventsToProcess.sort((a, b) => new Date(a.created_at) - new Date(b.created_at));
62
+ debug(`[MovementTracker EventBuffer processing ${eventsToProcess.length} events]`);
54
63
 
55
64
  // Process sorted events
56
- for (const output of this.events) {
65
+ for (const output of eventsToProcess) {
57
66
  const { event, data } = output;
58
67
 
68
+ // get movingObject marker
69
+ const marker = this.model.leafletLayer || this.model._layer || this.model._marker;
70
+ if (!marker || !marker._map) {
71
+ debug('No marker or marker not on map yet');
72
+ continue;
73
+ }
74
+
59
75
  // log incoming event
60
76
  debug(`${event} - ${data.id} ${data.additionalData?.index ? '#' + data.additionalData?.index : ''} (${output.created_at}) [ ${data.location.coordinates.join(' ')} ]`);
61
77
 
62
- // get movingObject marker
63
- const marker = this.model._layer || this.model._marker;
64
- if (marker) {
65
- if (typeof marker.setRotationAngle === 'function' && data.heading) {
78
+ // GeoJSON -> Leaflet [lat, lng]
79
+ const [lng, lat] = data.location.coordinates;
80
+ const nextLatLng = [lat, lng];
81
+
82
+ // Calc speed
83
+ const map = marker._map;
84
+ const prev = marker.getLatLng();
85
+ const meters = map ? map.distance(prev, nextLatLng) : prev.distanceTo(nextLatLng);
86
+
87
+ // Assume payload speed is m/s; if it's km/h, convert: mps = kmh / 3.6
88
+ let mps = Number.isFinite(data.speed) && data.speed > 0 ? data.speed : null;
89
+
90
+ // Reduce animation duration and clamp between 100ms and 500ms
91
+ // This makes animations faster and prevents long delays
92
+ const durationMs = mps ? Math.max(100, Math.min((meters / mps) * 1000, 500)) : 500;
93
+
94
+ try {
95
+ // Apply rotation if heading is valid
96
+ if (typeof marker.setRotationAngle === 'function' && Number.isFinite(data.heading) && data.heading !== -1) {
66
97
  marker.setRotationAngle(data.heading);
67
98
  }
68
99
 
100
+ // Move marker with animation
69
101
  if (typeof marker.slideTo === 'function') {
70
- marker.slideTo(data.location.coordinates);
102
+ marker.slideTo(nextLatLng, { duration: durationMs });
71
103
  } else {
72
- marker.setLatLng(data.location.coordinates);
104
+ marker.setLatLng(nextLatLng);
73
105
  }
74
106
 
75
- yield timeout(1000);
107
+ if (typeof this.callback === 'function') {
108
+ this.callback(output, { nextLatLng, duration: durationMs, mps });
109
+ }
110
+
111
+ // Wait for animation to complete
112
+ yield timeout(durationMs + 50);
113
+ } catch (err) {
114
+ debug('MovementTracker EventBuffer error: ' + err.message);
76
115
  }
77
116
  }
78
117
 
79
- // Clear the buffer
80
- this.clear();
118
+ // Don't clear here - we already cleared at the start
119
+ debug(`[MovementTracker EventBuffer finished processing ${eventsToProcess.length} events]`);
81
120
  }
82
121
  }
83
122
 
84
123
  export default class MovementTrackerService extends Service {
85
124
  @service socket;
86
125
  @tracked channels = [];
126
+ @tracked buffers = new Map();
87
127
 
88
128
  constructor() {
89
129
  super(...arguments);
90
130
  this.registerTrackingMarker();
91
131
  }
92
132
 
93
- _getOwner(owner = null) {
133
+ #getOwner(owner = null) {
94
134
  return owner ?? window.Fleetbase ?? getOwner(this);
95
135
  }
96
136
 
137
+ #getBuffer(key, model, opts = {}) {
138
+ let buf = this.buffers.get(key);
139
+ if (!buf) {
140
+ buf = new EventBuffer(model, opts);
141
+ buf.start();
142
+ this.buffers.set(key, buf);
143
+ }
144
+ return buf;
145
+ }
146
+
97
147
  registerTrackingMarker(_owner = null) {
98
- const owner = this._getOwner(_owner);
148
+ const owner = this.#getOwner(_owner);
99
149
  const emberLeafletService = owner.lookup('service:ember-leaflet');
100
150
 
101
151
  if (emberLeafletService) {
102
152
  const alreadyRegistered = emberLeafletService.components.find((registeredComponent) => registeredComponent.name === 'leaflet-tracking-marker');
103
- if (alreadyRegistered) {
104
- return;
105
- }
153
+ if (alreadyRegistered) return;
154
+
106
155
  // we then invoke the `registerComponent` method
107
156
  emberLeafletService.registerComponent('leaflet-tracking-marker', {
108
157
  as: 'tracking-marker',
@@ -123,37 +172,39 @@ export default class MovementTrackerService extends Service {
123
172
  });
124
173
  }
125
174
 
126
- async track(model) {
175
+ async track(model, options = {}) {
127
176
  // Create socket instance
128
177
  const socket = this.socket.instance();
129
178
 
130
179
  // Get model type and identifier
131
180
  const type = getModelName(model);
132
181
  const identifier = model.id;
133
- debug(`Tracking movement started for ${type} with id ${identifier}`, model);
182
+
183
+ // Location events to listen for
184
+ const locationEvents = [`${type}.location_changed`, `${type}.simulated_location_changed`, 'position.changed', 'position.simulated'];
134
185
 
135
186
  // Listen on the specific channel
136
- const channelId = `${type}.${identifier}`;
187
+ const channelId = options?.channelId ?? `${type}.${identifier}`;
137
188
  const channel = socket.subscribe(channelId);
138
189
 
190
+ // Debug output
191
+ debug(`Tracking movement started for ${type} with id ${identifier}${options?.channelId ? ' on channel ' + channelId : ''}`, model);
192
+
139
193
  // Track the channel
140
- this.channels.pushObject(channel);
194
+ this.channels = [...this.channels, channel];
141
195
 
142
196
  // Listen to the channel for events
143
197
  await channel.listener('subscribe').once();
144
198
 
145
199
  // Create event buffer for tracking model
146
- const eventBuffer = new EventBuffer(model);
147
-
148
- // Start tracking with event buffer
149
- eventBuffer.start();
200
+ const eventBuffer = this.#getBuffer(channelId, model, options);
150
201
 
151
202
  // Get incoming data and console out
152
203
  (async () => {
153
204
  for await (let output of channel) {
154
205
  const { event } = output;
155
206
 
156
- if (event === `${type}.location_changed` || event === `${type}.simulated_location_changed`) {
207
+ if (locationEvents.includes(event)) {
157
208
  eventBuffer.add(output);
158
209
  debug(`Socket Event : ${event} : Added to EventBuffer : ${JSON.stringify(output)}`);
159
210
  }
@@ -1,6 +1,6 @@
1
1
  import Service, { inject as service } from '@ember/service';
2
2
  import { tracked } from '@glimmer/tracking';
3
- import { later } from '@ember/runloop';
3
+ import { next } from '@ember/runloop';
4
4
 
5
5
  export default class OrderCreationService extends Service {
6
6
  @service orderActions;
@@ -12,13 +12,9 @@ export default class OrderCreationService extends Service {
12
12
  const order = this.orderActions.createNewInstance(attrs);
13
13
  this.order = order;
14
14
 
15
- later(
16
- this,
17
- () => {
18
- this.addContext('order', order);
19
- },
20
- 0
21
- );
15
+ next(() => {
16
+ this.addContext('order', order);
17
+ });
22
18
 
23
19
  return order;
24
20
  }
@@ -23,7 +23,7 @@ export default class OrderValidationService extends Service {
23
23
  const hasWaypoints = order.payload.waypoints.length >= 2;
24
24
  const hasPickup = isNotEmpty(order.payload.pickup);
25
25
  const hasDropoff = isNotEmpty(order.payload.dropoff);
26
- const hasValidCustomFields = cfManager ? this.isCustomFieldsValid(cfManager) : false;
26
+ const hasValidCustomFields = cfManager ? this.isCustomFieldsValid(cfManager) : true;
27
27
 
28
28
  if (hasWaypoints) {
29
29
  return hasOrderConfig && hasOrderType && hasValidCustomFields;
@@ -37,13 +37,13 @@ export default class OrderValidationService extends Service {
37
37
  }
38
38
 
39
39
  validateCustomFields(cfManager) {
40
- if (!cfManager) return false;
40
+ if (!cfManager) return true;
41
41
 
42
42
  return cfManager.validateRequired();
43
43
  }
44
44
 
45
45
  isCustomFieldsValid(cfManager) {
46
- if (!cfManager) return false;
46
+ if (!cfManager) return true;
47
47
 
48
48
  const { isValid } = this.validateCustomFields(cfManager);
49
49
  return isValid;
@@ -1654,3 +1654,195 @@ button.fleetops-btn-xxs,
1654
1654
  padding-top: 0.2rem !important;
1655
1655
  padding-bottom: 0.2rem !important;
1656
1656
  }
1657
+
1658
+ /** css fix for operations index/kanban */
1659
+ main.console-fleet-ops-operations-orders-index-index section.next-view-section {
1660
+ max-width: 100vw;
1661
+ min-width: 0;
1662
+ }
1663
+
1664
+ main.console-fleet-ops-operations-orders-index-index section.next-view-section > .next-view-section-container {
1665
+ display: flex;
1666
+ flex-direction: column;
1667
+ max-width: 100vw;
1668
+ min-width: 0;
1669
+ }
1670
+
1671
+ main.console-fleet-ops-operations-orders-index-index section.next-view-section > .next-view-section-container > .next-view-section-subheader {
1672
+ flex: 0 0 auto;
1673
+ width: 100%;
1674
+ }
1675
+
1676
+ main.console-fleet-ops-operations-orders-index-index section.next-view-section > .next-view-section-container > .next-view-section-body {
1677
+ flex: 1 1 auto;
1678
+ min-width: 0;
1679
+ overflow: auto;
1680
+ }
1681
+
1682
+ main.console-fleet-ops-operations-orders-index-index section.next-view-section > .next-view-section-container > .next-view-section-body > .kanban-board {
1683
+ display: flex;
1684
+ }
1685
+
1686
+ .next-view-section-subheader .next-view-section-subheader-actions .order-board-type-filter.ember-power-select-trigger.ember-basic-dropdown-trigger {
1687
+ height: 2rem;
1688
+ align-items: center;
1689
+ padding-left: 0.5rem;
1690
+ padding-right: 2rem;
1691
+ }
1692
+
1693
+ .positions-replay-component {
1694
+ width: 100%;
1695
+ max-width: 100%;
1696
+ }
1697
+
1698
+ .positions-replay-component .metric-card {
1699
+ padding: 0.75rem;
1700
+ border-radius: 0.5rem;
1701
+ background-color: rgba(0, 0, 0, 2%);
1702
+ transition: all 0.2s ease-in-out;
1703
+ }
1704
+
1705
+ .positions-replay-component .metric-card:hover {
1706
+ background-color: rgba(0, 0, 0, 4%);
1707
+ box-shadow: 0 4px 6px rgba(0, 0, 0, 10%);
1708
+ }
1709
+
1710
+ body[data-theme='dark'] .positions-replay-component .metric-card {
1711
+ background-color: rgba(255, 255, 255, 5%);
1712
+ }
1713
+
1714
+ body[data-theme='dark'] .positions-replay-component .metric-card:hover {
1715
+ background-color: rgba(255, 255, 255, 8%);
1716
+ }
1717
+
1718
+ .positions-replay-component .metric-label {
1719
+ font-size: 0.65rem;
1720
+ text-transform: uppercase;
1721
+ letter-spacing: 0.05em;
1722
+ margin-bottom: 0.25rem;
1723
+ }
1724
+
1725
+ .positions-replay-component .metric-value {
1726
+ font-size: 0.85rem;
1727
+ line-height: 1;
1728
+ }
1729
+
1730
+ .positions-replay-component .positions-replay-map-container {
1731
+ overflow: hidden;
1732
+ height: 100%;
1733
+ width: 100%;
1734
+ }
1735
+
1736
+ .positions-replay-component .replay-controls {
1737
+ display: flex;
1738
+ align-items: center;
1739
+ justify-content: space-between;
1740
+ flex-wrap: wrap;
1741
+ gap: 1rem;
1742
+ }
1743
+
1744
+ .positions-replay-component .speed-control {
1745
+ display: flex;
1746
+ align-items: center;
1747
+ }
1748
+
1749
+ .positions-replay-component .speed-control > select.speed-select {
1750
+ height: 1.7rem;
1751
+ font-size: 0.85rem;
1752
+ line-height: 0.85rem;
1753
+ text-align: center;
1754
+ padding: 0 2rem 0 0.75rem;
1755
+ }
1756
+
1757
+ .positions-replay-component .replay-progress {
1758
+ display: flex;
1759
+ align-items: center;
1760
+ gap: 0.5rem;
1761
+ }
1762
+
1763
+ .positions-replay-component .progress-bar {
1764
+ position: relative;
1765
+ overflow: hidden;
1766
+ border-radius: 9999px;
1767
+ }
1768
+
1769
+ .positions-replay-component .progress-fill {
1770
+ height: 100%;
1771
+ background: linear-gradient(90deg, #3b82f6, #2563eb);
1772
+ transition: width 0.3s ease-in-out;
1773
+ }
1774
+
1775
+ .positions-replay-component .map-wrapper {
1776
+ position: relative;
1777
+ width: 100%;
1778
+ }
1779
+
1780
+ .positions-replay-component .positions-replay-map {
1781
+ width: 100%;
1782
+ height: 100%;
1783
+ }
1784
+
1785
+ .positions-replay-component .positions-replay-timeline {
1786
+ margin-top: 1rem;
1787
+ }
1788
+
1789
+ .positions-replay-component .timeline-container {
1790
+ position: relative;
1791
+ }
1792
+
1793
+ .positions-replay-component .timeline-track {
1794
+ display: flex;
1795
+ padding-bottom: 1rem;
1796
+ min-width: min-content;
1797
+ }
1798
+
1799
+ .positions-replay-component .timeline-item {
1800
+ position: relative;
1801
+ min-width: 80px;
1802
+ padding: 0.5rem;
1803
+ border-radius: 0.375rem;
1804
+ transition: all 0.2s ease-in-out;
1805
+ }
1806
+
1807
+ .positions-replay-component .timeline-item:hover {
1808
+ transform: translateY(-2px);
1809
+ }
1810
+
1811
+ .positions-replay-component .timeline-marker {
1812
+ width: 0.75rem;
1813
+ height: 0.75rem;
1814
+ border-radius: 50%;
1815
+ margin: 0 auto 0.25rem;
1816
+ transition: all 0.2s ease-in-out;
1817
+ }
1818
+
1819
+ .positions-replay-component .timeline-item:hover .timeline-marker {
1820
+ transform: scale(1.3);
1821
+ }
1822
+
1823
+ .positions-replay-component .timeline-content {
1824
+ text-align: center;
1825
+ white-space: nowrap;
1826
+ }
1827
+
1828
+ .positions-replay-component .positions-replay-table table {
1829
+ width: 100%;
1830
+ border-collapse: collapse;
1831
+ }
1832
+
1833
+ .positions-replay-component .positions-replay-table th {
1834
+ position: sticky;
1835
+ top: 0;
1836
+ z-index: 10;
1837
+ white-space: nowrap;
1838
+ text-overflow: ellipsis;
1839
+ font-size: 0.65rem;
1840
+ }
1841
+
1842
+ .positions-replay-component .empty-state {
1843
+ display: flex;
1844
+ flex-direction: column;
1845
+ align-items: center;
1846
+ justify-content: center;
1847
+ min-height: 400px;
1848
+ }
@@ -1,2 +1,2 @@
1
-
2
- {{outlet}}
1
+ <Device::Details @resource={{@model}} />
2
+ <Spacer @height="200px" />
@@ -1,2 +1,15 @@
1
-
2
- {{outlet}}
1
+ <Layout::Resource::Panel
2
+ @resource={{@model}}
3
+ @controller={{this}}
4
+ @headerComponent={{component "device/panel-header" resource=@model}}
5
+ @headerTitle={{or @model.name @model.serial_number}}
6
+ @actionButtons={{this.actionButtons}}
7
+ @onPressCancel={{transition-to "connectivity.devices.index"}}
8
+ @onOverlayReady={{fn (mut this.overlay)}}
9
+ @headerClass="no-bottom-border"
10
+ @bodyClass="no-scroll"
11
+ >
12
+ <TabNavigation @tabs={{this.tabs}} @contentClass="scrollable" @tablistClass="pl-2">
13
+ {{outlet}}
14
+ </TabNavigation>
15
+ </Layout::Resource::Panel>
@@ -1,7 +1,7 @@
1
1
  <Layout::Resource::Panel
2
2
  @resource={{@model}}
3
3
  @controller={{this}}
4
- @headerTitle={{concat "Edit: " @model.name}}
4
+ @headerTitle={{concat "Edit: " (or @model.name @model.serial_number)}}
5
5
  @actionButtons={{this.actionButtons}}
6
6
  @saveTask={{this.save}}
7
7
  @onPressCancel={{this.cancel}}
@@ -1,6 +1,6 @@
1
1
  <Layout::Resource::Tabular
2
2
  @resource="device-event"
3
- @title={{t "resource.device-events"}}
3
+ @title="Connectivity Events"
4
4
  @searchQuery={{this.query}}
5
5
  @onSearch={{perform this.deviceEventActions.controllerSearchTask this}}
6
6
  @data={{@model}}
@@ -1,2 +1,2 @@
1
-
2
- {{outlet}}
1
+ <Sensor::Details @resource={{@model}} />
2
+ <Spacer @height="200px" />
@@ -1,2 +1,15 @@
1
-
2
- {{outlet}}
1
+ <Layout::Resource::Panel
2
+ @resource={{@model}}
3
+ @controller={{this}}
4
+ @headerComponent={{component "sensor/panel-header" resource=@model}}
5
+ @headerTitle={{or @model.name @model.serial_number}}
6
+ @actionButtons={{this.actionButtons}}
7
+ @onPressCancel={{transition-to "connectivity.sensors.index"}}
8
+ @onOverlayReady={{fn (mut this.overlay)}}
9
+ @headerClass="no-bottom-border"
10
+ @bodyClass="no-scroll"
11
+ >
12
+ <TabNavigation @tabs={{this.tabs}} @contentClass="scrollable" @tablistClass="pl-2">
13
+ {{outlet}}
14
+ </TabNavigation>
15
+ </Layout::Resource::Panel>
@@ -1,7 +1,7 @@
1
1
  <Layout::Resource::Panel
2
2
  @resource={{@model}}
3
3
  @controller={{this}}
4
- @headerTitle={{concat "Edit: " @model.name}}
4
+ @headerTitle={{concat "Edit: " (or @model.name @model.serial_number)}}
5
5
  @actionButtons={{this.actionButtons}}
6
6
  @saveTask={{this.save}}
7
7
  @onPressCancel={{this.cancel}}
@@ -1,2 +1,2 @@
1
-
2
- {{outlet}}
1
+ <Telematic::Details @resource={{@model}} />
2
+ <Spacer @height="200px" />
@@ -1,2 +1,14 @@
1
-
2
- {{outlet}}
1
+ <Layout::Resource::Panel
2
+ @resource={{@model}}
3
+ @controller={{this}}
4
+ @headerTitle={{or @model.name @model.provider}}
5
+ @actionButtons={{this.actionButtons}}
6
+ @onPressCancel={{transition-to "connectivity.telematics.index"}}
7
+ @onOverlayReady={{fn (mut this.overlay)}}
8
+ @headerClass="no-bottom-border"
9
+ @bodyClass="no-scroll"
10
+ >
11
+ <TabNavigation @tabs={{this.tabs}} @contentClass="scrollable" @tablistClass="pl-2">
12
+ {{outlet}}
13
+ </TabNavigation>
14
+ </Layout::Resource::Panel>
@@ -1,7 +1,7 @@
1
1
  <Layout::Resource::Panel
2
2
  @resource={{@model}}
3
3
  @controller={{this}}
4
- @headerTitle={{concat "Edit: " @model.name}}
4
+ @headerTitle={{concat "Edit: " (or @model.name @model.provider)}}
5
5
  @actionButtons={{this.actionButtons}}
6
6
  @saveTask={{this.save}}
7
7
  @onPressCancel={{this.cancel}}
@@ -0,0 +1 @@
1
+ <PositionsReplay @resource={{@model}} />
@@ -53,9 +53,33 @@
53
53
  {{/if}}
54
54
 
55
55
  {{#if (eq this.layout "kanban")}}
56
- <Layout::Section::Header @title={{t "menu.order-board"}} @actionsWrapperClass="space-x-1" />
56
+ <Layout::Section::Header @title={{t "menu.order-board"}} @subtitle={{@model.length}} @subtitleClass="text-xs text-center font-semibold ml-2 w-6 h-6 rounded-full bg-blue-100 text-blue-900 dark:bg-blue-900 dark:text-blue-100 flex items-center justify-center" @actionsWrapperClass="space-x-1">
57
+ <div class="flex flex-row items-center space-x-2">
58
+ <div class="w-64">
59
+ <div class="fleetbase-model-select fleetbase-power-select ember-model-select">
60
+ <ModelSelect
61
+ @modelName="order-config"
62
+ @selectedModel={{or this.orderConfig this.type}}
63
+ @placeholder={{t "order.fields.order-type-placeholder"}}
64
+ @triggerClass="form-select form-input order-board-type-filter"
65
+ @infiniteScroll={{false}}
66
+ @renderInPlace={{true}}
67
+ @allowClear={{true}}
68
+ @onChange={{fn (mut this.orderConfig)}}
69
+ @onChangeId={{fn (mut this.type)}}
70
+ as |orderConfig|
71
+ >
72
+ <div class="text-sm">
73
+ <div class="font-semibold normalize-in-trigger">{{orderConfig.name}}</div>
74
+ <div class="hide-from-trigger">{{n-a orderConfig.description}}</div>
75
+ </div>
76
+ </ModelSelect>
77
+ </div>
78
+ </div>
79
+ </div>
80
+ </Layout::Section::Header>
57
81
  <Layout::Section::Body>
58
- <Order::Kanban @orders={{@model}} @headerOffset={{160}} />
82
+ <Order::Kanban @orders={{@model}} @headerOffset={{160}} @orderConfig={{this.type}} />
59
83
  </Layout::Section::Body>
60
84
  {{/if}}
61
85