@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
|
@@ -171,6 +171,8 @@ export default class ConnectivityTelematicsDetailsSensorsController extends Cont
|
|
|
171
171
|
filterParam: 'type',
|
|
172
172
|
filterComponent: 'filter/multi-option',
|
|
173
173
|
filterOptions: fleetOpsOptions('sensorTypes'),
|
|
174
|
+
filterOptionLabel: 'label',
|
|
175
|
+
filterOptionValue: 'value',
|
|
174
176
|
},
|
|
175
177
|
{
|
|
176
178
|
label: 'Device',
|
|
@@ -209,6 +211,8 @@ export default class ConnectivityTelematicsDetailsSensorsController extends Cont
|
|
|
209
211
|
filterable: true,
|
|
210
212
|
filterComponent: 'filter/multi-option',
|
|
211
213
|
filterOptions: fleetOpsOptions('sensorStatuses'),
|
|
214
|
+
filterOptionLabel: 'label',
|
|
215
|
+
filterOptionValue: 'value',
|
|
212
216
|
},
|
|
213
217
|
{
|
|
214
218
|
label: 'Last Reading',
|
|
@@ -19,6 +19,12 @@ export default class SettingsMapController extends Controller {
|
|
|
19
19
|
* @var {Array}
|
|
20
20
|
*/
|
|
21
21
|
@tracked mapProviderOptions = MAP_PROVIDER_OPTIONS;
|
|
22
|
+
@tracked googleMapsMapTypeOptions = [
|
|
23
|
+
{ label: 'Roadmap', value: 'roadmap' },
|
|
24
|
+
{ label: 'Satellite', value: 'satellite' },
|
|
25
|
+
{ label: 'Hybrid', value: 'hybrid' },
|
|
26
|
+
{ label: 'Terrain', value: 'terrain' },
|
|
27
|
+
];
|
|
22
28
|
|
|
23
29
|
/**
|
|
24
30
|
* The currently-selected map provider key (e.g. 'leaflet' or 'google').
|
|
@@ -27,6 +33,9 @@ export default class SettingsMapController extends Controller {
|
|
|
27
33
|
* @var {String}
|
|
28
34
|
*/
|
|
29
35
|
@tracked mapProvider = 'leaflet';
|
|
36
|
+
@tracked googleMapsMapType = 'roadmap';
|
|
37
|
+
@tracked showGoogleMapsTrafficLayer = false;
|
|
38
|
+
@tracked showGoogleMapsTransitLayer = false;
|
|
30
39
|
|
|
31
40
|
/**
|
|
32
41
|
* Whether settings have been loaded from the server.
|
|
@@ -68,6 +77,9 @@ export default class SettingsMapController extends Controller {
|
|
|
68
77
|
@task *saveSettings() {
|
|
69
78
|
const settings = {
|
|
70
79
|
mapProvider: this.mapProvider,
|
|
80
|
+
googleMapsMapType: this.googleMapsMapType,
|
|
81
|
+
showGoogleMapsTrafficLayer: this.showGoogleMapsTrafficLayer,
|
|
82
|
+
showGoogleMapsTransitLayer: this.showGoogleMapsTransitLayer,
|
|
71
83
|
};
|
|
72
84
|
|
|
73
85
|
try {
|
|
@@ -89,6 +101,9 @@ export default class SettingsMapController extends Controller {
|
|
|
89
101
|
const response = yield this.mapSettings.load({ force: true });
|
|
90
102
|
|
|
91
103
|
this.mapProvider = response?.mapProvider ?? 'leaflet';
|
|
104
|
+
this.googleMapsMapType = response?.googleMapsMapType ?? 'roadmap';
|
|
105
|
+
this.showGoogleMapsTrafficLayer = Boolean(response?.showGoogleMapsTrafficLayer);
|
|
106
|
+
this.showGoogleMapsTransitLayer = Boolean(response?.showGoogleMapsTransitLayer);
|
|
92
107
|
this.settingsLoaded = true;
|
|
93
108
|
} catch (error) {
|
|
94
109
|
this.notifications.serverError(error);
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* Third-party orchestration engines follow the same pattern:
|
|
11
11
|
* 1. Create a service extending OrchestrationEngineInterfaceService
|
|
12
12
|
* 2. Create an instance initializer that calls orchestrationEngine.register()
|
|
13
|
-
* 3. The engine will appear in the
|
|
13
|
+
* 3. The engine will appear in the Fleet-Ops orchestrator settings dropdown
|
|
14
14
|
*/
|
|
15
15
|
export function initialize(appInstance) {
|
|
16
16
|
const orchestrationEngine = appInstance.lookup('service:orchestration-engine');
|
|
@@ -1,3 +1,28 @@
|
|
|
1
1
|
import Route from '@ember/routing/route';
|
|
2
|
+
import { action } from '@ember/object';
|
|
3
|
+
import { inject as service } from '@ember/service';
|
|
2
4
|
|
|
3
|
-
export default class ConnectivityEventsDetailsRoute extends Route {
|
|
5
|
+
export default class ConnectivityEventsDetailsRoute extends Route {
|
|
6
|
+
@service abilities;
|
|
7
|
+
@service hostRouter;
|
|
8
|
+
@service intl;
|
|
9
|
+
@service notifications;
|
|
10
|
+
@service store;
|
|
11
|
+
|
|
12
|
+
@action error(error) {
|
|
13
|
+
this.notifications.serverError(error);
|
|
14
|
+
|
|
15
|
+
return this.hostRouter.transitionTo('console.fleet-ops.connectivity.events.index');
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
beforeModel() {
|
|
19
|
+
if (this.abilities.cannot('fleet-ops view device-event')) {
|
|
20
|
+
this.notifications.warning(this.intl.t('common.unauthorized-access'));
|
|
21
|
+
return this.hostRouter.transitionTo('console.fleet-ops.connectivity.events.index');
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
model({ public_id }) {
|
|
26
|
+
return this.store.findRecord('device-event', public_id);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -108,6 +108,12 @@ export default class GoogleMapsAdapter extends MapAdapterInterface {
|
|
|
108
108
|
/** @type {google.maps.ImageMapType|null} */
|
|
109
109
|
_customTileLayer = null;
|
|
110
110
|
|
|
111
|
+
/** @type {google.maps.TrafficLayer|null} */
|
|
112
|
+
_trafficLayer = null;
|
|
113
|
+
|
|
114
|
+
/** @type {google.maps.TransitLayer|null} */
|
|
115
|
+
_transitLayer = null;
|
|
116
|
+
|
|
111
117
|
/** @type {Map<string, Function[]>} Normalized event name → array of [handler, gmListener] pairs */
|
|
112
118
|
_eventListeners = new Map();
|
|
113
119
|
|
|
@@ -176,6 +182,7 @@ export default class GoogleMapsAdapter extends MapAdapterInterface {
|
|
|
176
182
|
styles: mergedMapStyles,
|
|
177
183
|
});
|
|
178
184
|
|
|
185
|
+
this.#syncGoogleLayers(options);
|
|
179
186
|
await this.#initDrawingManager();
|
|
180
187
|
|
|
181
188
|
debug('[GoogleMapsAdapter] Map initialized');
|
|
@@ -231,6 +238,10 @@ export default class GoogleMapsAdapter extends MapAdapterInterface {
|
|
|
231
238
|
this._tooltipOverlayView?.setMap?.(null);
|
|
232
239
|
this._tooltipOverlayView = null;
|
|
233
240
|
this._supportsAdvancedMarkers = false;
|
|
241
|
+
this._trafficLayer?.setMap(null);
|
|
242
|
+
this._trafficLayer = null;
|
|
243
|
+
this._transitLayer?.setMap(null);
|
|
244
|
+
this._transitLayer = null;
|
|
234
245
|
|
|
235
246
|
this._map = null;
|
|
236
247
|
debug('[GoogleMapsAdapter] Map destroyed');
|
|
@@ -1184,6 +1195,20 @@ export default class GoogleMapsAdapter extends MapAdapterInterface {
|
|
|
1184
1195
|
}
|
|
1185
1196
|
}
|
|
1186
1197
|
|
|
1198
|
+
#syncGoogleLayers(options = {}) {
|
|
1199
|
+
if (!this._map) return;
|
|
1200
|
+
|
|
1201
|
+
if (options.showTrafficLayer) {
|
|
1202
|
+
this._trafficLayer = new google.maps.TrafficLayer();
|
|
1203
|
+
this._trafficLayer.setMap(this._map);
|
|
1204
|
+
}
|
|
1205
|
+
|
|
1206
|
+
if (options.showTransitLayer) {
|
|
1207
|
+
this._transitLayer = new google.maps.TransitLayer();
|
|
1208
|
+
this._transitLayer.setMap(this._map);
|
|
1209
|
+
}
|
|
1210
|
+
}
|
|
1211
|
+
|
|
1187
1212
|
#ensureDrawToolbar() {
|
|
1188
1213
|
if (this._drawControlEl || !this._map) return;
|
|
1189
1214
|
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Implements the MapAdapterInterface using the Leaflet library (via ember-leaflet).
|
|
5
5
|
* This adapter wraps all existing Leaflet-specific logic, preserving 100% of
|
|
6
|
-
* the original
|
|
6
|
+
* the original Fleet-Ops map functionality while conforming to the provider-agnostic
|
|
7
7
|
* interface.
|
|
8
8
|
*
|
|
9
9
|
* All Leaflet-specific plugins are supported:
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* MapAdapterInterface
|
|
3
3
|
*
|
|
4
4
|
* Abstract base class that defines the standardized contract all map provider
|
|
5
|
-
* adapters must implement. This enables
|
|
5
|
+
* adapters must implement. This enables Fleet-Ops to support multiple map
|
|
6
6
|
* providers (Leaflet, Google Maps, Mapbox, etc.) without changing any
|
|
7
7
|
* application-level business logic.
|
|
8
8
|
*
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* MapManagerService
|
|
3
3
|
*
|
|
4
|
-
* Provider-agnostic central map service for
|
|
4
|
+
* Provider-agnostic central map service for Fleet-Ops. Replaces the
|
|
5
5
|
* Leaflet-specific `leaflet-map-manager` as the single point of contact
|
|
6
6
|
* for all map operations across the application.
|
|
7
7
|
*
|
|
@@ -139,6 +139,9 @@ export default class MapManagerService extends Service {
|
|
|
139
139
|
...options,
|
|
140
140
|
apiKey: options.apiKey ?? this.mapSettings.googleMapsApiKey,
|
|
141
141
|
mapId: options.mapId ?? this.mapSettings.googleMapsMapId,
|
|
142
|
+
mapTypeId: options.mapTypeId ?? this.mapSettings.googleMapsMapType,
|
|
143
|
+
showTrafficLayer: options.showTrafficLayer ?? this.mapSettings.showGoogleMapsTrafficLayer,
|
|
144
|
+
showTransitLayer: options.showTransitLayer ?? this.mapSettings.showGoogleMapsTransitLayer,
|
|
142
145
|
});
|
|
143
146
|
|
|
144
147
|
return Promise.resolve(map).then((nativeMap) => {
|
|
@@ -6,6 +6,9 @@ const DEFAULT_SETTINGS = {
|
|
|
6
6
|
mapProvider: 'leaflet',
|
|
7
7
|
googleMapsApiKey: '',
|
|
8
8
|
googleMapsMapId: '',
|
|
9
|
+
googleMapsMapType: 'roadmap',
|
|
10
|
+
showGoogleMapsTrafficLayer: false,
|
|
11
|
+
showGoogleMapsTransitLayer: false,
|
|
9
12
|
};
|
|
10
13
|
|
|
11
14
|
export default class MapSettingsService extends Service {
|
|
@@ -26,6 +29,18 @@ export default class MapSettingsService extends Service {
|
|
|
26
29
|
return this.settings.googleMapsMapId ?? '';
|
|
27
30
|
}
|
|
28
31
|
|
|
32
|
+
get googleMapsMapType() {
|
|
33
|
+
return this.settings.googleMapsMapType ?? 'roadmap';
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
get showGoogleMapsTrafficLayer() {
|
|
37
|
+
return Boolean(this.settings.showGoogleMapsTrafficLayer);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
get showGoogleMapsTransitLayer() {
|
|
41
|
+
return Boolean(this.settings.showGoogleMapsTransitLayer);
|
|
42
|
+
}
|
|
43
|
+
|
|
29
44
|
get isGoogleMaps() {
|
|
30
45
|
return this.mapProvider === 'google';
|
|
31
46
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/* stylelint-disable alpha-value-notation --
|
|
1
|
+
/* stylelint-disable alpha-value-notation -- Fleet-Ops lazy engine production CSS is minified by legacy clean-css, which corrupts comma-form rgba() percent alpha values. */
|
|
2
2
|
.scheduler-left-sidebar {
|
|
3
3
|
border-right: 1px rgb(229 231 235 / 100%) solid;
|
|
4
4
|
height: 100vh;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<div class="flex flex-col gap-3 p-3">
|
|
2
|
-
<div class="flex items-start justify-between gap-3">
|
|
2
|
+
<div class="flex flex-col items-start justify-between gap-3">
|
|
3
3
|
<div class="min-w-0">
|
|
4
4
|
<h2 class="text-base font-bold text-gray-900 dark:text-gray-100">Vehicle Attachment</h2>
|
|
5
5
|
<p class="mt-0.5 text-xs text-gray-500 dark:text-gray-300">Fleet context for telemetry from this device.</p>
|
|
@@ -119,6 +119,7 @@
|
|
|
119
119
|
<div class="border-t border-gray-200 dark:border-gray-700">
|
|
120
120
|
<PositionsReplay @resource={{this.vehicle}} @mapHeight={{180}} class="max-h-[360px] overflow-hidden" />
|
|
121
121
|
</div>
|
|
122
|
+
<Spacer @height="150px" />
|
|
122
123
|
{{else}}
|
|
123
124
|
<div class="p-3">
|
|
124
125
|
<Telematic::TabEmptyState
|
|
@@ -1 +1,40 @@
|
|
|
1
|
-
|
|
1
|
+
<div class="h-full overflow-y-auto bg-gray-50 dark:bg-gray-900">
|
|
2
|
+
<section class="sticky top-0 z-40 border-b border-gray-200 bg-white shadow-sm dark:border-gray-700 dark:bg-gray-900">
|
|
3
|
+
<div class="mx-auto flex w-full max-w-7xl flex-col gap-3 px-4 py-3 md:px-6 lg:flex-row lg:items-start lg:justify-between">
|
|
4
|
+
<div class="min-w-0 flex-1">
|
|
5
|
+
<div class="flex flex-wrap items-center gap-2">
|
|
6
|
+
<h1 class="truncate text-lg font-bold text-gray-900 dark:text-gray-100">{{smart-humanize this.event.event_type}}</h1>
|
|
7
|
+
<Badge @status={{this.event.severity}} @hideStatusDot={{true}}>{{smart-humanize this.event.severity}}</Badge>
|
|
8
|
+
<Badge @status={{this.processedStatus}} @hideStatusDot={{true}}>{{this.processedLabel}}</Badge>
|
|
9
|
+
</div>
|
|
10
|
+
<div class="mt-1 flex flex-wrap gap-2 text-xs text-gray-500 dark:text-gray-300">
|
|
11
|
+
<span>{{n-a this.event.public_id}}</span>
|
|
12
|
+
<span>Device: {{n-a (or this.event.device_name this.event.device_id this.event.ident)}}</span>
|
|
13
|
+
<span>Provider: {{n-a (or this.event.telematic_name this.event.provider)}}</span>
|
|
14
|
+
<span>Occurred: {{n-a (format-date-fns this.event.occurred_at "dd MMM yyyy, HH:mm")}}</span>
|
|
15
|
+
</div>
|
|
16
|
+
</div>
|
|
17
|
+
|
|
18
|
+
<div class="flex shrink-0 flex-row flex-nowrap items-center gap-2 whitespace-nowrap">
|
|
19
|
+
<Button @icon="arrow-left" @text="Back" @size="xs" @wrapperClass="shrink-0" @onClick={{this.goBack}} />
|
|
20
|
+
<Button @icon="refresh" @text="Refresh" @size="xs" @wrapperClass="shrink-0" @onClick={{this.refresh}} />
|
|
21
|
+
{{#unless this.isProcessed}}
|
|
22
|
+
<Button
|
|
23
|
+
@icon="check"
|
|
24
|
+
@text="Mark Processed"
|
|
25
|
+
@type="primary"
|
|
26
|
+
@size="xs"
|
|
27
|
+
@wrapperClass="shrink-0"
|
|
28
|
+
@isLoading={{this.isMarkingProcessed}}
|
|
29
|
+
@disabled={{this.isMarkingProcessed}}
|
|
30
|
+
@onClick={{this.markProcessed}}
|
|
31
|
+
/>
|
|
32
|
+
{{/unless}}
|
|
33
|
+
</div>
|
|
34
|
+
</div>
|
|
35
|
+
</section>
|
|
36
|
+
|
|
37
|
+
<section class="mx-auto w-full max-w-7xl">
|
|
38
|
+
<DeviceEvent::Details @resource={{this.event}} />
|
|
39
|
+
</section>
|
|
40
|
+
</div>
|
|
@@ -14,15 +14,12 @@
|
|
|
14
14
|
<div class="container mx-auto h-screen">
|
|
15
15
|
<div class="max-w-3xl my-10 mx-auto space-y-6">
|
|
16
16
|
|
|
17
|
-
{{
|
|
17
|
+
{{! ─── Map Provider Panel ──────────────────────────────────────────── }}
|
|
18
18
|
<ContentPanel @title={{t "settings.map.configure-map"}} @open={{true}} @wrapperClass="bordered-classic">
|
|
19
19
|
{{#if this.settingsLoaded}}
|
|
20
20
|
|
|
21
|
-
{{
|
|
22
|
-
<InputGroup
|
|
23
|
-
@name={{t "settings.map.map-provider"}}
|
|
24
|
-
@helpText={{t "settings.map.map-provider-help-text"}}
|
|
25
|
-
>
|
|
21
|
+
{{! Provider selector }}
|
|
22
|
+
<InputGroup @name={{t "settings.map.map-provider"}} @helpText={{t "settings.map.map-provider-help-text"}}>
|
|
26
23
|
<Select
|
|
27
24
|
class="w-full flex-1"
|
|
28
25
|
@value={{this.mapProvider}}
|
|
@@ -34,15 +31,47 @@
|
|
|
34
31
|
/>
|
|
35
32
|
</InputGroup>
|
|
36
33
|
|
|
34
|
+
{{#if this.isGoogleMapsSelected}}
|
|
35
|
+
<InputGroup @name={{t "settings.map.google-maps-map-type"}} @helpText={{t "settings.map.google-maps-map-type-help-text"}}>
|
|
36
|
+
<Select
|
|
37
|
+
class="w-full flex-1"
|
|
38
|
+
@value={{this.googleMapsMapType}}
|
|
39
|
+
@options={{this.googleMapsMapTypeOptions}}
|
|
40
|
+
@optionLabel="label"
|
|
41
|
+
@optionValue="value"
|
|
42
|
+
@placeholder={{t "settings.map.select-map-type"}}
|
|
43
|
+
@onSelect={{fn (mut this.googleMapsMapType)}}
|
|
44
|
+
/>
|
|
45
|
+
</InputGroup>
|
|
46
|
+
|
|
47
|
+
<InputGroup>
|
|
48
|
+
<Toggle
|
|
49
|
+
@label={{t "settings.map.show-traffic-layer"}}
|
|
50
|
+
@isToggled={{this.showGoogleMapsTrafficLayer}}
|
|
51
|
+
@onToggle={{fn (mut this.showGoogleMapsTrafficLayer)}}
|
|
52
|
+
@helpText={{t "settings.map.show-traffic-layer-help-text"}}
|
|
53
|
+
/>
|
|
54
|
+
</InputGroup>
|
|
55
|
+
|
|
56
|
+
<InputGroup>
|
|
57
|
+
<Toggle
|
|
58
|
+
@label={{t "settings.map.show-transit-layer"}}
|
|
59
|
+
@isToggled={{this.showGoogleMapsTransitLayer}}
|
|
60
|
+
@onToggle={{fn (mut this.showGoogleMapsTransitLayer)}}
|
|
61
|
+
@helpText={{t "settings.map.show-transit-layer-help-text"}}
|
|
62
|
+
/>
|
|
63
|
+
</InputGroup>
|
|
64
|
+
{{/if}}
|
|
65
|
+
|
|
37
66
|
{{else}}
|
|
38
|
-
{{
|
|
67
|
+
{{! Loading state }}
|
|
39
68
|
<div class="flex items-center justify-center py-8">
|
|
40
69
|
<Spinner />
|
|
41
70
|
</div>
|
|
42
71
|
{{/if}}
|
|
43
72
|
</ContentPanel>
|
|
44
73
|
|
|
45
|
-
{{
|
|
74
|
+
{{! Extension point for third-party panels }}
|
|
46
75
|
<RegistryYield @registry="fleet-ops:template:settings:map" as |RegistryComponent|>
|
|
47
76
|
<RegistryComponent @controller={{this}} />
|
|
48
77
|
</RegistryYield>
|
|
@@ -50,4 +79,4 @@
|
|
|
50
79
|
</div>
|
|
51
80
|
</div>
|
|
52
81
|
<Spacer @height="600px" />
|
|
53
|
-
</Layout::Section::Body>
|
|
82
|
+
</Layout::Section::Body>
|
|
@@ -79,7 +79,7 @@ function waitForLeafletGlobal({ timeoutMs = 8000 } = {}) {
|
|
|
79
79
|
|
|
80
80
|
if (timeoutMs != null && Date.now() - startedAt >= timeoutMs) {
|
|
81
81
|
clearInterval(interval);
|
|
82
|
-
reject(new Error('[
|
|
82
|
+
reject(new Error('[Fleet-Ops Leaflet] Leaflet global is not available'));
|
|
83
83
|
}
|
|
84
84
|
}, 50);
|
|
85
85
|
});
|
|
@@ -105,7 +105,7 @@ function appendStylesheet(href) {
|
|
|
105
105
|
|
|
106
106
|
function loadScript(src, { timeoutMs = 8000, isReady = null } = {}) {
|
|
107
107
|
if (typeof document === 'undefined') {
|
|
108
|
-
return Promise.reject(new Error('[
|
|
108
|
+
return Promise.reject(new Error('[Fleet-Ops Leaflet] document is not available'));
|
|
109
109
|
}
|
|
110
110
|
|
|
111
111
|
const existing = findAssetElement('script', 'src', src);
|
|
@@ -137,7 +137,7 @@ function loadScript(src, { timeoutMs = 8000, isReady = null } = {}) {
|
|
|
137
137
|
|
|
138
138
|
const onError = () => {
|
|
139
139
|
cleanup();
|
|
140
|
-
reject(new Error(`[
|
|
140
|
+
reject(new Error(`[Fleet-Ops Leaflet] Failed to load ${src}`));
|
|
141
141
|
};
|
|
142
142
|
|
|
143
143
|
script.addEventListener('load', onLoad);
|
|
@@ -146,7 +146,7 @@ function loadScript(src, { timeoutMs = 8000, isReady = null } = {}) {
|
|
|
146
146
|
if (timeoutMs != null) {
|
|
147
147
|
timeoutId = setTimeout(() => {
|
|
148
148
|
cleanup();
|
|
149
|
-
reject(new Error(`[
|
|
149
|
+
reject(new Error(`[Fleet-Ops Leaflet] Timed out loading ${src}`));
|
|
150
150
|
}, timeoutMs);
|
|
151
151
|
}
|
|
152
152
|
|
|
@@ -202,7 +202,7 @@ export default function ensureLeafletPluginsReady(options = {}) {
|
|
|
202
202
|
.then(() => {
|
|
203
203
|
const readyLeaflet = normalizeLeafletGlobal();
|
|
204
204
|
if (!hasLeafletPluginsReady()) {
|
|
205
|
-
throw new Error('[
|
|
205
|
+
throw new Error('[Fleet-Ops Leaflet] Leaflet plugins loaded but required Draw/contextmenu APIs are missing');
|
|
206
206
|
}
|
|
207
207
|
|
|
208
208
|
window.fleetopsLeafletPluginsLoaded = true;
|
|
@@ -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 @@
|
|
|
1
|
+
export { default } from '@fleetbase/fleetops-engine/components/device-event/details';
|
package/composer.json
CHANGED