@fleetbase/fleetops-engine 0.6.20 → 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 (120) 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-config-manager/custom-fields.js +1 -1
  20. package/addon/components/positions-replay.hbs +333 -0
  21. package/addon/components/positions-replay.js +372 -0
  22. package/addon/components/sensor/details.hbs +64 -38
  23. package/addon/components/sensor/form.hbs +112 -63
  24. package/addon/components/sensor/form.js +36 -24
  25. package/addon/components/sensor/panel-header.hbs +32 -0
  26. package/addon/components/sensor/panel-header.js +3 -0
  27. package/addon/components/telematic/details.hbs +40 -16
  28. package/addon/components/telematic/form.hbs +63 -64
  29. package/addon/components/telematic/form.js +73 -4
  30. package/addon/components/vehicle/card.hbs +1 -1
  31. package/addon/controllers/analytics/reports/index/edit.js +1 -1
  32. package/addon/controllers/connectivity/devices/index/details.js +22 -1
  33. package/addon/controllers/connectivity/devices/index/edit.js +66 -1
  34. package/addon/controllers/connectivity/devices/index.js +51 -9
  35. package/addon/controllers/connectivity/events/index.js +65 -16
  36. package/addon/controllers/connectivity/sensors/index/details.js +22 -1
  37. package/addon/controllers/connectivity/sensors/index/edit.js +66 -1
  38. package/addon/controllers/connectivity/sensors/index.js +66 -6
  39. package/addon/controllers/connectivity/telematics/index/details.js +22 -1
  40. package/addon/controllers/connectivity/telematics/index/edit.js +66 -1
  41. package/addon/controllers/connectivity/telematics/index.js +20 -11
  42. package/addon/controllers/management/fleets/index/details.js +26 -21
  43. package/addon/controllers/management/fleets/index/edit.js +9 -6
  44. package/addon/controllers/management/vehicles/index/details.js +21 -13
  45. package/addon/controllers/settings/custom-fields.js +6 -0
  46. package/addon/helpers/get-fleet-ops-option-label.js +11 -0
  47. package/addon/routes/connectivity/devices/index/details.js +27 -1
  48. package/addon/routes/connectivity/devices/index/edit.js +27 -1
  49. package/addon/routes/connectivity/sensors/index/details.js +27 -1
  50. package/addon/routes/connectivity/sensors/index/edit.js +27 -1
  51. package/addon/routes/connectivity/telematics/index/details.js +27 -1
  52. package/addon/routes/connectivity/telematics/index/edit.js +27 -1
  53. package/addon/routes/management/vehicles/index/details/positions.js +3 -0
  54. package/addon/routes.js +1 -0
  55. package/addon/services/movement-tracker.js +81 -30
  56. package/addon/styles/fleetops-engine.css +157 -0
  57. package/addon/templates/connectivity/devices/index/details/index.hbs +2 -2
  58. package/addon/templates/connectivity/devices/index/details.hbs +15 -2
  59. package/addon/templates/connectivity/devices/index/edit.hbs +1 -1
  60. package/addon/templates/connectivity/events/index.hbs +1 -1
  61. package/addon/templates/connectivity/sensors/index/details/index.hbs +2 -2
  62. package/addon/templates/connectivity/sensors/index/details.hbs +15 -2
  63. package/addon/templates/connectivity/sensors/index/edit.hbs +1 -1
  64. package/addon/templates/connectivity/telematics/index/details/index.hbs +2 -2
  65. package/addon/templates/connectivity/telematics/index/details.hbs +14 -2
  66. package/addon/templates/connectivity/telematics/index/edit.hbs +1 -1
  67. package/addon/templates/management/vehicles/index/details/positions.hbs +1 -0
  68. package/addon/utils/fleet-ops-options.js +95 -0
  69. package/app/components/device/panel-header.js +1 -0
  70. package/app/components/map/drawer/device-event-listing.js +1 -0
  71. package/app/components/map/drawer/position-listing.js +1 -0
  72. package/app/components/positions-replay.js +1 -0
  73. package/app/components/sensor/panel-header.js +1 -0
  74. package/app/helpers/get-fleet-ops-option-label.js +1 -0
  75. package/app/routes/management/vehicles/index/details/positions.js +1 -0
  76. package/app/templates/management/vehicles/index/details/positions.js +1 -0
  77. package/composer.json +1 -1
  78. package/extension.json +1 -1
  79. package/package.json +4 -4
  80. package/server/config/telematics.php +111 -0
  81. package/server/migrations/2025_10_27_000001_add_telematics_integration_fields.php +70 -0
  82. package/server/migrations/2025_10_27_171322_fix_device_column_names.php +107 -0
  83. package/server/migrations/2025_10_27_203023_add_company_uuid_to_device_events_table.php +28 -0
  84. package/server/src/Console/Commands/ReplayVehicleLocations.php +225 -0
  85. package/server/src/Contracts/TelematicProviderDescriptor.php +72 -0
  86. package/server/src/Contracts/TelematicProviderInterface.php +119 -0
  87. package/server/src/Exceptions/TelematicProviderException.php +14 -0
  88. package/server/src/Exceptions/TelematicRateLimitExceededException.php +12 -0
  89. package/server/src/Http/Controllers/Api/v1/DriverController.php +24 -14
  90. package/server/src/Http/Controllers/Api/v1/VehicleController.php +27 -7
  91. package/server/src/Http/Controllers/Internal/v1/DeviceController.php +22 -0
  92. package/server/src/Http/Controllers/Internal/v1/PositionController.php +240 -0
  93. package/server/src/Http/Controllers/Internal/v1/SensorController.php +11 -0
  94. package/server/src/Http/Controllers/Internal/v1/TelematicController.php +141 -0
  95. package/server/src/Http/Controllers/Internal/v1/TelematicWebhookController.php +170 -0
  96. package/server/src/Http/Filter/DeviceEventFilter.php +68 -0
  97. package/server/src/Http/Filter/PositionFilter.php +35 -0
  98. package/server/src/Http/Resources/v1/Position.php +44 -0
  99. package/server/src/Jobs/ReplayPositions.php +64 -0
  100. package/server/src/Jobs/SendPositionReplay.php +65 -0
  101. package/server/src/Jobs/SyncTelematicDevicesJob.php +106 -0
  102. package/server/src/Jobs/TestTelematicConnectionJob.php +102 -0
  103. package/server/src/Models/Device.php +72 -10
  104. package/server/src/Models/DeviceEvent.php +7 -0
  105. package/server/src/Models/Driver.php +28 -1
  106. package/server/src/Models/Payload.php +0 -1
  107. package/server/src/Models/Place.php +4 -1
  108. package/server/src/Models/Position.php +17 -17
  109. package/server/src/Models/Sensor.php +78 -13
  110. package/server/src/Models/Telematic.php +116 -6
  111. package/server/src/Models/Vehicle.php +8 -11
  112. package/server/src/Providers/FleetOpsServiceProvider.php +2 -0
  113. package/server/src/Support/Telematics/Providers/AbstractProvider.php +151 -0
  114. package/server/src/Support/Telematics/Providers/FlespiProvider.php +182 -0
  115. package/server/src/Support/Telematics/Providers/GeotabProvider.php +181 -0
  116. package/server/src/Support/Telematics/Providers/SamsaraProvider.php +177 -0
  117. package/server/src/Support/Telematics/TelematicProviderRegistry.php +147 -0
  118. package/server/src/Support/Telematics/TelematicService.php +223 -0
  119. package/server/src/Support/Utils.php +1 -1
  120. package/server/src/routes.php +12 -1
@@ -0,0 +1,289 @@
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 { htmlSafe } from '@ember/template';
8
+ import { startOfWeek, endOfWeek, format } from 'date-fns';
9
+ import getModelName from '@fleetbase/ember-core/utils/get-model-name';
10
+
11
+ export default class MapDrawerPositionListingComponent extends Component {
12
+ @service leafletMapManager;
13
+ @service store;
14
+ @service fetch;
15
+ @service movementTracker;
16
+ @service hostRouter;
17
+ @service notifications;
18
+
19
+ @service intl;
20
+ @tracked positions = [];
21
+ @tracked resource = null;
22
+ @tracked selectedOrder = null;
23
+ @tracked dateFilter = [format(startOfWeek(new Date(), { weekStartsOn: 1 }), 'yyyy-MM-dd'), format(endOfWeek(new Date(), { weekStartsOn: 1 }), 'yyyy-MM-dd')];
24
+ @tracked isReplaying = false;
25
+ @tracked replaySpeed = '1';
26
+ @tracked currentReplayIndex = 0;
27
+ @tracked channelId = null;
28
+
29
+ get trackables() {
30
+ const vehicles = this.leafletMapManager._livemap?.vehicles ?? [];
31
+ const drivers = this.leafletMapManager._livemap?.drivers ?? [];
32
+
33
+ return [...vehicles, ...drivers];
34
+ }
35
+
36
+ get replayProgressWidth() {
37
+ return htmlSafe(`width: ${this.replayProgress}%;`);
38
+ }
39
+
40
+ get orderFilters() {
41
+ const params = {};
42
+
43
+ if (this.resourceType === 'vehicle') {
44
+ params.vehicle_assigned_uuid = this.resource?.id;
45
+ }
46
+
47
+ if (this.resourceType === 'driver') {
48
+ params.driver_assigned_uuid = this.resource?.id;
49
+ }
50
+
51
+ return params;
52
+ }
53
+
54
+ get resourceType() {
55
+ if (!this.resource) {
56
+ return 'resource';
57
+ }
58
+ return getModelName(this.resource) || 'resource';
59
+ }
60
+
61
+ get hasPositions() {
62
+ return this.positions.length > 0;
63
+ }
64
+
65
+ get totalPositions() {
66
+ return this.positions.length;
67
+ }
68
+
69
+ get replayProgress() {
70
+ if (this.totalPositions === 0) {
71
+ return 0;
72
+ }
73
+ return Math.round((this.currentReplayIndex / this.totalPositions) * 100);
74
+ }
75
+
76
+ get speedOptions() {
77
+ return [
78
+ { label: '0.5x', value: '0.5' },
79
+ { label: '1x', value: '1' },
80
+ { label: '2x', value: '2' },
81
+ { label: '5x', value: '5' },
82
+ { label: '10x', value: '10' },
83
+ { label: '20x', value: '20' },
84
+ { label: '30x', value: '30' },
85
+ { label: '40x', value: '40' },
86
+ { label: '50x', value: '50' },
87
+ { label: '100x', value: '100' },
88
+ { label: '80x', value: '80' },
89
+ { label: '120x', value: '120' },
90
+ { label: '160x', value: '160' },
91
+ { label: '180x', value: '180' },
92
+ { label: '200x', value: '200' },
93
+ { label: '250x', value: '250' },
94
+ { label: '280x', value: '280' },
95
+ { label: '300x', value: '300' },
96
+ { label: '350x', value: '350' },
97
+ { label: '400x', value: '400' },
98
+ { label: '500x', value: '500' },
99
+ { label: '600x', value: '600' },
100
+ { label: '1000x', value: '1000' },
101
+ ];
102
+ }
103
+
104
+ /** columns */
105
+ get columns() {
106
+ return [
107
+ {
108
+ label: '#',
109
+ valuePath: 'index',
110
+ width: '55px',
111
+ },
112
+ {
113
+ label: 'Timestamp',
114
+ valuePath: 'timestamp',
115
+ },
116
+ {
117
+ label: 'Latitude',
118
+ valuePath: 'latitude',
119
+ },
120
+ {
121
+ label: 'Longitude',
122
+ valuePath: 'longitude',
123
+ },
124
+ {
125
+ label: 'Speed (km/h)',
126
+ valuePath: 'speedKmh',
127
+ },
128
+ {
129
+ label: 'Heading',
130
+ valuePath: 'heading',
131
+ },
132
+ {
133
+ label: 'Altitude (m)',
134
+ valuePath: 'altitude',
135
+ },
136
+ ];
137
+ }
138
+
139
+ constructor() {
140
+ super(...arguments);
141
+ this.loadPositions.perform();
142
+ }
143
+
144
+ @action onResourceSelected(resource) {
145
+ this.resource = resource;
146
+ this.loadPositions.perform();
147
+ }
148
+
149
+ @action onOrderSelected(order) {
150
+ this.selectedOrder = order;
151
+ this.loadPositions.perform();
152
+ }
153
+
154
+ @action onDateRangeChanged({ formattedDate }) {
155
+ if (isArray(formattedDate) && formattedDate.length === 2) {
156
+ this.dateFilter = formattedDate;
157
+ this.loadPositions.perform();
158
+ }
159
+ }
160
+
161
+ @action onSpeedChanged(speed) {
162
+ this.replaySpeed = speed;
163
+ }
164
+
165
+ @action startReplay() {
166
+ if (this.positions.length === 0) {
167
+ this.notifications.warning('No positions to replay');
168
+ return;
169
+ }
170
+ this.replayPositions.perform();
171
+ }
172
+
173
+ @action stopReplay() {
174
+ this.isReplaying = false;
175
+ this.currentReplayIndex = 0;
176
+ }
177
+
178
+ @action clearFilters() {
179
+ this.selectedOrder = null;
180
+ this.dateFilter = null;
181
+ this.loadPositions.perform();
182
+ }
183
+
184
+ @action onPositionClicked(position) {
185
+ if (this.leafletMapManager.map && position.latitude && position.longitude) {
186
+ this.leafletMapManager.map.setView([position.latitude, position.longitude], this.zoom);
187
+ }
188
+ }
189
+
190
+ @task *loadPositions() {
191
+ if (!this.resource) return;
192
+
193
+ try {
194
+ const params = {
195
+ limit: 900,
196
+ sort: 'created_at',
197
+ subject_uuid: this.resource.id,
198
+ };
199
+
200
+ if (this.selectedOrder) {
201
+ params.order_uuid = this.selectedOrder.id;
202
+ }
203
+
204
+ if (isArray(this.dateFilter) && this.dateFilter.length === 2) {
205
+ params.created_at = this.dateFilter.join(',');
206
+ }
207
+
208
+ const positions = yield this.store.query('position', params);
209
+ this.positions = isArray(positions)
210
+ ? positions.map((pos, index) => {
211
+ pos.set('index', index + 1);
212
+ return pos;
213
+ })
214
+ : [];
215
+
216
+ if (this.positions?.length) {
217
+ const bounds = positions.map((pos) => pos.latLng).filter(Boolean);
218
+ const lastFiveBounds = bounds.slice(-5);
219
+ this.leafletMapManager.map.flyToBounds(lastFiveBounds, {
220
+ animate: true,
221
+ zoom: 16,
222
+ });
223
+ }
224
+ } catch (error) {
225
+ this.notifications.serverError(error);
226
+ }
227
+ }
228
+
229
+ @task *replayPositions() {
230
+ if (!this.resource) {
231
+ this.notifications.warning('No resource provided for replay');
232
+ return;
233
+ }
234
+
235
+ try {
236
+ this.isReplaying = true;
237
+ this.currentReplayIndex = 0;
238
+
239
+ const positionIds = this.positions.map((p) => p.id);
240
+
241
+ if (positionIds.length === 0) {
242
+ this.notifications.warning('No positions to replay');
243
+ this.isReplaying = false;
244
+ return;
245
+ }
246
+
247
+ // Generate unique channel ID for this replay session
248
+ this.channelId = `position.replay.${this.id}.${Date.now()}`;
249
+
250
+ // Start tracking on custom channel
251
+ yield this.movementTracker.track(this.resource, {
252
+ channelId: this.channelId,
253
+ callback: (output) => {
254
+ const {
255
+ data: { additionalData },
256
+ } = output;
257
+
258
+ const leafletLayer = this.resource.leafletLayer;
259
+ if (leafletLayer) {
260
+ const latlng = leafletLayer._slideToLatLng ?? leafletLayer.getLatLng();
261
+ this.leafletMapManager.map.panTo(latlng, { animate: true });
262
+ }
263
+
264
+ if (additionalData && Number.isFinite(additionalData.index)) {
265
+ this.currentReplayIndex = additionalData.index + 1;
266
+ if (this.currentReplayIndex === this.totalPositions) {
267
+ this.isReplaying = false;
268
+ }
269
+ }
270
+ },
271
+ });
272
+
273
+ // Trigger backend replay
274
+ const response = yield this.fetch.post('positions/replay', {
275
+ position_ids: positionIds,
276
+ channel_id: this.channelId,
277
+ speed: parseFloat(this.replaySpeed),
278
+ subject_uuid: this.resource.id,
279
+ });
280
+
281
+ if (response && response.status === 'ok') {
282
+ this.notifications.success('Replay started successfully');
283
+ }
284
+ } catch (error) {
285
+ this.notifications.serverError(error);
286
+ this.isReplaying = false;
287
+ }
288
+ }
289
+ }
@@ -13,6 +13,8 @@ export default class MapDrawerComponent extends Component {
13
13
  this.universe._createMenuItem('Vehicles', null, { icon: 'car', component: 'map/drawer/vehicle-listing' }),
14
14
  this.universe._createMenuItem('Drivers', null, { icon: 'id-card', component: 'map/drawer/driver-listing' }),
15
15
  this.universe._createMenuItem('Places', null, { icon: 'building', component: 'map/drawer/place-listing' }),
16
+ this.universe._createMenuItem('Positions', null, { icon: 'map-marker', component: 'map/drawer/position-listing' }),
17
+ this.universe._createMenuItem('Events', null, { icon: 'stream', component: 'map/drawer/device-event-listing' }),
16
18
  ...(isArray(registeredTabs) ? registeredTabs : []),
17
19
  ];
18
20
  }
@@ -66,10 +66,11 @@
66
66
  {{/each}}
67
67
 
68
68
  {{#each this.vehicles as |vehicle|}}
69
- <layers.marker
69
+ <layers.tracking-marker
70
70
  @id={{vehicle.id}}
71
71
  @publicId={{vehicle.public_id}}
72
72
  @location={{point-to-coordinates vehicle.location}}
73
+ @rotationAngle={{vehicle.heading}}
73
74
  @icon={{icon iconUrl=vehicle.avatar_url iconSize=(array 24 24)}}
74
75
  @onAdd={{fn this.trigger "onVehicleAdded" vehicle}}
75
76
  @onClick={{fn this.trigger "onVehicleClicked" vehicle}}
@@ -83,6 +84,8 @@
83
84
  </div>
84
85
  <div class="flex-1">
85
86
  <div class="text-xs font-semibold">{{vehicle.displayName}}</div>
87
+ <div class="text-xs">ID: {{n-a vehicle.public_id}}</div>
88
+ <div class="text-xs">Serial No: {{n-a vehicle.serial_number vehicle.vin vehicle.internal_id vehicle.id}}</div>
86
89
  <div class="text-xs">Driver: {{n-a vehicle.driver_name}}</div>
87
90
  <div class="text-xs">Status: <span class="{{if vehicle.online 'text-green-500' 'text-red-400'}}">{{if vehicle.online "Online" "Offline"}}</span></div>
88
91
  <div class="text-xs truncate">Pos: {{point-coordinates vehicle.location}}</div>
@@ -95,9 +98,11 @@
95
98
  <div>•</div>
96
99
  <div class="text-xs {{if vehicle.online 'text-green-500' 'text-red-400'}}">{{if vehicle.online "Online" "Offline"}}</div>
97
100
  </div>
101
+ <div class="text-xs">ID: {{n-a vehicle.public_id}}</div>
102
+ <div class="text-xs">Serial No: {{or vehicle.serial_number vehicle.vin vehicle.internal_id vehicle.id "-"}}</div>
98
103
  <div class="text-xs truncate"><FaIcon @icon="location-dot" @size="xs" class="mr-0.5" />{{point-coordinates vehicle.location}}</div>
99
104
  </marker.tooltip>
100
- </layers.marker>
105
+ </layers.tracking-marker>
101
106
  {{/each}}
102
107
 
103
108
  {{#each this.places as |place|}}
@@ -1,5 +1,5 @@
1
1
  <ContentPanel @title={{t "order.fields.payload"}} @open={{true}} @actionButtons={{this.actionButtons}} @wrapperClass="bordered-top">
2
- {{#if @resource.isMultiDropOrder}}
2
+ {{#if @resource.isMultiDrop}}
3
3
  <div class="space-y-2">
4
4
  {{#each @resource.entitiesByDestination as |group|}}
5
5
  <div class="rounded-md border border-gray-200 dark:border-gray-900 p-3">
@@ -40,13 +40,15 @@
40
40
  <DropdownButton
41
41
  @triggerClass="mr-2"
42
42
  @iconClass="icon-text-height"
43
- @buttonSize="sm"
43
+ @size="xs"
44
+ @buttonSize="xs"
44
45
  @icon="ellipsis-h"
45
46
  @iconPrefix="fas"
46
47
  @contentClass="dropdown-menu"
48
+ @renderInPlace={{true}}
47
49
  as |dd|
48
50
  >
49
- <div class="next-dd-menu mt-1 mx-0" aria-labelledby="user-menu">
51
+ <div class="next-dd-menu mx-0 mt-0i" aria-labelledby="user-menu">
50
52
  <div class="px-1">
51
53
  <div class="text-sm flex flex-row items-center px-3 py-1 rounded-md my-1 text-gray-300">
52
54
  {{t "order.fields.waypoint-actions"}}
@@ -72,7 +74,7 @@
72
74
  @icon="plus"
73
75
  @iconPrefix="fas"
74
76
  @text={{t "order.fields.add-item-button"}}
75
- @size="sm"
77
+ @size="xs"
76
78
  @onClick={{perform this.addEntity group.waypoint}}
77
79
  @permission="fleet-ops update order"
78
80
  />
@@ -13,6 +13,8 @@ export default class OrderDetailsPayloadComponent extends Component {
13
13
  @service intl;
14
14
 
15
15
  get actionButtons() {
16
+ if (this.args.resource.isMultiDrop) return [];
17
+
16
18
  return [
17
19
  {
18
20
  type: 'default',
@@ -142,7 +142,7 @@ export default class OrderConfigManagerCustomFieldsComponent extends Component {
142
142
  });
143
143
 
144
144
  this.modalsManager.show('modals/custom-field-group-form', {
145
- title: this.intl.t('fleet-ops.component.modals.new-custom-field-group.modal-title'),
145
+ title: this.intl.t('modals.new-custom-field-group.modal-title'),
146
146
  acceptButtonIcon: 'check',
147
147
  acceptButtonIconPrefix: 'fas',
148
148
  declineButtonIcon: 'times',