@fleetbase/fleetops-engine 0.6.21 → 0.6.22

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 (61) hide show
  1. package/addon/components/device/card.hbs +1 -0
  2. package/addon/components/device/card.js +3 -0
  3. package/addon/components/device/manager.hbs +29 -0
  4. package/addon/components/device/manager.js +95 -0
  5. package/addon/components/device/pill.hbs +16 -0
  6. package/addon/components/device/pill.js +3 -0
  7. package/addon/components/driver/details.hbs +4 -0
  8. package/addon/components/driver/details.js +19 -1
  9. package/addon/components/driver/form.hbs +13 -2
  10. package/addon/components/driver/pill.hbs +17 -0
  11. package/addon/components/driver/pill.js +3 -0
  12. package/addon/components/map/drawer/device-event-listing.hbs +6 -0
  13. package/addon/components/map/drawer/position-listing.hbs +35 -19
  14. package/addon/components/map/drawer/position-listing.js +230 -64
  15. package/addon/components/modals/attach-device.hbs +18 -0
  16. package/addon/components/modals/attach-device.js +3 -0
  17. package/addon/components/order/details/detail.hbs +2 -54
  18. package/addon/components/order/details/detail.js +1 -0
  19. package/addon/components/order/pill.hbs +34 -0
  20. package/addon/components/order/pill.js +3 -0
  21. package/addon/components/positions-replay.hbs +26 -20
  22. package/addon/components/positions-replay.js +100 -63
  23. package/addon/components/telematic/form.hbs +4 -4
  24. package/addon/components/vehicle/card.hbs +1 -1
  25. package/addon/components/vehicle/details.hbs +4 -0
  26. package/addon/components/vehicle/details.js +19 -1
  27. package/addon/components/vehicle/form.hbs +4 -0
  28. package/addon/components/vehicle/pill.hbs +34 -0
  29. package/addon/components/vehicle/pill.js +3 -0
  30. package/addon/controllers/management/vehicles/index/details.js +5 -0
  31. package/addon/routes/management/drivers/index/details/positions.js +3 -0
  32. package/addon/routes.js +3 -0
  33. package/addon/services/position-playback.js +486 -0
  34. package/addon/services/resource-metadata.js +46 -0
  35. package/addon/templates/management/drivers/index/details/positions.hbs +2 -0
  36. package/addon/templates/management/vehicles/index/details/devices.hbs +1 -2
  37. package/app/components/device/card.js +1 -0
  38. package/app/components/device/manager.js +1 -0
  39. package/app/components/device/pill.js +1 -0
  40. package/app/components/driver/pill.js +1 -0
  41. package/app/components/modals/attach-device.js +1 -0
  42. package/app/components/order/pill.js +1 -0
  43. package/app/components/vehicle/pill.js +1 -0
  44. package/app/routes/management/drivers/index/details/positions.js +1 -0
  45. package/app/services/position-playback.js +1 -0
  46. package/app/services/resource-metadata.js +1 -0
  47. package/app/templates/management/drivers/index/details/positions.js +1 -0
  48. package/composer.json +1 -1
  49. package/extension.json +1 -1
  50. package/package.json +2 -2
  51. package/server/src/Http/Controllers/{Internal/v1/TelematicWebhookController.php → TelematicWebhookController.php} +1 -2
  52. package/server/src/Http/Resources/v1/Position.php +1 -1
  53. package/server/src/Models/Asset.php +10 -8
  54. package/server/src/Models/Device.php +11 -6
  55. package/server/src/Models/DeviceEvent.php +26 -3
  56. package/server/src/Models/Maintenance.php +15 -12
  57. package/server/src/Models/Part.php +2 -0
  58. package/server/src/Models/Position.php +10 -0
  59. package/server/src/Models/TrackingNumber.php +3 -1
  60. package/server/src/Models/WorkOrder.php +8 -5
  61. package/server/src/routes.php +12 -0
@@ -0,0 +1,486 @@
1
+ import Service from '@ember/service';
2
+ import { tracked } from '@glimmer/tracking';
3
+ import { task, timeout } from 'ember-concurrency';
4
+ import { debug } from '@ember/debug';
5
+ import { isArray } from '@ember/array';
6
+
7
+ /**
8
+ * PositionPlayback Service
9
+ *
10
+ * Client-side service for replaying historical position data with full playback controls.
11
+ * Unlike movement-tracker which uses socket connections for real-time tracking,
12
+ * this service handles pre-loaded position data entirely on the client side.
13
+ *
14
+ * Features:
15
+ * - Play/Pause/Stop controls
16
+ * - Step forward/backward through positions
17
+ * - Adjustable playback speed (can be changed during playback)
18
+ * - Jump to specific position
19
+ * - Progress callbacks
20
+ * - Automatic marker animation with rotation
21
+ * - Real-time replay: respects actual time intervals between positions
22
+ */
23
+ export default class PositionPlaybackService extends Service {
24
+ @tracked isPlaying = false;
25
+ @tracked isPaused = false;
26
+ @tracked currentIndex = 0;
27
+ @tracked positions = [];
28
+ @tracked speed = 1;
29
+ @tracked marker = null;
30
+ @tracked map = null;
31
+ @tracked callback = null;
32
+
33
+ /**
34
+ * Initialize replay session with positions and marker
35
+ *
36
+ * @param {Object} options - Configuration options
37
+ * @param {Object} options.subject - Model/subject being tracked (must have leafletLayer property)
38
+ * @param {Object} options.leafletLayer - Optional manual leaflet layer instance (overrides subject.leafletLayer)
39
+ * @param {Array} options.positions - Array of position objects to replay
40
+ * @param {Number} options.speed - Initial playback speed multiplier (default: 1)
41
+ * @param {Function} options.callback - Callback function called after each position update
42
+ * @param {Object} options.map - Optional Leaflet map instance for auto-panning
43
+ */
44
+ initialize(options = {}) {
45
+ const { subject, leafletLayer, positions = [], speed = 1, callback = null, map = null } = options;
46
+
47
+ // Get marker from subject or manual layer
48
+ this.marker = leafletLayer || subject?.leafletLayer || subject?._layer || subject?._marker;
49
+
50
+ if (!this.marker) {
51
+ debug('[PositionPlayback] Warning: No leaflet marker found. Marker must be provided or subject must have leafletLayer property.');
52
+ }
53
+
54
+ this.positions = positions;
55
+ this.speed = speed;
56
+ this.callback = callback;
57
+ this.map = map;
58
+ this.currentIndex = 0;
59
+ this.isPlaying = false;
60
+ this.isPaused = false;
61
+
62
+ debug(`[PositionPlayback] Initialized with ${positions.length} positions at ${speed}x speed`);
63
+ }
64
+
65
+ /**
66
+ * Start or resume playback
67
+ */
68
+ play() {
69
+ if (this.positions.length === 0) {
70
+ debug('[PositionPlayback] Cannot play: No positions loaded');
71
+ return;
72
+ }
73
+
74
+ if (this.isPlaying) {
75
+ debug('[PositionPlayback] Already playing');
76
+ return;
77
+ }
78
+
79
+ // If we're at the end, restart from beginning
80
+ if (this.currentIndex >= this.positions.length) {
81
+ this.currentIndex = 0;
82
+ }
83
+
84
+ this.isPlaying = true;
85
+ this.isPaused = false;
86
+
87
+ debug(`[PositionPlayback] Starting playback from position ${this.currentIndex + 1}/${this.positions.length}`);
88
+
89
+ this.playbackTask.perform();
90
+ }
91
+
92
+ /**
93
+ * Pause playback (can be resumed)
94
+ */
95
+ pause() {
96
+ if (!this.isPlaying) {
97
+ debug('[PositionPlayback] Not playing');
98
+ return;
99
+ }
100
+
101
+ this.isPlaying = false;
102
+ this.isPaused = true;
103
+
104
+ debug(`[PositionPlayback] Paused at position ${this.currentIndex + 1}/${this.positions.length}`);
105
+ }
106
+
107
+ /**
108
+ * Stop playback and reset to beginning
109
+ */
110
+ stop() {
111
+ this.isPlaying = false;
112
+ this.isPaused = false;
113
+ this.currentIndex = 0;
114
+
115
+ debug('[PositionPlayback] Stopped and reset to beginning');
116
+ }
117
+
118
+ /**
119
+ * Set playback speed (can be changed during playback)
120
+ *
121
+ * @param {Number} speed - Speed multiplier (e.g., 1 = normal, 2 = 2x speed, 0.5 = half speed)
122
+ */
123
+ setSpeed(speed) {
124
+ this.speed = speed;
125
+ debug(`[PositionPlayback] Speed changed to ${speed}x`);
126
+ }
127
+
128
+ /**
129
+ * Step forward by N positions
130
+ *
131
+ * @param {Number} steps - Number of positions to step forward (default: 1)
132
+ */
133
+ stepForward(steps = 1) {
134
+ const targetIndex = Math.min(this.currentIndex + steps, this.positions.length - 1);
135
+
136
+ if (targetIndex === this.currentIndex) {
137
+ debug('[PositionPlayback] Already at last position');
138
+ return;
139
+ }
140
+
141
+ this.jumpToPosition(targetIndex);
142
+ debug(`[PositionPlayback] Stepped forward ${steps} position(s) to ${targetIndex + 1}/${this.positions.length}`);
143
+ }
144
+
145
+ /**
146
+ * Step backward by N positions
147
+ *
148
+ * @param {Number} steps - Number of positions to step backward (default: 1)
149
+ */
150
+ stepBackward(steps = 1) {
151
+ const targetIndex = Math.max(this.currentIndex - steps, 0);
152
+
153
+ if (targetIndex === this.currentIndex) {
154
+ debug('[PositionPlayback] Already at first position');
155
+ return;
156
+ }
157
+
158
+ this.jumpToPosition(targetIndex);
159
+ debug(`[PositionPlayback] Stepped backward ${steps} position(s) to ${targetIndex + 1}/${this.positions.length}`);
160
+ }
161
+
162
+ /**
163
+ * Jump to specific position index (no animation)
164
+ *
165
+ * @param {Number} index - Target position index (0-based)
166
+ */
167
+ jumpToPosition(index) {
168
+ if (index < 0 || index >= this.positions.length) {
169
+ debug(`[PositionPlayback] Invalid index: ${index}`);
170
+ return;
171
+ }
172
+
173
+ const position = this.positions[index];
174
+ this.currentIndex = index;
175
+
176
+ if (!this.marker || !position) {
177
+ return;
178
+ }
179
+
180
+ // Update marker position without animation
181
+ const latLng = this.#getLatLngFromPosition(position);
182
+ if (latLng) {
183
+ // Update rotation if heading is available
184
+ if (typeof this.marker.setRotationAngle === 'function' && Number.isFinite(position.heading) && position.heading !== -1) {
185
+ this.marker.setRotationAngle(position.heading);
186
+ }
187
+
188
+ if (typeof this.marker.slideTo === 'function') {
189
+ this.marker.slideTo(latLng, { duration: 100 });
190
+ } else {
191
+ this.marker.setLatLng(latLng);
192
+ requestAnimationFrame(() => {
193
+ if (typeof this.marker.setRotationAngle === 'function' && Number.isFinite(position.heading) && position.heading !== -1) {
194
+ this.marker.setRotationAngle(position.heading);
195
+ }
196
+ });
197
+ }
198
+
199
+ // Pan map to position if map is provided
200
+ if (this.map) {
201
+ this.map.panTo(latLng, { animate: true });
202
+ }
203
+
204
+ // Trigger callback
205
+ this.#triggerCallback(position, index, { animated: false });
206
+ }
207
+
208
+ debug(`[PositionPlayback] Jumped to position ${index + 1}/${this.positions.length}`);
209
+ }
210
+
211
+ /**
212
+ * Get current playback progress as percentage
213
+ *
214
+ * @returns {Number} Progress percentage (0-100)
215
+ */
216
+ getProgress() {
217
+ if (this.positions.length === 0) {
218
+ return 0;
219
+ }
220
+ return Math.round((this.currentIndex / this.positions.length) * 100);
221
+ }
222
+
223
+ /**
224
+ * Get current position data
225
+ *
226
+ * @returns {Object|null} Current position object or null
227
+ */
228
+ getCurrentPosition() {
229
+ return this.positions[this.currentIndex] || null;
230
+ }
231
+
232
+ /**
233
+ * Reset replay state
234
+ */
235
+ reset() {
236
+ this.stop();
237
+ this.positions = [];
238
+ this.marker = null;
239
+ this.map = null;
240
+ this.callback = null;
241
+ this.speed = 1;
242
+
243
+ debug('[PositionPlayback] Reset complete');
244
+ }
245
+
246
+ /**
247
+ * Main playback task using ember-concurrency
248
+ * Handles sequential position updates with timing based on real intervals
249
+ */
250
+ @task *playbackTask() {
251
+ debug(`[PositionPlayback] Playback task started from position ${this.currentIndex}`);
252
+
253
+ while (this.isPlaying && this.currentIndex < this.positions.length) {
254
+ const position = this.positions[this.currentIndex];
255
+ const nextPosition = this.positions[this.currentIndex + 1];
256
+
257
+ if (!position) {
258
+ debug(`[PositionPlayback] Invalid position at index ${this.currentIndex}`);
259
+ this.currentIndex++;
260
+ continue;
261
+ }
262
+
263
+ // Get marker (it might have been updated)
264
+ const marker = this.marker;
265
+ if (!marker || !marker._map) {
266
+ debug('[PositionPlayback] Marker not available or not on map');
267
+ this.currentIndex++;
268
+ continue;
269
+ }
270
+
271
+ // Calculate next position
272
+ const latLng = this.#getLatLngFromPosition(position);
273
+ if (!latLng) {
274
+ debug(`[PositionPlayback] Invalid coordinates for position ${this.currentIndex}`);
275
+ this.currentIndex++;
276
+ continue;
277
+ }
278
+
279
+ // Calculate animation duration based on distance and speed
280
+ const animationDuration = this.#calculateAnimationDuration(marker, latLng, position);
281
+
282
+ try {
283
+ // Apply rotation if heading is valid
284
+ if (typeof marker.setRotationAngle === 'function' && Number.isFinite(position.heading) && position.heading !== -1) {
285
+ marker.setRotationAngle(position.heading);
286
+ }
287
+
288
+ // Move marker with animation
289
+ if (typeof marker.slideTo === 'function') {
290
+ marker.slideTo(latLng, { duration: animationDuration });
291
+ } else {
292
+ marker.setLatLng(latLng);
293
+ }
294
+
295
+ // Pan map to follow marker if map is provided
296
+ if (this.map) {
297
+ const targetLatLng = marker._slideToLatLng ?? marker.getLatLng();
298
+ this.map.panTo(targetLatLng, { animate: true });
299
+ }
300
+
301
+ // Trigger callback
302
+ this.#triggerCallback(position, this.currentIndex, { duration: animationDuration, animated: true });
303
+
304
+ // Wait for animation to complete
305
+ yield timeout(animationDuration + 50);
306
+
307
+ // Calculate delay until next position based on real-time interval
308
+ if (nextPosition) {
309
+ const delayUntilNext = this.#calculateDelayToNextPosition(position, nextPosition);
310
+
311
+ if (delayUntilNext > 0) {
312
+ debug(`[PositionPlayback] Waiting ${delayUntilNext}ms until next position (${this.speed}x speed)`);
313
+ yield timeout(delayUntilNext);
314
+ }
315
+ }
316
+ } catch (err) {
317
+ debug(`[PositionPlayback] Error processing position ${this.currentIndex}: ${err.message}`);
318
+ }
319
+
320
+ // Move to next position
321
+ this.currentIndex++;
322
+ }
323
+
324
+ // Playback complete
325
+ if (this.currentIndex >= this.positions.length) {
326
+ this.isPlaying = false;
327
+ this.isPaused = false;
328
+ debug('[PositionPlayback] Playback complete');
329
+
330
+ // Trigger completion callback
331
+ if (typeof this.callback === 'function') {
332
+ this.callback({ type: 'complete', totalPositions: this.positions.length });
333
+ }
334
+ }
335
+ }
336
+
337
+ /**
338
+ * Calculate delay to next position based on real-time interval
339
+ *
340
+ * @private
341
+ * @param {Object} currentPosition - Current position object
342
+ * @param {Object} nextPosition - Next position object
343
+ * @returns {Number} Delay in milliseconds (adjusted by speed multiplier)
344
+ */
345
+ #calculateDelayToNextPosition(currentPosition, nextPosition) {
346
+ // Try to get timestamps from positions
347
+ const currentTime = this.#getTimestamp(currentPosition);
348
+ const nextTime = this.#getTimestamp(nextPosition);
349
+
350
+ if (!currentTime || !nextTime) {
351
+ // No timestamp data, use default delay
352
+ debug('[PositionPlayback] No timestamp data, using default delay');
353
+ return 1000 / this.speed; // 1 second default, adjusted by speed
354
+ }
355
+
356
+ // Calculate real-time interval in milliseconds
357
+ const realTimeInterval = nextTime - currentTime;
358
+
359
+ if (realTimeInterval <= 0) {
360
+ // Invalid interval, use minimum delay
361
+ return 100 / this.speed;
362
+ }
363
+
364
+ // Apply speed multiplier (higher speed = shorter delay)
365
+ const adjustedDelay = realTimeInterval / this.speed;
366
+
367
+ // Clamp between reasonable bounds (50ms to 60 seconds)
368
+ // At high speeds, we don't want delays too short
369
+ // At low speeds, we cap at 60 seconds to prevent extremely long waits
370
+ return Math.max(50, Math.min(adjustedDelay, 60000));
371
+ }
372
+
373
+ /**
374
+ * Get timestamp from position object
375
+ * Handles multiple timestamp field formats
376
+ *
377
+ * @private
378
+ * @param {Object} position - Position object
379
+ * @returns {Number|null} Timestamp in milliseconds or null
380
+ */
381
+ #getTimestamp(position) {
382
+ // Try different timestamp fields
383
+ const timestampFields = ['created_at', 'timestamp', 'recorded_at', 'time', 'datetime'];
384
+
385
+ for (const field of timestampFields) {
386
+ const value = position[field];
387
+ if (value) {
388
+ // Try to parse as date
389
+ const timestamp = new Date(value).getTime();
390
+ if (Number.isFinite(timestamp)) {
391
+ return timestamp;
392
+ }
393
+ }
394
+ }
395
+
396
+ return null;
397
+ }
398
+
399
+ /**
400
+ * Extract lat/lng from position object
401
+ * Handles multiple position data formats
402
+ *
403
+ * @private
404
+ * @param {Object} position - Position object
405
+ * @returns {Array|null} [lat, lng] or null if invalid
406
+ */
407
+ #getLatLngFromPosition(position) {
408
+ // Direct latitude/longitude properties
409
+ if (Number.isFinite(position.latitude) && Number.isFinite(position.longitude)) {
410
+ return [position.latitude, position.longitude];
411
+ }
412
+
413
+ // GeoJSON format in location.coordinates [lng, lat]
414
+ if (position.location?.coordinates && isArray(position.location.coordinates)) {
415
+ const [lng, lat] = position.location.coordinates;
416
+ if (Number.isFinite(lat) && Number.isFinite(lng)) {
417
+ return [lat, lng];
418
+ }
419
+ }
420
+
421
+ // Coordinates array [lat, lng]
422
+ if (position.coordinates && isArray(position.coordinates)) {
423
+ const [lat, lng] = position.coordinates;
424
+ if (Number.isFinite(lat) && Number.isFinite(lng)) {
425
+ return [lat, lng];
426
+ }
427
+ }
428
+
429
+ return null;
430
+ }
431
+
432
+ /**
433
+ * Calculate animation duration based on distance and speed
434
+ * This is for the marker movement animation, separate from the delay between positions
435
+ *
436
+ * @private
437
+ * @param {Object} marker - Leaflet marker
438
+ * @param {Array} nextLatLng - Target [lat, lng]
439
+ * @param {Object} position - Position object with optional speed data
440
+ * @returns {Number} Duration in milliseconds
441
+ */
442
+ #calculateAnimationDuration(marker, nextLatLng, position) {
443
+ const map = marker._map;
444
+ const prev = marker.getLatLng();
445
+ const meters = map ? map.distance(prev, nextLatLng) : prev.distanceTo(nextLatLng);
446
+
447
+ // Get speed from position data (assume m/s)
448
+ let mps = Number.isFinite(position.speed) && position.speed > 0 ? position.speed : null;
449
+
450
+ // If speed is in km/h, convert to m/s
451
+ if (mps && position.speed_unit === 'kmh') {
452
+ mps = mps / 3.6;
453
+ }
454
+
455
+ // Calculate base duration for animation
456
+ let baseDuration = mps ? (meters / mps) * 1000 : 500;
457
+
458
+ // For animation, we want it relatively quick regardless of playback speed
459
+ // The playback speed affects the delay between positions, not the animation speed
460
+ // Clamp between 100ms and 1000ms for smooth animation
461
+ const duration = Math.max(100, Math.min(baseDuration, 1000));
462
+
463
+ return duration;
464
+ }
465
+
466
+ /**
467
+ * Trigger callback with position data
468
+ *
469
+ * @private
470
+ * @param {Object} position - Position object
471
+ * @param {Number} index - Current position index
472
+ * @param {Object} metadata - Additional metadata
473
+ */
474
+ #triggerCallback(position, index, metadata = {}) {
475
+ if (typeof this.callback === 'function') {
476
+ this.callback({
477
+ type: 'position',
478
+ position,
479
+ index,
480
+ totalPositions: this.positions.length,
481
+ progress: this.getProgress(),
482
+ ...metadata,
483
+ });
484
+ }
485
+ }
486
+ }
@@ -0,0 +1,46 @@
1
+ import Service, { inject as service } from '@ember/service';
2
+ import { action } from '@ember/object';
3
+ import getModelName from '@fleetbase/ember-core/utils/get-model-name';
4
+
5
+ export default class ResourceMetadataService extends Service {
6
+ @service modalsManager;
7
+ @service notifications;
8
+ @service intl;
9
+
10
+ @action view(resource, options = {}) {
11
+ this.modalsManager.show('modals/view-metadata', {
12
+ title: `${this.intl.t('resource.' + getModelName(resource))} ${this.intl.t('common.metadata')}`,
13
+ acceptButtonText: this.intl.t('common.done'),
14
+ hideDeclineButton: true,
15
+ metadata: resource.meta,
16
+ ...options,
17
+ });
18
+ }
19
+
20
+ @action edit(resource, options = {}) {
21
+ this.modalsManager.show('modals/edit-metadata', {
22
+ title: `${this.intl.t('common.edit')} ${this.intl.t('resource.' + getModelName(resource))} ${this.intl.t('common.metadata')}`,
23
+ acceptButtonText: this.intl.t('common.save-changes'),
24
+ acceptButtonIcon: 'save',
25
+ actionsWrapperClass: 'px-3',
26
+ metadata: resource.meta,
27
+ onChange: (meta) => {
28
+ resource.set('meta', meta);
29
+ },
30
+ confirm: async (modal) => {
31
+ modal.startLoading();
32
+
33
+ try {
34
+ await resource.save();
35
+ this.notifications.success(this.intl.t('common.field-saved', { field: this.intl.t('common.metadata') }));
36
+ modal.done();
37
+ } catch (error) {
38
+ this.notifications.serverError(error);
39
+ } finally {
40
+ modal.stopLoading();
41
+ }
42
+ },
43
+ ...options,
44
+ });
45
+ }
46
+ }
@@ -0,0 +1,2 @@
1
+ {{page-title "Positions"}}
2
+ {{outlet}}
@@ -1,2 +1 @@
1
-
2
- {{outlet}}
1
+ <Device::Manager @resource={{@model}} />
@@ -0,0 +1 @@
1
+ export { default } from '@fleetbase/fleetops-engine/components/device/card';
@@ -0,0 +1 @@
1
+ export { default } from '@fleetbase/fleetops-engine/components/device/manager';
@@ -0,0 +1 @@
1
+ export { default } from '@fleetbase/fleetops-engine/components/device/pill';
@@ -0,0 +1 @@
1
+ export { default } from '@fleetbase/fleetops-engine/components/driver/pill';
@@ -0,0 +1 @@
1
+ export { default } from '@fleetbase/fleetops-engine/components/modals/attach-device';
@@ -0,0 +1 @@
1
+ export { default } from '@fleetbase/fleetops-engine/components/order/pill';
@@ -0,0 +1 @@
1
+ export { default } from '@fleetbase/fleetops-engine/components/vehicle/pill';
@@ -0,0 +1 @@
1
+ export { default } from '@fleetbase/fleetops-engine/routes/management/drivers/index/details/positions';
@@ -0,0 +1 @@
1
+ export { default } from '@fleetbase/fleetops-engine/services/position-playback';
@@ -0,0 +1 @@
1
+ export { default } from '@fleetbase/fleetops-engine/services/resource-metadata';
@@ -0,0 +1 @@
1
+ export { default } from '@fleetbase/fleetops-engine/templates/management/drivers/index/details/positions';
package/composer.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fleetbase/fleetops-api",
3
- "version": "0.6.21",
3
+ "version": "0.6.22",
4
4
  "description": "Fleet & Transport Management Extension for Fleetbase",
5
5
  "keywords": [
6
6
  "fleetbase-extension",
package/extension.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "Fleet-Ops",
3
- "version": "0.6.21",
3
+ "version": "0.6.22",
4
4
  "description": "Fleet & Transport Management Extension for Fleetbase",
5
5
  "repository": "https://github.com/fleetbase/fleetops",
6
6
  "license": "AGPL-3.0-or-later",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fleetbase/fleetops-engine",
3
- "version": "0.6.21",
3
+ "version": "0.6.22",
4
4
  "description": "Fleet & Transport Management Extension for Fleetbase",
5
5
  "fleetbase": {
6
6
  "route": "fleet-ops"
@@ -43,7 +43,7 @@
43
43
  "dependencies": {
44
44
  "@babel/core": "^7.23.2",
45
45
  "@fleetbase/ember-core": "^0.3.6",
46
- "@fleetbase/ember-ui": "^0.3.7",
46
+ "@fleetbase/ember-ui": "^0.3.8",
47
47
  "@fleetbase/fleetops-data": "^0.1.21",
48
48
  "@fleetbase/leaflet-routing-machine": "^3.2.17",
49
49
  "@fortawesome/ember-fontawesome": "^2.0.0",
@@ -124,8 +124,7 @@ class TelematicWebhookController extends Controller
124
124
  */
125
125
  public function ingest(Request $request, string $id): JsonResponse
126
126
  {
127
- $telematic = Telematic::where('uuid', $id)->firstOrFail();
128
-
127
+ $telematic = Telematic::where('uuid', $id)->orWhere('public_id', $id)->firstOrFail();
129
128
  $correlationId = Str::uuid()->toString();
130
129
 
131
130
  Log::info('Custom ingest received', [
@@ -36,7 +36,7 @@ class Position extends FleetbaseResource
36
36
  'altitude' => $this->altitude ?? 0,
37
37
  'latitude' => $this->latitude ?? 0,
38
38
  'longitude' => $this->longitude ?? 0,
39
- 'coordinates' => $this->location ?? new Point(0, 0),
39
+ 'coordinates' => $this->coordinates ?? new Point(0, 0),
40
40
  'updated_at' => $this->updated_at,
41
41
  'created_at' => $this->created_at,
42
42
  ];
@@ -3,6 +3,7 @@
3
3
  namespace Fleetbase\FleetOps\Models;
4
4
 
5
5
  use Fleetbase\Casts\Json;
6
+ use Fleetbase\Casts\PolymorphicType;
6
7
  use Fleetbase\FleetOps\Casts\Point;
7
8
  use Fleetbase\Models\Category;
8
9
  use Fleetbase\Models\File;
@@ -157,14 +158,15 @@ class Asset extends Model
157
158
  * @var array
158
159
  */
159
160
  protected $casts = [
160
- 'year' => 'integer',
161
- 'odometer' => 'integer',
162
- 'engine_hours' => 'integer',
163
- 'gvw' => 'decimal:2',
164
- 'capacity' => Json::class,
165
- 'specs' => Json::class,
166
- 'attributes' => Json::class,
167
- 'location' => Point::class,
161
+ 'year' => 'integer',
162
+ 'odometer' => 'integer',
163
+ 'engine_hours' => 'integer',
164
+ 'gvw' => 'decimal:2',
165
+ 'capacity' => Json::class,
166
+ 'specs' => Json::class,
167
+ 'attributes' => Json::class,
168
+ 'location' => Point::class,
169
+ 'assigned_to_type' => PolymorphicType::class,
168
170
  ];
169
171
 
170
172
  /**