@fleetbase/fleetops-engine 0.6.55 → 0.6.56

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 (109) hide show
  1. package/addon/components/admin/map-settings.hbs +1 -1
  2. package/addon/components/device/details.js +1 -1
  3. package/addon/components/device-event/details.hbs +192 -0
  4. package/addon/components/device-event/details.js +151 -0
  5. package/addon/components/driver/schedule.js +1 -1
  6. package/addon/components/layout/fleet-ops-sidebar.js +15 -9
  7. package/addon/components/telematic/details.hbs +1 -1
  8. package/addon/components/telematic/form.hbs +2 -2
  9. package/addon/components/telematic/settings.hbs +1 -1
  10. package/addon/controllers/connectivity/devices/index/details/events.js +1 -1
  11. package/addon/controllers/connectivity/devices/index/details/vehicle.js +2 -2
  12. package/addon/controllers/connectivity/events/details.js +68 -1
  13. package/addon/controllers/connectivity/sensors/index.js +4 -0
  14. package/addon/controllers/connectivity/telematics/details/events.js +1 -1
  15. package/addon/controllers/connectivity/telematics/details/sensors.js +4 -0
  16. package/addon/controllers/settings/map.js +15 -0
  17. package/addon/instance-initializers/register-vroom-allocation.js +1 -1
  18. package/addon/routes/connectivity/events/details.js +26 -1
  19. package/addon/routes/management/vehicles/index/details.js +3 -1
  20. package/addon/services/map-adapter/google.js +25 -0
  21. package/addon/services/map-adapter/leaflet.js +1 -1
  22. package/addon/services/map-adapter-interface.js +1 -1
  23. package/addon/services/map-manager.js +4 -1
  24. package/addon/services/map-settings.js +15 -0
  25. package/addon/styles/fleetops-engine.css +1 -1
  26. package/addon/templates/connectivity/devices/index/details/vehicle.hbs +2 -1
  27. package/addon/templates/connectivity/events/details.hbs +40 -1
  28. package/addon/templates/settings/map.hbs +38 -9
  29. package/addon/utils/leaflet-plugin-loader.js +5 -5
  30. package/addon/utils/route-colors.js +1 -1
  31. package/app/components/device-event/details.hbs +192 -0
  32. package/app/components/device-event/details.js +1 -0
  33. package/composer.json +1 -1
  34. package/extension.json +1 -1
  35. package/package.json +2 -2
  36. package/server/migrations/2026_06_23_000001_delete_bad_afaqy_telematics_sensors.php +34 -0
  37. package/server/src/Exports/ContactExport.php +12 -5
  38. package/server/src/Exports/DeviceExport.php +92 -0
  39. package/server/src/Exports/DriverExport.php +28 -3
  40. package/server/src/Exports/EquipmentExport.php +94 -0
  41. package/server/src/Exports/FleetExport.php +22 -7
  42. package/server/src/Exports/FuelReportExport.php +16 -1
  43. package/server/src/Exports/IssueExport.php +25 -3
  44. package/server/src/Exports/MaintenanceExport.php +106 -0
  45. package/server/src/Exports/MaintenanceScheduleExport.php +99 -0
  46. package/server/src/Exports/OrderExport.php +24 -5
  47. package/server/src/Exports/PartExport.php +99 -0
  48. package/server/src/Exports/PlaceExport.php +33 -7
  49. package/server/src/Exports/SensorExport.php +94 -0
  50. package/server/src/Exports/ServiceAreaExport.php +31 -7
  51. package/server/src/Exports/ServiceRateExport.php +41 -1
  52. package/server/src/Exports/TelematicExport.php +90 -0
  53. package/server/src/Exports/VehicleExport.php +274 -8
  54. package/server/src/Exports/VendorExport.php +15 -2
  55. package/server/src/Exports/WorkOrderExport.php +92 -0
  56. package/server/src/Http/Controllers/Api/v1/Concerns/ResolvesFleetOpsApiResources.php +176 -0
  57. package/server/src/Http/Controllers/Api/v1/DeviceController.php +225 -0
  58. package/server/src/Http/Controllers/Api/v1/EquipmentController.php +114 -0
  59. package/server/src/Http/Controllers/Api/v1/FuelTransactionController.php +185 -0
  60. package/server/src/Http/Controllers/Api/v1/PartController.php +120 -0
  61. package/server/src/Http/Controllers/Api/v1/SensorController.php +132 -0
  62. package/server/src/Http/Controllers/Api/v1/WorkOrderController.php +161 -0
  63. package/server/src/Http/Controllers/Internal/v1/DeviceController.php +18 -0
  64. package/server/src/Http/Controllers/Internal/v1/EquipmentController.php +17 -0
  65. package/server/src/Http/Controllers/Internal/v1/MaintenanceController.php +17 -0
  66. package/server/src/Http/Controllers/Internal/v1/MaintenanceScheduleController.php +17 -0
  67. package/server/src/Http/Controllers/Internal/v1/PartController.php +17 -0
  68. package/server/src/Http/Controllers/Internal/v1/SensorController.php +18 -0
  69. package/server/src/Http/Controllers/Internal/v1/SettingController.php +29 -5
  70. package/server/src/Http/Controllers/Internal/v1/TelematicController.php +18 -0
  71. package/server/src/Http/Controllers/Internal/v1/WorkOrderController.php +17 -0
  72. package/server/src/Http/Filter/DeviceFilter.php +33 -10
  73. package/server/src/Http/Filter/EquipmentFilter.php +23 -0
  74. package/server/src/Http/Filter/FuelProviderTransactionFilter.php +57 -2
  75. package/server/src/Http/Filter/PartFilter.php +47 -0
  76. package/server/src/Http/Filter/SensorFilter.php +34 -6
  77. package/server/src/Http/Filter/WorkOrderFilter.php +23 -0
  78. package/server/src/Http/Requests/CreateDeviceRequest.php +51 -0
  79. package/server/src/Http/Requests/CreateEquipmentRequest.php +35 -0
  80. package/server/src/Http/Requests/CreateFuelTransactionRequest.php +50 -0
  81. package/server/src/Http/Requests/CreatePartRequest.php +40 -0
  82. package/server/src/Http/Requests/CreateSensorRequest.php +47 -0
  83. package/server/src/Http/Requests/CreateWorkOrderRequest.php +42 -0
  84. package/server/src/Http/Requests/UpdateDeviceRequest.php +7 -0
  85. package/server/src/Http/Requests/UpdateEquipmentRequest.php +7 -0
  86. package/server/src/Http/Requests/UpdateFuelTransactionRequest.php +7 -0
  87. package/server/src/Http/Requests/UpdatePartRequest.php +7 -0
  88. package/server/src/Http/Requests/UpdateSensorRequest.php +7 -0
  89. package/server/src/Http/Requests/UpdateWorkOrderRequest.php +7 -0
  90. package/server/src/Http/Resources/v1/Device.php +72 -0
  91. package/server/src/Http/Resources/v1/Equipment.php +57 -0
  92. package/server/src/Http/Resources/v1/FuelTransaction.php +69 -0
  93. package/server/src/Http/Resources/v1/Part.php +64 -0
  94. package/server/src/Http/Resources/v1/Sensor.php +68 -0
  95. package/server/src/Jobs/SyncTelematicDevicesJob.php +194 -38
  96. package/server/src/Models/Order.php +2 -1
  97. package/server/src/Observers/PurchaseRateObserver.php +2 -1
  98. package/server/src/Support/Analytics/RevenueTrend.php +3 -8
  99. package/server/src/Support/Metrics/ActiveRevenueQuery.php +121 -0
  100. package/server/src/Support/Metrics/AvgOrderValueMetric.php +1 -7
  101. package/server/src/Support/Metrics/EarningsMetric.php +1 -10
  102. package/server/src/Support/Telematics/Providers/AfaqyProvider.php +104 -5
  103. package/server/src/Support/Telematics/Providers/SafeeProvider.php +550 -54
  104. package/server/src/Support/Telematics/TelematicService.php +28 -9
  105. package/server/src/routes.php +67 -14
  106. package/server/tests/ExportCoverageTest.php +79 -0
  107. package/server/tests/MetricsRegistryTest.php +45 -5
  108. package/server/tests/PublicFleetOpsRestEndpointsTest.php +191 -0
  109. package/server/tests/TelematicsHardeningTest.php +636 -1
@@ -1,5 +1,5 @@
1
1
  <ContentPanel @title="Configure Map" @open={{true}} @wrapperClass="bordered-classic">
2
- <InputGroup @name="Default Map Provider" @helpText="Sets the default map provider for all organizations unless they override it in their own FleetOps map settings.">
2
+ <InputGroup @name="Default Map Provider" @helpText="Sets the default map provider for all organizations unless they override it in their own Fleet-Ops map settings.">
3
3
  <Select
4
4
  class="w-full flex-1"
5
5
  @value={{this.mapProvider}}
@@ -111,7 +111,7 @@ export default class DeviceDetailsComponent extends Component {
111
111
  }
112
112
 
113
113
  get unprocessedEventsCount() {
114
- return this.events.filter((event) => !event.processed_at && !event.is_processed).length;
114
+ return this.events.filter((event) => !event.processed_at).length;
115
115
  }
116
116
 
117
117
  get activeSensorsCount() {
@@ -0,0 +1,192 @@
1
+ <div class="flex flex-col gap-3 p-3" ...attributes>
2
+ <section class="overflow-hidden rounded-md border border-gray-200 bg-white shadow-sm dark:border-gray-700 dark:bg-gray-800">
3
+ <div class="flex flex-col gap-3 p-3 lg:flex-row lg:items-start lg:justify-between">
4
+ <div class="min-w-0">
5
+ <div class="flex flex-wrap items-center gap-2">
6
+ <h2 class="truncate text-base font-bold text-gray-900 dark:text-gray-100">{{smart-humanize this.eventType}}</h2>
7
+ <Badge @status={{this.severity}} @hideStatusDot={{true}}>{{smart-humanize this.severity}}</Badge>
8
+ <Badge @status={{this.processedStatus}} @hideStatusDot={{true}}>{{this.processedLabel}}</Badge>
9
+ </div>
10
+
11
+ <p class="mt-2 max-w-4xl text-sm leading-5 text-gray-700 dark:text-gray-200">{{n-a this.message}}</p>
12
+
13
+ <div class="mt-2 flex flex-wrap gap-2 text-xs text-gray-500 dark:text-gray-300">
14
+ <span>Occurred: {{n-a (format-date-fns this.occurredAt "dd MMM yyyy, HH:mm")}}</span>
15
+ <span>Processed: {{n-a (format-date-fns this.processedAt "dd MMM yyyy, HH:mm")}}</span>
16
+ <span>Public ID: {{n-a @resource.public_id}}</span>
17
+ <span>UUID: {{n-a @resource.id}}</span>
18
+ </div>
19
+ </div>
20
+ </div>
21
+ </section>
22
+
23
+ <section class="grid grid-cols-1 gap-2 sm:grid-cols-2 lg:grid-cols-4">
24
+ {{#each this.metrics as |metric|}}
25
+ <div class="fleet-ops-widget fleet-ops-kpi-tile fleetops-connectivity-kpi-tile {{metric.accentClass}} relative min-h-[82px] overflow-hidden rounded-md border bg-white shadow-sm dark:bg-gray-800">
26
+ <div class="flex h-full items-start justify-between gap-2 p-3">
27
+ <div class="min-w-0">
28
+ <div class="truncate text-[10px] font-bold uppercase text-gray-500 dark:text-gray-400">{{metric.label}}</div>
29
+ <div class="mt-1 truncate text-lg font-extrabold leading-none text-gray-900 dark:text-gray-50">{{n-a metric.value}}</div>
30
+ {{#if metric.meta}}
31
+ <div class="mt-1 truncate text-xs text-gray-500 dark:text-gray-300">{{metric.meta}}</div>
32
+ {{/if}}
33
+ </div>
34
+ <div class="fleetops-connectivity-kpi-icon flex h-8 w-8 flex-shrink-0 items-center justify-center rounded-md text-[13px]">
35
+ <FaIcon @icon={{metric.icon}} />
36
+ </div>
37
+ </div>
38
+ </div>
39
+ {{/each}}
40
+ </section>
41
+
42
+ <section class="grid grid-cols-1 gap-3 xl:grid-cols-2">
43
+ <div class="overflow-hidden rounded-md border border-gray-200 bg-white shadow-sm dark:border-gray-700 dark:bg-gray-800">
44
+ <div class="border-b border-gray-200 px-3 py-2 dark:border-gray-700">
45
+ <h3 class="text-sm font-bold text-gray-900 dark:text-gray-100">Event Details</h3>
46
+ </div>
47
+ <div class="grid grid-cols-1 gap-2 p-3 sm:grid-cols-2 lg:grid-cols-3">
48
+ <div class="field-info-container">
49
+ <div class="field-name">Event Type</div>
50
+ <div class="field-value">{{n-a (smart-humanize this.eventType)}}</div>
51
+ </div>
52
+ <div class="field-info-container">
53
+ <div class="field-name">Severity</div>
54
+ <div class="field-value"><Badge @status={{this.severity}}>{{smart-humanize this.severity}}</Badge></div>
55
+ </div>
56
+ <div class="field-info-container">
57
+ <div class="field-name">State</div>
58
+ <div class="field-value">{{n-a (smart-humanize @resource.state)}}</div>
59
+ </div>
60
+ <div class="field-info-container">
61
+ <div class="field-name">Code</div>
62
+ <div class="field-value">{{n-a @resource.code}}</div>
63
+ </div>
64
+ <div class="field-info-container">
65
+ <div class="field-name">Reason</div>
66
+ <div class="field-value">{{n-a @resource.reason}}</div>
67
+ </div>
68
+ <div class="field-info-container">
69
+ <div class="field-name">Mileage</div>
70
+ <div class="field-value">{{n-a @resource.mileage}}</div>
71
+ </div>
72
+ <div class="field-info-container">
73
+ <div class="field-name">Age</div>
74
+ <div class="field-value">{{#if this.ageMinutes}}{{this.ageMinutes}} minutes{{else}}{{n-a null}}{{/if}}</div>
75
+ </div>
76
+ <div class="field-info-container">
77
+ <div class="field-name">Processing Delay</div>
78
+ <div class="field-value">{{#if this.processingDelay}}{{this.processingDelay}} minutes{{else}}{{n-a null}}{{/if}}</div>
79
+ </div>
80
+ <div class="field-info-container sm:col-span-2 lg:col-span-3">
81
+ <div class="field-name">Comment</div>
82
+ <div class="field-value whitespace-pre-line">{{n-a @resource.comment}}</div>
83
+ </div>
84
+ </div>
85
+ </div>
86
+
87
+ <div class="overflow-hidden rounded-md border border-gray-200 bg-white shadow-sm dark:border-gray-700 dark:bg-gray-800">
88
+ <div class="border-b border-gray-200 px-3 py-2 dark:border-gray-700">
89
+ <h3 class="text-sm font-bold text-gray-900 dark:text-gray-100">Device & Provider</h3>
90
+ </div>
91
+ <div class="grid grid-cols-1 gap-2 p-3 sm:grid-cols-2 lg:grid-cols-3">
92
+ <div class="field-info-container">
93
+ <div class="field-name">Device</div>
94
+ <div class="field-value">
95
+ {{#if this.deviceRouteModel}}
96
+ <button
97
+ type="button"
98
+ class="text-blue-600 hover:text-blue-700 hover:underline dark:text-blue-300 dark:hover:text-blue-200"
99
+ data-test-device-event-device-link
100
+ {{on "click" this.openDevice}}
101
+ >
102
+ {{n-a this.deviceName}}
103
+ </button>
104
+ {{else}}
105
+ {{n-a this.deviceName}}
106
+ {{/if}}
107
+ </div>
108
+ </div>
109
+ <div class="field-info-container">
110
+ <div class="field-name">Device UUID</div>
111
+ <div class="field-value">{{n-a @resource.device_uuid}}</div>
112
+ </div>
113
+ <div class="field-info-container">
114
+ <div class="field-name">Device ID</div>
115
+ <div class="field-value">{{n-a @resource.device_id}}</div>
116
+ </div>
117
+ <div class="field-info-container">
118
+ <div class="field-name">IMEI</div>
119
+ <div class="field-value">{{n-a @resource.device_imei}}</div>
120
+ </div>
121
+ <div class="field-info-container">
122
+ <div class="field-name">Serial Number</div>
123
+ <div class="field-value">{{n-a @resource.device_serial_number}}</div>
124
+ </div>
125
+ <div class="field-info-container">
126
+ <div class="field-name">IDENT</div>
127
+ <div class="field-value">{{n-a @resource.ident}}</div>
128
+ </div>
129
+ <div class="field-info-container">
130
+ <div class="field-name">Provider</div>
131
+ <div class="field-value">
132
+ {{#if this.telematicRouteModel}}
133
+ <button
134
+ type="button"
135
+ class="text-blue-600 hover:text-blue-700 hover:underline dark:text-blue-300 dark:hover:text-blue-200"
136
+ data-test-device-event-telematic-link
137
+ {{on "click" this.openTelematic}}
138
+ >
139
+ {{n-a this.providerLabel}}
140
+ </button>
141
+ {{else}}
142
+ {{n-a this.providerLabel}}
143
+ {{/if}}
144
+ </div>
145
+ </div>
146
+ <div class="field-info-container">
147
+ <div class="field-name">Protocol</div>
148
+ <div class="field-value">{{n-a @resource.protocol}}</div>
149
+ </div>
150
+ <div class="field-info-container">
151
+ <div class="field-name">Telematic UUID</div>
152
+ <div class="field-value">{{n-a @resource.telematic_uuid}}</div>
153
+ </div>
154
+ <div class="field-info-container">
155
+ <div class="field-name">Device Status</div>
156
+ <div class="field-value" data-test-device-event-device-status>
157
+ {{#if this.deviceStatus}}
158
+ <Badge @status={{this.deviceStatus}}>{{smart-humanize this.deviceStatus}}</Badge>
159
+ {{else}}
160
+ {{n-a null}}
161
+ {{/if}}
162
+ </div>
163
+ </div>
164
+ </div>
165
+ </div>
166
+ </section>
167
+
168
+ <section class="grid grid-cols-1 gap-3 xl:grid-cols-3">
169
+ <div class="overflow-hidden rounded-md border border-gray-200 bg-white shadow-sm dark:border-gray-700 dark:bg-gray-800">
170
+ <div class="border-b border-gray-200 px-3 py-2 dark:border-gray-700">
171
+ <h3 class="text-sm font-bold text-gray-900 dark:text-gray-100">Payload</h3>
172
+ </div>
173
+ <pre class="max-h-80 overflow-auto bg-gray-900 p-3 text-[11px] leading-5 text-gray-100">{{n-a this.payloadJson}}</pre>
174
+ </div>
175
+
176
+ <div class="overflow-hidden rounded-md border border-gray-200 bg-white shadow-sm dark:border-gray-700 dark:bg-gray-800">
177
+ <div class="border-b border-gray-200 px-3 py-2 dark:border-gray-700">
178
+ <h3 class="text-sm font-bold text-gray-900 dark:text-gray-100">Data</h3>
179
+ </div>
180
+ <pre class="max-h-80 overflow-auto bg-gray-900 p-3 text-[11px] leading-5 text-gray-100">{{n-a this.dataJson}}</pre>
181
+ </div>
182
+
183
+ <div class="overflow-hidden rounded-md border border-gray-200 bg-white shadow-sm dark:border-gray-700 dark:bg-gray-800">
184
+ <div class="border-b border-gray-200 px-3 py-2 dark:border-gray-700">
185
+ <h3 class="text-sm font-bold text-gray-900 dark:text-gray-100">Meta</h3>
186
+ </div>
187
+ <pre class="max-h-80 overflow-auto bg-gray-900 p-3 text-[11px] leading-5 text-gray-100">{{n-a this.metaJson}}</pre>
188
+ </div>
189
+ </section>
190
+
191
+ <CustomField::Yield @subject={{@resource}} @viewMode={{true}} @wrapperClass="bordered-top" />
192
+ </div>
@@ -0,0 +1,151 @@
1
+ import Component from '@glimmer/component';
2
+ import { action, get } from '@ember/object';
3
+ import { inject as service } from '@ember/service';
4
+
5
+ const warningSeverities = ['warning', 'error', 'critical', 'high'];
6
+
7
+ function firstPresent(...values) {
8
+ return values.find((value) => value !== undefined && value !== null && value !== '');
9
+ }
10
+
11
+ function prettyJson(value) {
12
+ if (!value) {
13
+ return null;
14
+ }
15
+
16
+ if (typeof value === 'string') {
17
+ try {
18
+ return JSON.stringify(JSON.parse(value), null, 2);
19
+ } catch {
20
+ return value;
21
+ }
22
+ }
23
+
24
+ return JSON.stringify(value, null, 2);
25
+ }
26
+
27
+ export default class DeviceEventDetailsComponent extends Component {
28
+ @service hostRouter;
29
+
30
+ get resource() {
31
+ return this.args.resource ?? {};
32
+ }
33
+
34
+ get eventType() {
35
+ return firstPresent(get(this.resource, 'event_type'), get(this.resource, 'eventType'), get(this.resource, 'type'));
36
+ }
37
+
38
+ get severity() {
39
+ return firstPresent(get(this.resource, 'severity'), 'info');
40
+ }
41
+
42
+ get isProcessed() {
43
+ return Boolean(firstPresent(get(this.resource, 'processed_at'), get(this.resource, 'processedAt')));
44
+ }
45
+
46
+ get processedStatus() {
47
+ return this.isProcessed ? 'success' : 'warning';
48
+ }
49
+
50
+ get processedLabel() {
51
+ return this.isProcessed ? 'Processed' : 'Unprocessed';
52
+ }
53
+
54
+ get severityAccentClass() {
55
+ return warningSeverities.includes(String(this.severity ?? '').toLowerCase()) ? 'fleetops-connectivity-kpi-accent-amber' : 'fleetops-connectivity-kpi-accent-green';
56
+ }
57
+
58
+ get message() {
59
+ return get(this.resource, 'message');
60
+ }
61
+
62
+ get occurredAt() {
63
+ return firstPresent(get(this.resource, 'occurred_at'), get(this.resource, 'occurredAt'), get(this.resource, 'created_at'), get(this.resource, 'createdAt'));
64
+ }
65
+
66
+ get processedAt() {
67
+ return firstPresent(get(this.resource, 'processed_at'), get(this.resource, 'processedAt'));
68
+ }
69
+
70
+ get processingDelay() {
71
+ return firstPresent(get(this.resource, 'processing_delay_minutes'), get(this.resource, 'processingDelayMinutes'));
72
+ }
73
+
74
+ get ageMinutes() {
75
+ return firstPresent(get(this.resource, 'age_minutes'), get(this.resource, 'ageMinutes'));
76
+ }
77
+
78
+ get deviceName() {
79
+ return firstPresent(
80
+ get(this.resource, 'device.displayName'),
81
+ get(this.resource, 'device.display_name'),
82
+ get(this.resource, 'device.name'),
83
+ get(this.resource, 'device_name'),
84
+ get(this.resource, 'device_id')
85
+ );
86
+ }
87
+
88
+ get deviceIdentifier() {
89
+ return firstPresent(
90
+ get(this.resource, 'device_id'),
91
+ get(this.resource, 'device.device_id'),
92
+ get(this.resource, 'device_imei'),
93
+ get(this.resource, 'device_serial_number'),
94
+ get(this.resource, 'ident')
95
+ );
96
+ }
97
+
98
+ get deviceRouteModel() {
99
+ return firstPresent(get(this.resource, 'device.id'), get(this.resource, 'device.public_id'), get(this.resource, 'device_uuid'));
100
+ }
101
+
102
+ get providerLabel() {
103
+ return firstPresent(get(this.resource, 'provider_descriptor.label'), get(this.resource, 'telematic.name'), get(this.resource, 'telematic_name'), get(this.resource, 'provider'));
104
+ }
105
+
106
+ get telematicRouteModel() {
107
+ return firstPresent(get(this.resource, 'telematic.id'), get(this.resource, 'telematic.public_id'), get(this.resource, 'telematic_uuid'));
108
+ }
109
+
110
+ get deviceStatus() {
111
+ return firstPresent(get(this.resource, 'device.status'), get(this.resource, 'device_status'), get(this.resource, 'device_connection_status'));
112
+ }
113
+
114
+ get metrics() {
115
+ return [
116
+ { label: 'Severity', value: this.severity, icon: 'triangle-exclamation', accentClass: this.severityAccentClass },
117
+ {
118
+ label: 'Processing',
119
+ value: this.processedLabel,
120
+ icon: this.isProcessed ? 'check' : 'clock',
121
+ accentClass: this.isProcessed ? 'fleetops-connectivity-kpi-accent-green' : 'fleetops-connectivity-kpi-accent-rose',
122
+ },
123
+ { label: 'Device', value: this.deviceName, meta: this.deviceIdentifier, icon: 'microchip', accentClass: 'fleetops-connectivity-kpi-accent-blue' },
124
+ { label: 'Provider', value: this.providerLabel, icon: 'satellite-dish', accentClass: 'fleetops-connectivity-kpi-accent-green' },
125
+ ];
126
+ }
127
+
128
+ get payloadJson() {
129
+ return prettyJson(get(this.resource, 'payload'));
130
+ }
131
+
132
+ get dataJson() {
133
+ return prettyJson(get(this.resource, 'data'));
134
+ }
135
+
136
+ get metaJson() {
137
+ return prettyJson(get(this.resource, 'meta'));
138
+ }
139
+
140
+ @action openDevice() {
141
+ if (this.deviceRouteModel) {
142
+ return this.hostRouter.transitionTo('console.fleet-ops.connectivity.devices.index.details', this.deviceRouteModel);
143
+ }
144
+ }
145
+
146
+ @action openTelematic() {
147
+ if (this.telematicRouteModel) {
148
+ return this.hostRouter.transitionTo('console.fleet-ops.connectivity.telematics.details', this.telematicRouteModel);
149
+ }
150
+ }
151
+ }
@@ -219,7 +219,7 @@ export default class DriverScheduleComponent extends Component {
219
219
  }
220
220
 
221
221
  /**
222
- * Load HOS (Hours of Service) status from the FleetOps driver endpoint.
222
+ * Load HOS (Hours of Service) status from the Fleet-Ops driver endpoint.
223
223
  * Best-effort — if the endpoint is not yet implemented hosStatus stays null.
224
224
  */
225
225
  @task *loadHOSStatus() {
@@ -63,6 +63,7 @@ export default class LayoutFleetOpsSidebarComponent extends Component {
63
63
  icon: 'truck',
64
64
  route: 'management',
65
65
  defaultRoute: 'management.index',
66
+ requiresVisibleChildren: true,
66
67
  keywords: ['drivers', 'vehicles', 'fleets', 'contacts', 'places'],
67
68
  children: this.resourcesItems,
68
69
  }),
@@ -72,6 +73,7 @@ export default class LayoutFleetOpsSidebarComponent extends Component {
72
73
  icon: 'cog',
73
74
  route: 'maintenance',
74
75
  defaultRoute: 'maintenance.index',
76
+ requiresVisibleChildren: true,
75
77
  keywords: ['work orders', 'equipment', 'parts'],
76
78
  children: this.maintenanceItems,
77
79
  }),
@@ -81,6 +83,7 @@ export default class LayoutFleetOpsSidebarComponent extends Component {
81
83
  icon: 'satellite-dish',
82
84
  route: 'connectivity',
83
85
  defaultRoute: 'connectivity.telematics',
86
+ requiresVisibleChildren: true,
84
87
  keywords: ['telematics', 'fuel integrations', 'devices', 'sensors'],
85
88
  children: this.connectivityItems,
86
89
  }),
@@ -90,6 +93,7 @@ export default class LayoutFleetOpsSidebarComponent extends Component {
90
93
  icon: 'chart-line',
91
94
  route: 'analytics',
92
95
  defaultRoute: 'analytics.index',
96
+ requiresVisibleChildren: true,
93
97
  keywords: ['reports', 'metrics'],
94
98
  children: this.analyticsItems,
95
99
  }),
@@ -99,6 +103,7 @@ export default class LayoutFleetOpsSidebarComponent extends Component {
99
103
  icon: 'gear',
100
104
  route: 'settings',
101
105
  defaultRoute: 'settings.index',
106
+ requiresVisibleChildren: true,
102
107
  keywords: ['configuration', 'navigator', 'map', 'routing', 'notifications'],
103
108
  children: this.settingsItems,
104
109
  }),
@@ -164,7 +169,7 @@ export default class LayoutFleetOpsSidebarComponent extends Component {
164
169
 
165
170
  get connectivityItems() {
166
171
  return this.withRegistryItems('connectivity', [
167
- this.createItem('menu.telematics', 'satellite-dish', 'connectivity.telematics', 'fleet-ops list telematic', 'fleet-ops see telematic', ['connectivity hub']),
172
+ this.createHubItem(this.intl.t('menu.telematics'), 'satellite-dish', 'connectivity.telematics', 'fleet-ops list telematic', 'fleet-ops see telematic', ['connectivity hub']),
168
173
  this.createItem('menu.fuel-providers', 'gas-pump', 'connectivity.fuel-providers', 'fleet-ops list fuel-report', 'fleet-ops see fuel-report', ['fuel integrations']),
169
174
  this.createItem('menu.devices', 'hard-drive', 'connectivity.devices', 'fleet-ops list device', 'fleet-ops see device'),
170
175
  this.createItem('menu.sensors', 'temperature-full', 'connectivity.sensors', 'fleet-ops list sensor', 'fleet-ops see sensor'),
@@ -247,13 +252,14 @@ export default class LayoutFleetOpsSidebarComponent extends Component {
247
252
  this.universeSettingsMenuItems = registeredMenuItems.filter((menuItem) => menuItem.section === 'settings');
248
253
  }
249
254
 
250
- createBranch({ id, label, icon, route, defaultRoute, children, keywords = [] }) {
255
+ createBranch({ id, label, icon, route, defaultRoute, requiresVisibleChildren = false, children, keywords = [] }) {
251
256
  return {
252
257
  id,
253
258
  label,
254
259
  icon,
255
260
  route: this.fullRoute(route),
256
261
  defaultRoute: this.fullRoute(defaultRoute),
262
+ requiresVisibleChildren,
257
263
  children: children.filter((item) => item.visible !== false),
258
264
  keywords,
259
265
  };
@@ -267,12 +273,12 @@ export default class LayoutFleetOpsSidebarComponent extends Component {
267
273
  icon,
268
274
  route: this.fullRoute(route),
269
275
  permission,
270
- visible: this.abilities.can(ability),
276
+ visiblePermission: ability,
271
277
  keywords: [intl, route, ...keywords].filter(Boolean),
272
278
  };
273
279
  }
274
280
 
275
- createHubItem(label, icon, route, permission, ability, keywords = []) {
281
+ createHubItem(label, icon, route, _permission, _ability, keywords = []) {
276
282
  return {
277
283
  pinnedFirst: true,
278
284
  priority: this.defaultPriorityForRoute(route),
@@ -280,8 +286,7 @@ export default class LayoutFleetOpsSidebarComponent extends Component {
280
286
  description: label,
281
287
  icon,
282
288
  route: this.fullRoute(route),
283
- permission,
284
- visible: this.abilities.can(ability),
289
+ isNavigationHub: true,
285
290
  keywords: [label, route, ...keywords].filter(Boolean),
286
291
  };
287
292
  }
@@ -390,10 +395,11 @@ export default class LayoutFleetOpsSidebarComponent extends Component {
390
395
  }
391
396
  }
392
397
 
393
- @action shouldSyncInitialActiveParent({ activePath = [], routeName }) {
398
+ @action shouldSyncInitialActiveParent({ activePath = [], currentURL }) {
394
399
  const [parent, child] = activePath;
395
- const defaultOrdersRoutes = [this.fullRoute('operations.orders'), this.fullRoute('operations.orders.index')];
396
- const isDefaultOrdersLanding = parent?.id === 'operations' && child?.route === this.fullRoute('operations.orders') && defaultOrdersRoutes.includes(routeName);
400
+ const normalizedURL = (currentURL ?? '').split('?')[0].replace(/\/+$/, '') || '/';
401
+ const isFleetOpsRootURL = normalizedURL === '/fleet-ops';
402
+ const isDefaultOrdersLanding = parent?.id === 'operations' && child?.route === this.fullRoute('operations.orders') && isFleetOpsRootURL;
397
403
 
398
404
  return !isDefaultOrdersLanding;
399
405
  }
@@ -92,7 +92,7 @@
92
92
  <div class="rounded-md border border-gray-200 p-3 dark:border-gray-700 md:col-span-2">
93
93
  <div class="text-gray-400 dark:text-gray-500">Update Mode</div>
94
94
  <div class="mt-1 font-semibold text-gray-900 dark:text-gray-100">Provider polling</div>
95
- <div class="mt-1 text-gray-500 dark:text-gray-400">FleetOps polls this provider for device snapshots and telemetry updates.</div>
95
+ <div class="mt-1 text-gray-500 dark:text-gray-400">Fleet-Ops polls this provider for device snapshots and telemetry updates.</div>
96
96
  </div>
97
97
  {{/if}}
98
98
  </div>
@@ -70,7 +70,7 @@
70
70
  <div class="mb-3">
71
71
  <div>
72
72
  <h2 class="text-sm font-bold text-gray-900 dark:text-gray-100">Credentials</h2>
73
- <p class="text-xs text-gray-500 dark:text-gray-300">FleetOps uses these credentials to test the connection and sync devices.</p>
73
+ <p class="text-xs text-gray-500 dark:text-gray-300">Fleet-Ops uses these credentials to test the connection and sync devices.</p>
74
74
  </div>
75
75
  </div>
76
76
 
@@ -266,7 +266,7 @@
266
266
  </InputGroup>
267
267
  {{else}}
268
268
  <div class="rounded-md border border-dashed border-gray-300 p-3 text-xs text-gray-500 dark:border-gray-700 dark:text-gray-300 lg:col-span-2">
269
- This provider does not require FleetOps webhook configuration.
269
+ This provider does not require Fleet-Ops webhook configuration.
270
270
  </div>
271
271
  {{/if}}
272
272
  </div>
@@ -112,7 +112,7 @@
112
112
  {{/if}}
113
113
  {{else}}
114
114
  <div class="rounded-md border border-dashed border-gray-300 p-3 text-xs text-gray-500 dark:border-gray-700 dark:text-gray-300">
115
- This provider does not require FleetOps webhook configuration.
115
+ This provider does not require Fleet-Ops webhook configuration.
116
116
  </div>
117
117
  {{/if}}
118
118
  </section>
@@ -56,7 +56,7 @@ export default class ConnectivityDevicesIndexDetailsEventsController extends Con
56
56
  }
57
57
 
58
58
  get unprocessedEventsCount() {
59
- return this.events.filter((event) => !event.processed_at && !event.is_processed).length;
59
+ return this.events.filter((event) => !event.processed_at).length;
60
60
  }
61
61
 
62
62
  get processedEventsCount() {
@@ -86,13 +86,13 @@ export default class ConnectivityDevicesIndexDetailsVehicleController extends Co
86
86
 
87
87
  @action openVehicle() {
88
88
  if (this.vehicle?.id) {
89
- return this.hostRouter.transitionTo('console.fleet-ops.management.vehicles.index.details', this.vehicle);
89
+ return this.hostRouter.transitionTo('console.fleet-ops.management.vehicles.index.details', this.vehicle.public_id);
90
90
  }
91
91
  }
92
92
 
93
93
  @action openVehiclePositions() {
94
94
  if (this.vehicle?.id) {
95
- return this.hostRouter.transitionTo('console.fleet-ops.management.vehicles.index.details.positions', this.vehicle);
95
+ return this.hostRouter.transitionTo('console.fleet-ops.management.vehicles.index.details.positions', this.vehicle.public_id);
96
96
  }
97
97
  }
98
98
 
@@ -1,3 +1,70 @@
1
1
  import Controller from '@ember/controller';
2
+ import { action, set } from '@ember/object';
3
+ import { inject as service } from '@ember/service';
4
+ import { tracked } from '@glimmer/tracking';
2
5
 
3
- export default class ConnectivityEventsDetailsController extends Controller {}
6
+ function firstPresent(...values) {
7
+ return values.find((value) => value !== undefined && value !== null && value !== '');
8
+ }
9
+
10
+ function toDate(value) {
11
+ if (value instanceof Date) {
12
+ return value;
13
+ }
14
+
15
+ const date = new Date(value);
16
+
17
+ return Number.isNaN(date.getTime()) ? value : date;
18
+ }
19
+
20
+ export default class ConnectivityEventsDetailsController extends Controller {
21
+ @service deviceEventActions;
22
+ @service hostRouter;
23
+
24
+ @tracked isMarkingProcessed = false;
25
+
26
+ get event() {
27
+ return this.model;
28
+ }
29
+
30
+ get isProcessed() {
31
+ return Boolean(firstPresent(this.event?.processed_at, this.event?.processedAt));
32
+ }
33
+
34
+ get processedStatus() {
35
+ return this.isProcessed ? 'success' : 'warning';
36
+ }
37
+
38
+ get processedLabel() {
39
+ return this.isProcessed ? 'Processed' : 'Unprocessed';
40
+ }
41
+
42
+ @action goBack() {
43
+ return this.hostRouter.transitionTo('console.fleet-ops.connectivity.events.index');
44
+ }
45
+
46
+ @action refresh() {
47
+ return this.hostRouter.refresh();
48
+ }
49
+
50
+ @action async markProcessed() {
51
+ if (this.isMarkingProcessed) {
52
+ return;
53
+ }
54
+
55
+ this.isMarkingProcessed = true;
56
+
57
+ try {
58
+ const response = await this.deviceEventActions.markProcessed(this.event);
59
+ const processedAt = firstPresent(response?.device_event?.processed_at, response?.device_event?.processedAt, new Date());
60
+
61
+ if (this.event) {
62
+ set(this.event, 'processed_at', toDate(processedAt));
63
+ }
64
+
65
+ return response;
66
+ } finally {
67
+ this.isMarkingProcessed = false;
68
+ }
69
+ }
70
+ }
@@ -178,6 +178,8 @@ export default class ConnectivitySensorsIndexController extends Controller {
178
178
  filterParam: 'type',
179
179
  filterComponent: 'filter/multi-option',
180
180
  filterOptions: fleetOpsOptions('sensorTypes'),
181
+ filterOptionLabel: 'label',
182
+ filterOptionValue: 'value',
181
183
  },
182
184
  {
183
185
  label: 'Last Value',
@@ -200,6 +202,8 @@ export default class ConnectivitySensorsIndexController extends Controller {
200
202
  filterable: true,
201
203
  filterComponent: 'filter/multi-option',
202
204
  filterOptions: fleetOpsOptions('sensorStatuses'),
205
+ filterOptionLabel: 'label',
206
+ filterOptionValue: 'value',
203
207
  },
204
208
  {
205
209
  label: 'Threshold',
@@ -40,7 +40,7 @@ export default class ConnectivityTelematicsDetailsEventsController extends Contr
40
40
  }
41
41
 
42
42
  get unprocessedEventsCount() {
43
- return this.events.filter((event) => !event.processed_at && !event.is_processed).length;
43
+ return this.events.filter((event) => !event.processed_at).length;
44
44
  }
45
45
 
46
46
  get deviceCount() {