@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.
- package/addon/components/admin/map-settings.hbs +1 -1
- package/addon/components/device/details.js +1 -1
- package/addon/components/device-event/details.hbs +192 -0
- package/addon/components/device-event/details.js +151 -0
- package/addon/components/driver/schedule.js +1 -1
- package/addon/components/layout/fleet-ops-sidebar.js +15 -9
- package/addon/components/telematic/details.hbs +1 -1
- package/addon/components/telematic/form.hbs +2 -2
- package/addon/components/telematic/settings.hbs +1 -1
- package/addon/controllers/connectivity/devices/index/details/events.js +1 -1
- package/addon/controllers/connectivity/devices/index/details/vehicle.js +2 -2
- package/addon/controllers/connectivity/events/details.js +68 -1
- package/addon/controllers/connectivity/sensors/index.js +4 -0
- package/addon/controllers/connectivity/telematics/details/events.js +1 -1
- package/addon/controllers/connectivity/telematics/details/sensors.js +4 -0
- package/addon/controllers/settings/map.js +15 -0
- package/addon/instance-initializers/register-vroom-allocation.js +1 -1
- package/addon/routes/connectivity/events/details.js +26 -1
- package/addon/routes/management/vehicles/index/details.js +3 -1
- package/addon/services/map-adapter/google.js +25 -0
- package/addon/services/map-adapter/leaflet.js +1 -1
- package/addon/services/map-adapter-interface.js +1 -1
- package/addon/services/map-manager.js +4 -1
- package/addon/services/map-settings.js +15 -0
- package/addon/styles/fleetops-engine.css +1 -1
- package/addon/templates/connectivity/devices/index/details/vehicle.hbs +2 -1
- package/addon/templates/connectivity/events/details.hbs +40 -1
- package/addon/templates/settings/map.hbs +38 -9
- package/addon/utils/leaflet-plugin-loader.js +5 -5
- package/addon/utils/route-colors.js +1 -1
- package/app/components/device-event/details.hbs +192 -0
- package/app/components/device-event/details.js +1 -0
- package/composer.json +1 -1
- package/extension.json +1 -1
- package/package.json +2 -2
- package/server/migrations/2026_06_23_000001_delete_bad_afaqy_telematics_sensors.php +34 -0
- package/server/src/Exports/ContactExport.php +12 -5
- package/server/src/Exports/DeviceExport.php +92 -0
- package/server/src/Exports/DriverExport.php +28 -3
- package/server/src/Exports/EquipmentExport.php +94 -0
- package/server/src/Exports/FleetExport.php +22 -7
- package/server/src/Exports/FuelReportExport.php +16 -1
- package/server/src/Exports/IssueExport.php +25 -3
- package/server/src/Exports/MaintenanceExport.php +106 -0
- package/server/src/Exports/MaintenanceScheduleExport.php +99 -0
- package/server/src/Exports/OrderExport.php +24 -5
- package/server/src/Exports/PartExport.php +99 -0
- package/server/src/Exports/PlaceExport.php +33 -7
- package/server/src/Exports/SensorExport.php +94 -0
- package/server/src/Exports/ServiceAreaExport.php +31 -7
- package/server/src/Exports/ServiceRateExport.php +41 -1
- package/server/src/Exports/TelematicExport.php +90 -0
- package/server/src/Exports/VehicleExport.php +274 -8
- package/server/src/Exports/VendorExport.php +15 -2
- package/server/src/Exports/WorkOrderExport.php +92 -0
- package/server/src/Http/Controllers/Api/v1/Concerns/ResolvesFleetOpsApiResources.php +176 -0
- package/server/src/Http/Controllers/Api/v1/DeviceController.php +225 -0
- package/server/src/Http/Controllers/Api/v1/EquipmentController.php +114 -0
- package/server/src/Http/Controllers/Api/v1/FuelTransactionController.php +185 -0
- package/server/src/Http/Controllers/Api/v1/PartController.php +120 -0
- package/server/src/Http/Controllers/Api/v1/SensorController.php +132 -0
- package/server/src/Http/Controllers/Api/v1/WorkOrderController.php +161 -0
- package/server/src/Http/Controllers/Internal/v1/DeviceController.php +18 -0
- package/server/src/Http/Controllers/Internal/v1/EquipmentController.php +17 -0
- package/server/src/Http/Controllers/Internal/v1/MaintenanceController.php +17 -0
- package/server/src/Http/Controllers/Internal/v1/MaintenanceScheduleController.php +17 -0
- package/server/src/Http/Controllers/Internal/v1/PartController.php +17 -0
- package/server/src/Http/Controllers/Internal/v1/SensorController.php +18 -0
- package/server/src/Http/Controllers/Internal/v1/SettingController.php +29 -5
- package/server/src/Http/Controllers/Internal/v1/TelematicController.php +18 -0
- package/server/src/Http/Controllers/Internal/v1/WorkOrderController.php +17 -0
- package/server/src/Http/Filter/DeviceFilter.php +33 -10
- package/server/src/Http/Filter/EquipmentFilter.php +23 -0
- package/server/src/Http/Filter/FuelProviderTransactionFilter.php +57 -2
- package/server/src/Http/Filter/PartFilter.php +47 -0
- package/server/src/Http/Filter/SensorFilter.php +34 -6
- package/server/src/Http/Filter/WorkOrderFilter.php +23 -0
- package/server/src/Http/Requests/CreateDeviceRequest.php +51 -0
- package/server/src/Http/Requests/CreateEquipmentRequest.php +35 -0
- package/server/src/Http/Requests/CreateFuelTransactionRequest.php +50 -0
- package/server/src/Http/Requests/CreatePartRequest.php +40 -0
- package/server/src/Http/Requests/CreateSensorRequest.php +47 -0
- package/server/src/Http/Requests/CreateWorkOrderRequest.php +42 -0
- package/server/src/Http/Requests/UpdateDeviceRequest.php +7 -0
- package/server/src/Http/Requests/UpdateEquipmentRequest.php +7 -0
- package/server/src/Http/Requests/UpdateFuelTransactionRequest.php +7 -0
- package/server/src/Http/Requests/UpdatePartRequest.php +7 -0
- package/server/src/Http/Requests/UpdateSensorRequest.php +7 -0
- package/server/src/Http/Requests/UpdateWorkOrderRequest.php +7 -0
- package/server/src/Http/Resources/v1/Device.php +72 -0
- package/server/src/Http/Resources/v1/Equipment.php +57 -0
- package/server/src/Http/Resources/v1/FuelTransaction.php +69 -0
- package/server/src/Http/Resources/v1/Part.php +64 -0
- package/server/src/Http/Resources/v1/Sensor.php +68 -0
- package/server/src/Jobs/SyncTelematicDevicesJob.php +194 -38
- package/server/src/Models/Order.php +2 -1
- package/server/src/Observers/PurchaseRateObserver.php +2 -1
- package/server/src/Support/Analytics/RevenueTrend.php +3 -8
- package/server/src/Support/Metrics/ActiveRevenueQuery.php +121 -0
- package/server/src/Support/Metrics/AvgOrderValueMetric.php +1 -7
- package/server/src/Support/Metrics/EarningsMetric.php +1 -10
- package/server/src/Support/Telematics/Providers/AfaqyProvider.php +104 -5
- package/server/src/Support/Telematics/Providers/SafeeProvider.php +550 -54
- package/server/src/Support/Telematics/TelematicService.php +28 -9
- package/server/src/routes.php +67 -14
- package/server/tests/ExportCoverageTest.php +79 -0
- package/server/tests/MetricsRegistryTest.php +45 -5
- package/server/tests/PublicFleetOpsRestEndpointsTest.php +191 -0
- 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
|
|
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
|
|
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
|
|
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.
|
|
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
|
-
|
|
276
|
+
visiblePermission: ability,
|
|
271
277
|
keywords: [intl, route, ...keywords].filter(Boolean),
|
|
272
278
|
};
|
|
273
279
|
}
|
|
274
280
|
|
|
275
|
-
createHubItem(label, icon, route,
|
|
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
|
-
|
|
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 = [],
|
|
398
|
+
@action shouldSyncInitialActiveParent({ activePath = [], currentURL }) {
|
|
394
399
|
const [parent, child] = activePath;
|
|
395
|
-
const
|
|
396
|
-
const
|
|
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">
|
|
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">
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
43
|
+
return this.events.filter((event) => !event.processed_at).length;
|
|
44
44
|
}
|
|
45
45
|
|
|
46
46
|
get deviceCount() {
|