@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
@@ -1,18 +1,21 @@
1
1
  import Controller from '@ember/controller';
2
2
  import { inject as service } from '@ember/service';
3
3
  import { tracked } from '@glimmer/tracking';
4
+ import fleetOpsOptions from '../../../utils/fleet-ops-options';
4
5
 
5
6
  export default class ConnectivityTelematicsIndexController extends Controller {
6
7
  @service telematicActions;
7
8
  @service intl;
8
9
 
9
10
  /** query params */
10
- @tracked queryParams = ['name', 'page', 'limit', 'sort', 'query', 'public_id', 'created_at', 'updated_at'];
11
+ @tracked queryParams = ['name', 'provider', 'status', 'page', 'limit', 'sort', 'query', 'public_id', 'created_at', 'updated_at'];
11
12
  @tracked page = 1;
12
13
  @tracked limit;
13
14
  @tracked sort = '-created_at';
14
15
  @tracked public_id;
15
16
  @tracked name;
17
+ @tracked provider;
18
+ @tracked status;
16
19
 
17
20
  /** action buttons */
18
21
  @tracked actionButtons = [
@@ -54,25 +57,32 @@ export default class ConnectivityTelematicsIndexController extends Controller {
54
57
  /** columns */
55
58
  @tracked columns = [
56
59
  {
57
- label: this.intl.t('column.name'),
58
- valuePath: 'name',
59
- width: '180px',
60
+ label: 'Provider',
61
+ valuePath: 'provider',
60
62
  cellComponent: 'table/cell/anchor',
61
63
  cellClassNames: 'uppercase',
62
64
  action: this.telematicActions.transition.view,
63
65
  permission: 'fleet-ops view telematic',
64
- hidden: true,
65
66
  resizable: true,
66
67
  sortable: true,
67
68
  filterable: true,
68
69
  filterParam: 'name',
69
70
  filterComponent: 'filter/string',
70
71
  },
72
+ {
73
+ label: this.intl.t('column.status'),
74
+ valuePath: 'status',
75
+ cellComponent: 'table/cell/status',
76
+ resizable: true,
77
+ sortable: true,
78
+ filterable: true,
79
+ filterComponent: 'filter/multi-option',
80
+ filterOptions: fleetOpsOptions('telematicStatuses'),
81
+ },
71
82
  {
72
83
  label: this.intl.t('column.created-at'),
73
84
  valuePath: 'createdAt',
74
85
  sortParam: 'created_at',
75
- width: '10%',
76
86
  resizable: true,
77
87
  sortable: true,
78
88
  filterable: true,
@@ -82,7 +92,6 @@ export default class ConnectivityTelematicsIndexController extends Controller {
82
92
  label: this.intl.t('column.updated-at'),
83
93
  valuePath: 'updatedAt',
84
94
  sortParam: 'updated_at',
85
- width: '10%',
86
95
  resizable: true,
87
96
  sortable: true,
88
97
  hidden: true,
@@ -96,18 +105,18 @@ export default class ConnectivityTelematicsIndexController extends Controller {
96
105
  ddButtonText: false,
97
106
  ddButtonIcon: 'ellipsis-h',
98
107
  ddButtonIconPrefix: 'fas',
99
- ddMenuLabel: this.intl.t('common.resource-actions', { resource: this.intl.t('resource.Telematic') }),
108
+ ddMenuLabel: this.intl.t('common.resource-actions', { resource: this.intl.t('resource.telematic') }),
100
109
  cellClassNames: 'overflow-visible',
101
110
  wrapperClass: 'flex items-center justify-end mx-2',
102
111
  width: '10%',
103
112
  actions: [
104
113
  {
105
- label: this.intl.t('column.view-details'),
114
+ label: this.intl.t('common.view-resource', { resource: this.intl.t('resource.telematic') }),
106
115
  fn: this.telematicActions.transition.view,
107
116
  permission: 'fleet-ops view telematic',
108
117
  },
109
118
  {
110
- label: this.intl.t('column.edit-place'),
119
+ label: this.intl.t('common.edit-resource', { resource: this.intl.t('resource.telematic') }),
111
120
  fn: this.telematicActions.transition.edit,
112
121
  permission: 'fleet-ops update telematic',
113
122
  },
@@ -115,7 +124,7 @@ export default class ConnectivityTelematicsIndexController extends Controller {
115
124
  separator: true,
116
125
  },
117
126
  {
118
- label: this.intl.t('column.delete'),
127
+ label: this.intl.t('common.delete-resource', { resource: this.intl.t('resource.telematic') }),
119
128
  fn: this.telematicActions.delete,
120
129
  permission: 'fleet-ops delete telematic',
121
130
  },
@@ -1,27 +1,32 @@
1
1
  import Controller from '@ember/controller';
2
- import { tracked } from '@glimmer/tracking';
3
2
  import { inject as service } from '@ember/service';
4
3
 
5
4
  export default class ManagementFleetsIndexDetailsController extends Controller {
6
5
  @service hostRouter;
7
- @tracked tabs = [
8
- {
9
- route: 'management.fleets.index.details.index',
10
- label: 'Overview',
11
- },
12
- {
13
- route: 'management.fleets.index.details.vehicles',
14
- label: 'Vehicles',
15
- },
16
- {
17
- route: 'management.fleets.index.details.drivers',
18
- label: 'Drivers',
19
- },
20
- ];
21
- @tracked actionButtons = [
22
- {
23
- icon: 'pencil',
24
- fn: () => this.hostRouter.transitionTo('console.fleet-ops.management.fleets.index.edit', this.model),
25
- },
26
- ];
6
+
7
+ get tabs() {
8
+ return [
9
+ {
10
+ route: 'management.fleets.index.details.index',
11
+ label: 'Overview',
12
+ },
13
+ {
14
+ route: 'management.fleets.index.details.vehicles',
15
+ label: 'Vehicles',
16
+ },
17
+ {
18
+ route: 'management.fleets.index.details.drivers',
19
+ label: 'Drivers',
20
+ },
21
+ ];
22
+ }
23
+
24
+ get actionButtons() {
25
+ return [
26
+ {
27
+ icon: 'pencil',
28
+ fn: () => this.hostRouter.transitionTo('console.fleet-ops.management.fleets.index.edit', this.model),
29
+ },
30
+ ];
31
+ }
27
32
  }
@@ -10,12 +10,15 @@ export default class ManagementFleetsIndexEditController extends Controller {
10
10
  @service notifications;
11
11
  @service modalsManager;
12
12
  @tracked overlay;
13
- @tracked actionButtons = [
14
- {
15
- icon: 'eye',
16
- fn: this.view,
17
- },
18
- ];
13
+
14
+ get actionButtons() {
15
+ return [
16
+ {
17
+ icon: 'eye',
18
+ fn: this.view,
19
+ },
20
+ ];
21
+ }
19
22
 
20
23
  @task *save(fleet) {
21
24
  try {
@@ -1,19 +1,27 @@
1
1
  import Controller from '@ember/controller';
2
- import { tracked } from '@glimmer/tracking';
3
2
  import { inject as service } from '@ember/service';
4
3
 
5
4
  export default class ManagementVehiclesIndexDetailsController extends Controller {
6
5
  @service hostRouter;
7
- @tracked tabs = [
8
- {
9
- route: 'management.vehicles.index.details.index',
10
- label: 'Overview',
11
- },
12
- ];
13
- @tracked actionButtons = [
14
- {
15
- icon: 'pencil',
16
- fn: () => this.hostRouter.transitionTo('console.fleet-ops.management.vehicles.index.edit', this.model),
17
- },
18
- ];
6
+
7
+ get tabs() {
8
+ return [
9
+ {
10
+ route: 'management.vehicles.index.details.index',
11
+ label: 'Overview',
12
+ },
13
+ {
14
+ route: 'management.vehicles.index.details.positions',
15
+ label: 'Positions',
16
+ },
17
+ ];
18
+ }
19
+ get actionButtons() {
20
+ return [
21
+ {
22
+ icon: 'pencil',
23
+ fn: () => this.hostRouter.transitionTo('console.fleet-ops.management.vehicles.index.edit', this.model),
24
+ },
25
+ ];
26
+ }
19
27
  }
@@ -33,6 +33,12 @@ export default class SettingsCustomFieldsController extends Controller {
33
33
  label: 'Place',
34
34
  groups: [],
35
35
  },
36
+ {
37
+ model: 'entity',
38
+ type: 'fleet-ops:entity',
39
+ label: 'Entity',
40
+ groups: [],
41
+ },
36
42
  {
37
43
  model: 'fleet',
38
44
  type: 'fleet-ops:fleet',
@@ -0,0 +1,11 @@
1
+ import { helper } from '@ember/component/helper';
2
+ import fleetOpsOptions from '../utils/fleet-ops-options';
3
+
4
+ export function getFleetOpsOptionLabel(optionsKey, value) {
5
+ const allOptions = fleetOpsOptions(optionsKey);
6
+ return allOptions.find((opt) => opt.value === value)?.label ?? null;
7
+ }
8
+
9
+ export default helper(function getFleetOpsOptionLabelHelper([optionsKey, value]) {
10
+ return getFleetOpsOptionLabel(optionsKey, value);
11
+ });
@@ -1,3 +1,29 @@
1
1
  import Route from '@ember/routing/route';
2
+ import { inject as service } from '@ember/service';
3
+ import { action } from '@ember/object';
2
4
 
3
- export default class ConnectivityDevicesIndexDetailsRoute extends Route {}
5
+ export default class ConnectivityDevicesIndexDetailsRoute extends Route {
6
+ @service store;
7
+ @service notifications;
8
+ @service hostRouter;
9
+ @service abilities;
10
+ @service intl;
11
+
12
+ @action error(error) {
13
+ this.notifications.serverError(error);
14
+ if (typeof error.message === 'string' && error.message.endsWith('not found')) {
15
+ return this.hostRouter.transitionTo('console.fleet-ops.connectivity.devices.index');
16
+ }
17
+ }
18
+
19
+ beforeModel() {
20
+ if (this.abilities.cannot('fleet-ops view device')) {
21
+ this.notifications.warning(this.intl.t('common.unauthorized-access'));
22
+ return this.hostRouter.transitionTo('console.fleet-ops.connectivity.devices.index');
23
+ }
24
+ }
25
+
26
+ model({ public_id }) {
27
+ return this.store.findRecord('device', public_id);
28
+ }
29
+ }
@@ -1,3 +1,29 @@
1
1
  import Route from '@ember/routing/route';
2
+ import { inject as service } from '@ember/service';
3
+ import { action } from '@ember/object';
2
4
 
3
- export default class ConnectivityDevicesIndexEditRoute extends Route {}
5
+ export default class ConnectivityDevicesIndexEditRoute extends Route {
6
+ @service store;
7
+ @service notifications;
8
+ @service hostRouter;
9
+ @service abilities;
10
+ @service intl;
11
+
12
+ @action error(error) {
13
+ this.notifications.serverError(error);
14
+ if (typeof error.message === 'string' && error.message.endsWith('not found')) {
15
+ return this.hostRouter.transitionTo('console.fleet-ops.connectivity.devices.index');
16
+ }
17
+ }
18
+
19
+ beforeModel() {
20
+ if (this.abilities.cannot('fleet-ops update device')) {
21
+ this.notifications.warning(this.intl.t('common.unauthorized-access'));
22
+ return this.hostRouter.transitionTo('console.fleet-ops.connectivity.devices.index');
23
+ }
24
+ }
25
+
26
+ model({ public_id }) {
27
+ return this.store.findRecord('device', public_id);
28
+ }
29
+ }
@@ -1,3 +1,29 @@
1
1
  import Route from '@ember/routing/route';
2
+ import { inject as service } from '@ember/service';
3
+ import { action } from '@ember/object';
2
4
 
3
- export default class ConnectivitySensorsIndexDetailsRoute extends Route {}
5
+ export default class ConnectivitySensorsIndexDetailsRoute extends Route {
6
+ @service store;
7
+ @service notifications;
8
+ @service hostRouter;
9
+ @service abilities;
10
+ @service intl;
11
+
12
+ @action error(error) {
13
+ this.notifications.serverError(error);
14
+ if (typeof error.message === 'string' && error.message.endsWith('not found')) {
15
+ return this.hostRouter.transitionTo('console.fleet-ops.connectivity.sensors.index');
16
+ }
17
+ }
18
+
19
+ beforeModel() {
20
+ if (this.abilities.cannot('fleet-ops view sensor')) {
21
+ this.notifications.warning(this.intl.t('common.unauthorized-access'));
22
+ return this.hostRouter.transitionTo('console.fleet-ops.connectivity.sensors.index');
23
+ }
24
+ }
25
+
26
+ model({ public_id }) {
27
+ return this.store.findRecord('sensor', public_id);
28
+ }
29
+ }
@@ -1,3 +1,29 @@
1
1
  import Route from '@ember/routing/route';
2
+ import { inject as service } from '@ember/service';
3
+ import { action } from '@ember/object';
2
4
 
3
- export default class ConnectivitySensorsIndexEditRoute extends Route {}
5
+ export default class ConnectivitySensorsIndexEditRoute extends Route {
6
+ @service store;
7
+ @service notifications;
8
+ @service hostRouter;
9
+ @service abilities;
10
+ @service intl;
11
+
12
+ @action error(error) {
13
+ this.notifications.serverError(error);
14
+ if (typeof error.message === 'string' && error.message.endsWith('not found')) {
15
+ return this.hostRouter.transitionTo('console.fleet-ops.connectivity.sensors.index');
16
+ }
17
+ }
18
+
19
+ beforeModel() {
20
+ if (this.abilities.cannot('fleet-ops update sensor')) {
21
+ this.notifications.warning(this.intl.t('common.unauthorized-access'));
22
+ return this.hostRouter.transitionTo('console.fleet-ops.connectivity.sensors.index');
23
+ }
24
+ }
25
+
26
+ model({ public_id }) {
27
+ return this.store.findRecord('sensor', public_id);
28
+ }
29
+ }
@@ -1,3 +1,29 @@
1
1
  import Route from '@ember/routing/route';
2
+ import { inject as service } from '@ember/service';
3
+ import { action } from '@ember/object';
2
4
 
3
- export default class ConnectivityTelematicsIndexDetailsRoute extends Route {}
5
+ export default class ConnectivityTelematicsIndexDetailsRoute extends Route {
6
+ @service store;
7
+ @service notifications;
8
+ @service hostRouter;
9
+ @service abilities;
10
+ @service intl;
11
+
12
+ @action error(error) {
13
+ this.notifications.serverError(error);
14
+ if (typeof error.message === 'string' && error.message.endsWith('not found')) {
15
+ return this.hostRouter.transitionTo('console.fleet-ops.connectivity.telematics.index');
16
+ }
17
+ }
18
+
19
+ beforeModel() {
20
+ if (this.abilities.cannot('fleet-ops view telematic')) {
21
+ this.notifications.warning(this.intl.t('common.unauthorized-access'));
22
+ return this.hostRouter.transitionTo('console.fleet-ops.connectivity.telematics.index');
23
+ }
24
+ }
25
+
26
+ model({ public_id }) {
27
+ return this.store.findRecord('telematic', public_id);
28
+ }
29
+ }
@@ -1,3 +1,29 @@
1
1
  import Route from '@ember/routing/route';
2
+ import { inject as service } from '@ember/service';
3
+ import { action } from '@ember/object';
2
4
 
3
- export default class ConnectivityTelematicsIndexEditRoute extends Route {}
5
+ export default class ConnectivityTelematicsIndexEditRoute extends Route {
6
+ @service store;
7
+ @service notifications;
8
+ @service hostRouter;
9
+ @service abilities;
10
+ @service intl;
11
+
12
+ @action error(error) {
13
+ this.notifications.serverError(error);
14
+ if (typeof error.message === 'string' && error.message.endsWith('not found')) {
15
+ return this.hostRouter.transitionTo('console.fleet-ops.connectivity.telematics.index');
16
+ }
17
+ }
18
+
19
+ beforeModel() {
20
+ if (this.abilities.cannot('fleet-ops update telematic')) {
21
+ this.notifications.warning(this.intl.t('common.unauthorized-access'));
22
+ return this.hostRouter.transitionTo('console.fleet-ops.connectivity.telematics.index');
23
+ }
24
+ }
25
+
26
+ model({ public_id }) {
27
+ return this.store.findRecord('telematic', public_id);
28
+ }
29
+ }
@@ -0,0 +1,3 @@
1
+ import Route from '@ember/routing/route';
2
+
3
+ export default class ManagementVehiclesIndexDetailsPositionsRoute extends Route {}
package/addon/routes.js CHANGED
@@ -71,6 +71,7 @@ export default buildRoutes(function () {
71
71
  this.route('new');
72
72
  this.route('details', { path: '/:public_id' }, function () {
73
73
  this.route('index', { path: '/' });
74
+ this.route('positions');
74
75
  });
75
76
  this.route('edit', { path: '/edit/:public_id' });
76
77
  });
@@ -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
  }