@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
@@ -1,5 +1,4 @@
1
1
  import Component from '@glimmer/component';
2
- import { tracked } from '@glimmer/tracking';
3
2
  import { inject as service } from '@ember/service';
4
3
  import { task } from 'ember-concurrency';
5
4
 
@@ -9,55 +8,58 @@ export default class DriverFormComponent extends Component {
9
8
  @service currentUser;
10
9
  @service notifications;
11
10
  @service modalsManager;
12
- @tracked userAccountActionButtons = [
13
- {
14
- icon: 'user-plus',
15
- size: 'xs',
16
- permission: 'iam create user',
17
- onClick: () => {
18
- const user = this.store.createRecord('user', {
19
- status: 'pending',
20
- type: 'user',
21
- });
22
11
 
23
- this.modalsManager.show('modals/user-form', {
24
- title: 'Create a new user',
25
- user,
26
- formPermission: 'iam create user',
27
- uploadNewPhoto: (file) => {
28
- this.fetch.uploadFile.perform(
29
- file,
30
- {
31
- path: `uploads/${this.currentUser.companyId}/users/${user.slug}`,
32
- subject_uui: user.id,
33
- subject_type: 'user',
34
- type: 'user_photo',
35
- },
36
- (uploadedFile) => {
37
- user.setProperties({
38
- avatar_uuid: uploadedFile.id,
39
- avatar_url: uploadedFile.url,
40
- avatar: uploadedFile,
41
- });
42
- }
43
- );
44
- },
45
- confirm: async (modal) => {
46
- modal.startLoading();
12
+ get userAccountActionButtons() {
13
+ return [
14
+ {
15
+ icon: 'user-plus',
16
+ size: 'xs',
17
+ permission: 'iam create user',
18
+ onClick: () => {
19
+ const user = this.store.createRecord('user', {
20
+ status: 'pending',
21
+ type: 'user',
22
+ });
47
23
 
48
- try {
49
- await user.save();
50
- this.notifications.success('New user created successfully!');
51
- modal.done();
52
- } catch (error) {
53
- this.notifications.serverError(error);
54
- modal.stopLoading();
55
- }
56
- },
57
- });
24
+ this.modalsManager.show('modals/user-form', {
25
+ title: 'Create a new user',
26
+ user,
27
+ formPermission: 'iam create user',
28
+ uploadNewPhoto: (file) => {
29
+ this.fetch.uploadFile.perform(
30
+ file,
31
+ {
32
+ path: `uploads/${this.currentUser.companyId}/users/${user.slug}`,
33
+ subject_uui: user.id,
34
+ subject_type: 'user',
35
+ type: 'user_photo',
36
+ },
37
+ (uploadedFile) => {
38
+ user.setProperties({
39
+ avatar_uuid: uploadedFile.id,
40
+ avatar_url: uploadedFile.url,
41
+ avatar: uploadedFile,
42
+ });
43
+ }
44
+ );
45
+ },
46
+ confirm: async (modal) => {
47
+ modal.startLoading();
48
+
49
+ try {
50
+ await user.save();
51
+ this.notifications.success('New user created successfully!');
52
+ modal.done();
53
+ } catch (error) {
54
+ this.notifications.serverError(error);
55
+ modal.stopLoading();
56
+ }
57
+ },
58
+ });
59
+ },
58
60
  },
59
- },
60
- ];
61
+ ];
62
+ }
61
63
 
62
64
  @task *handlePhotoUpload(file) {
63
65
  try {
@@ -25,11 +25,7 @@
25
25
  <InputGroup @name={{t "common.internal-id"}} @value={{@resource.internal_id}} @helpText={{t "modals.entity-form.id-text"}} />
26
26
  <InputGroup @name={{t "modals.entity-form.sku"}} @value={{@resource.sku}} @helpText={{t "modals.entity-form.sku-text"}} />
27
27
  <div></div>
28
- <InputGroup
29
- @name={{t "modals.entity-form.description"}}
30
- @helpText={{t "modals.entity-form.description-text"}}
31
- @wrapperClass="col-span-2"
32
- >
28
+ <InputGroup @name={{t "modals.entity-form.description"}} @helpText={{t "modals.entity-form.description-text"}} @wrapperClass="col-span-2">
33
29
  <Textarea @value={{@resource.description}} type="text" class="w-full form-input" placeholder={{t "modals.entity-form.description"}} />
34
30
  </InputGroup>
35
31
  </div>
@@ -83,4 +79,10 @@
83
79
  </div>
84
80
  </div>
85
81
  </ContentPanel>
82
+
83
+ <CustomField::Yield @subject={{@resource}} @wrapperClass="bordered-top" />
84
+
85
+ <RegistryYield @registry="fleet-ops:component:entity:form" as |RegistryComponent|>
86
+ <RegistryComponent @resource={{@resource}} @controller={{this.controller}} @permission={{get-write-permission @resource}} />
87
+ </RegistryYield>
86
88
  </div>
@@ -171,14 +171,14 @@ export default class LayoutFleetOpsSidebarComponent extends Component {
171
171
  permission: 'fleet-ops list device-event',
172
172
  visible: this.abilities.can('fleet-ops see device-event'),
173
173
  },
174
- {
175
- intl: 'menu.tracking',
176
- title: this.intl.t('menu.tracking'),
177
- icon: 'map-marked-alt',
178
- route: 'connectivity.tracking',
179
- permission: 'fleet-ops list device',
180
- visible: this.abilities.can('fleet-ops see device'),
181
- },
174
+ // {
175
+ // intl: 'menu.tracking',
176
+ // title: this.intl.t('menu.tracking'),
177
+ // icon: 'map-marked-alt',
178
+ // route: 'connectivity.tracking',
179
+ // permission: 'fleet-ops list device',
180
+ // visible: this.abilities.can('fleet-ops see device'),
181
+ // },
182
182
  ];
183
183
 
184
184
  const maintenanceItems = [
@@ -284,10 +284,10 @@ export default class LayoutFleetOpsSidebarComponent extends Component {
284
284
  // open: this.appCache.get('fleet-ops:sidebar:maintenance:open', false),
285
285
  // onToggle: (open) => this.appCache.set('fleet-ops:sidebar:maintenance:open', open),
286
286
  // }),
287
- // createPanel('menu.connectivity', 'connectivity', connectivityItems, {
288
- // open: this.appCache.get('fleet-ops:sidebar:connectivity:open', false),
289
- // onToggle: (open) => this.appCache.set('fleet-ops:sidebar:connectivity:open', open),
290
- // }),
287
+ createPanel('menu.connectivity', 'connectivity', connectivityItems, {
288
+ open: this.appCache.get('fleet-ops:sidebar:connectivity:open', false),
289
+ onToggle: (open) => this.appCache.set('fleet-ops:sidebar:connectivity:open', open),
290
+ }),
291
291
  createPanel('menu.analytics', 'analytics', analyticsItems, {
292
292
  open: this.appCache.get('fleet-ops:sidebar:analytics:open', false),
293
293
  onToggle: (open) => this.appCache.set('fleet-ops:sidebar:analytics:open', open),
@@ -0,0 +1,58 @@
1
+ <div class="flex flex-row items-center justify-between w-full px-5 py-2 border-b border-gray-200 dark:border-gray-700">
2
+ <div class="flex flex-row items-center space-x-2">
3
+ <div>
4
+ <ModelSelect
5
+ @modelName="telematic"
6
+ @selectedModel={{this.telematic}}
7
+ @placeholder="Filter by Telematic"
8
+ @triggerClass="form-select form-input form-input-sm w-48"
9
+ @infiniteScroll={{false}}
10
+ @renderInPlace={{true}}
11
+ @onChange={{this.onTelematicSelected}}
12
+ as |model|
13
+ >
14
+ <div class="space-x-2 text-sm">
15
+ <div class="inline-block align-top">
16
+ <div class="hide-from-trigger h-1.5 w-full" />
17
+ <Image src={{model.provider_descriptor.icon}} class="w-5 h-5" />
18
+ </div>
19
+ <div class="inline-block">
20
+ <div class="font-semibold normalize-in-trigger">{{or model.name model.public_id}}</div>
21
+ <div class="hide-from-trigger">{{n-a model.provider_descriptor.label (titleize model.provider)}}</div>
22
+ </div>
23
+ </div>
24
+ </ModelSelect>
25
+ </div>
26
+
27
+ <div>
28
+ <ModelSelect
29
+ @modelName="device"
30
+ @selectedModel={{this.device}}
31
+ @placeholder="Filter by Device"
32
+ @triggerClass="form-select form-input form-input-sm w-48"
33
+ @infiniteScroll={{false}}
34
+ @renderInPlace={{true}}
35
+ @onChange={{this.onDeviceSelected}}
36
+ as |model|
37
+ >
38
+ <div class="text-sm">
39
+ <div class="font-semibold normalize-in-trigger">{{model.name}}</div>
40
+ <div class="hide-from-trigger">{{n-a model.serial_number}}</div>
41
+ </div>
42
+ </ModelSelect>
43
+ </div>
44
+
45
+ <div class="w-48">
46
+ <DatePicker
47
+ @value={{this.dateFilter}}
48
+ @range={{true}}
49
+ @onSelect={{this.onDateRangeChanged}}
50
+ @autoClose={{true}}
51
+ @placeholder="Select date range"
52
+ class="w-full form-input form-input-sm w-48"
53
+ />
54
+ </div>
55
+ </div>
56
+ </div>
57
+ <Table @rows={{this.events}} @columns={{this.columns}} />
58
+ <Spacer @height="200px" />
@@ -0,0 +1,181 @@
1
+ import Component from '@glimmer/component';
2
+ import { tracked } from '@glimmer/tracking';
3
+ import { inject as service } from '@ember/service';
4
+ import { action } from '@ember/object';
5
+ import { task } from 'ember-concurrency';
6
+ import { isArray } from '@ember/array';
7
+ import { startOfWeek, endOfWeek, format } from 'date-fns';
8
+
9
+ export default class MapDrawerDeviceEventListingComponent extends Component {
10
+ @service store;
11
+ @service hostRouter;
12
+ @service notifications;
13
+ @service deviceEventActions;
14
+ @service deviceActions;
15
+ @service intl;
16
+
17
+ @tracked events = [];
18
+ @tracked telematic = null;
19
+ @tracked device = null;
20
+ @tracked dateFilter = [format(startOfWeek(new Date(), { weekStartsOn: 1 }), 'yyyy-MM-dd'), format(endOfWeek(new Date(), { weekStartsOn: 1 }), 'yyyy-MM-dd')];
21
+
22
+ get columns() {
23
+ return [
24
+ {
25
+ label: 'Event',
26
+ valuePath: 'event_type',
27
+ cellComponent: 'table/cell/anchor',
28
+ cellClassNames: 'uppercase',
29
+ action: this.deviceEventActions.panel.view,
30
+ permission: 'fleet-ops view device-event',
31
+ resizable: true,
32
+ sortable: true,
33
+ filterable: true,
34
+ filterParam: 'name',
35
+ filterComponent: 'filter/string',
36
+ },
37
+ {
38
+ label: 'Device',
39
+ valuePath: 'device.displayName',
40
+ cellComponent: 'table/cell/anchor',
41
+ action: this.deviceActions.panel.view,
42
+ permission: 'fleet-ops view device',
43
+ resizable: true,
44
+ sortable: true,
45
+ filterable: true,
46
+ filterComponent: 'filter/model',
47
+ filterComponentPlaceholder: 'Select device',
48
+ filterParam: 'device',
49
+ model: 'device',
50
+ },
51
+ {
52
+ label: 'Provider',
53
+ valuePath: 'provider',
54
+ resizable: true,
55
+ sortable: true,
56
+ filterable: true,
57
+ filterParam: 'provider',
58
+ filterComponent: 'filter/string',
59
+ },
60
+ {
61
+ label: 'Severity',
62
+ valuePath: 'severity',
63
+ resizable: true,
64
+ sortable: true,
65
+ filterable: true,
66
+ filterParam: 'severity',
67
+ filterComponent: 'filter/string',
68
+ },
69
+ {
70
+ label: 'IDENT',
71
+ valuePath: 'ident',
72
+ hidden: true,
73
+ resizable: true,
74
+ sortable: true,
75
+ },
76
+ {
77
+ label: 'Protocol',
78
+ valuePath: 'protocol',
79
+ hidden: true,
80
+ resizable: true,
81
+ sortable: true,
82
+ },
83
+ {
84
+ label: 'State',
85
+ valuePath: 'state',
86
+ hidden: true,
87
+ resizable: true,
88
+ sortable: true,
89
+ },
90
+ {
91
+ label: 'Code',
92
+ valuePath: 'code',
93
+ resizable: true,
94
+ sortable: true,
95
+ filterable: true,
96
+ filterParam: 'code',
97
+ filterComponent: 'filter/string',
98
+ },
99
+ {
100
+ label: this.intl.t('column.created-at'),
101
+ valuePath: 'createdAt',
102
+ sortParam: 'created_at',
103
+ width: '10%',
104
+ resizable: true,
105
+ sortable: true,
106
+ filterable: true,
107
+ filterComponent: 'filter/date',
108
+ },
109
+ {
110
+ label: '',
111
+ cellComponent: 'table/cell/dropdown',
112
+ ddButtonText: false,
113
+ ddButtonIcon: 'ellipsis-h',
114
+ ddButtonIconPrefix: 'fas',
115
+ ddMenuLabel: this.intl.t('common.resource-actions', { resource: this.intl.t('resource.device-event') }),
116
+ cellClassNames: 'overflow-visible',
117
+ wrapperClass: 'flex items-center justify-end mx-2',
118
+ width: '10%',
119
+ actions: [
120
+ {
121
+ label: this.intl.t('common.view-resource', { resource: this.intl.t('resource.device-event') }),
122
+ fn: this.deviceEventActions.panel.view,
123
+ permission: 'fleet-ops view device-event',
124
+ },
125
+ ],
126
+ sortable: false,
127
+ filterable: false,
128
+ resizable: false,
129
+ searchable: false,
130
+ },
131
+ ];
132
+ }
133
+
134
+ constructor() {
135
+ super(...arguments);
136
+ this.loadEvents.perform();
137
+ }
138
+
139
+ @action onDeviceSelected(device) {
140
+ this.device = device;
141
+ this.loadEvents.perform();
142
+ }
143
+
144
+ @action onTelematicSelected(telematic) {
145
+ this.telematic = telematic;
146
+ this.loadEvents.perform();
147
+ }
148
+
149
+ @action onDateRangeChanged({ formattedDate }) {
150
+ if (isArray(formattedDate) && formattedDate.length === 2) {
151
+ this.dateFilter = formattedDate;
152
+ this.loadEvents.perform();
153
+ }
154
+ }
155
+
156
+ @task *loadEvents() {
157
+ try {
158
+ const params = {
159
+ limit: 900,
160
+ sort: 'created_at',
161
+ };
162
+
163
+ if (this.telematic) {
164
+ params.telematic = this.telematic.id;
165
+ }
166
+
167
+ if (this.device) {
168
+ params.device = this.device.id;
169
+ }
170
+
171
+ if (isArray(this.dateFilter) && this.dateFilter.length === 2) {
172
+ params.created_at = this.dateFilter.join(',');
173
+ }
174
+
175
+ const events = yield this.store.query('device-event', params);
176
+ this.positions = isArray(events) ? events : [];
177
+ } catch (error) {
178
+ this.notifications.serverError(error);
179
+ }
180
+ }
181
+ }
@@ -0,0 +1,84 @@
1
+ <div class="positions-replay-component flex flex-row items-center justify-between w-full px-5 py-2 border-b border-gray-200 dark:border-gray-700">
2
+ <div class="flex flex-row items-center space-x-2">
3
+ <div>
4
+ <div class="fleetbase-model-select fleetbase-power-select ember-model-select">
5
+ <PowerSelect
6
+ @options={{this.trackables}}
7
+ @selected={{this.resource}}
8
+ @onChange={{this.onResourceSelected}}
9
+ @placeholder={{t "common.select-field" field="trackable"}}
10
+ @triggerClass="form-select form-input form-input-sm w-48"
11
+ as |option|
12
+ >
13
+ <div class="flex flex-row items-center space-x-2">
14
+ <div class="inline-block">
15
+ <Image src={{or option.avatar_url option.photo_url}} class="w-4 h-4" />
16
+ </div>
17
+ <div class="inline-block text-sm">
18
+ <div class="font-semibold normalize-in-trigger">{{or option.name option.displayName option.public_id}}</div>
19
+ <div class="hide-from-trigger">{{or option.email option.serial_number option.plate_number option.internal_id}}</div>
20
+ </div>
21
+ </div>
22
+ </PowerSelect>
23
+ </div>
24
+ </div>
25
+
26
+ <div class="w-48">
27
+ <DatePicker
28
+ @value={{this.dateFilter}}
29
+ @range={{true}}
30
+ @onSelect={{this.onDateRangeChanged}}
31
+ @autoClose={{true}}
32
+ @placeholder="Select date range"
33
+ class="w-full form-input form-input-sm"
34
+ />
35
+ </div>
36
+ </div>
37
+
38
+ <div class="replay-controls">
39
+ <div class="flex items-center justify-between space-x-2">
40
+ <div class="flex items-center space-x-2">
41
+ <Button @type="danger" @text="Stop" @icon="stop" @size="xs" @onClick={{this.stopReplay}} />
42
+ <Button
43
+ @type="success"
44
+ @text="Play"
45
+ @icon="play"
46
+ @size="xs"
47
+ @isLoading={{this.isReplaying}}
48
+ @disabled={{or this.replayPositions.isRunning (not this.hasPositions)}}
49
+ @onClick={{this.startReplay}}
50
+ />
51
+
52
+ <div class="speed-control">
53
+ <label class="text-xs uppercase tracking-wide font-medium text-gray-700 dark:text-gray-400 mr-1">
54
+ Speed:
55
+ </label>
56
+ <Select
57
+ @value={{this.replaySpeed}}
58
+ @options={{this.speedOptions}}
59
+ @optionValue="value"
60
+ @optionLabel="label"
61
+ @onSelect={{this.onSpeedChanged}}
62
+ class="form-select speed-select"
63
+ />
64
+ </div>
65
+ </div>
66
+
67
+ {{#if this.isReplaying}}
68
+ <div class="replay-progress flex items-center space-x-1">
69
+ <span class="text-sm text-gray-600 dark:text-gray-400">
70
+ {{this.currentReplayIndex}}/{{this.totalPositions}}
71
+ </span>
72
+ <div class="progress-bar w-32 h-2 bg-gray-200 dark:bg-gray-700 rounded-full overflow-hidden">
73
+ <div class="progress-fill h-full bg-blue-600 transition-all duration-300" style={{this.replayProgressWidth}}></div>
74
+ </div>
75
+ <span class="text-sm font-medium text-gray-700 dark:text-gray-300">
76
+ {{this.replayProgress}}%
77
+ </span>
78
+ </div>
79
+ {{/if}}
80
+ </div>
81
+ </div>
82
+ </div>
83
+ <Table @rows={{this.positions}} @columns={{this.columns}} />
84
+ <Spacer @height="200px" />