@fleetbase/fleetops-engine 0.6.19 → 0.6.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/addon/components/custom-entity/form.hbs +14 -14
- package/addon/components/device/details.hbs +92 -43
- package/addon/components/device/form.hbs +108 -60
- package/addon/components/device/form.js +36 -8
- package/addon/components/device/panel-header.hbs +32 -0
- package/addon/components/device/panel-header.js +3 -0
- package/addon/components/driver/form.hbs +1 -1
- package/addon/components/driver/form.js +49 -47
- package/addon/components/entity/form.hbs +7 -5
- package/addon/components/layout/fleet-ops-sidebar.js +12 -12
- package/addon/components/map/drawer/device-event-listing.hbs +58 -0
- package/addon/components/map/drawer/device-event-listing.js +181 -0
- package/addon/components/map/drawer/position-listing.hbs +84 -0
- package/addon/components/map/drawer/position-listing.js +289 -0
- package/addon/components/map/drawer.js +2 -0
- package/addon/components/map/leaflet-live-map.hbs +7 -2
- package/addon/components/order/details/payload.hbs +6 -4
- package/addon/components/order/details/payload.js +2 -0
- package/addon/components/order/kanban.hbs +12 -10
- package/addon/components/order/kanban.js +27 -3
- package/addon/components/order-config-manager/custom-fields.js +1 -1
- package/addon/components/positions-replay.hbs +333 -0
- package/addon/components/positions-replay.js +372 -0
- package/addon/components/sensor/details.hbs +64 -38
- package/addon/components/sensor/form.hbs +112 -63
- package/addon/components/sensor/form.js +36 -24
- package/addon/components/sensor/panel-header.hbs +32 -0
- package/addon/components/sensor/panel-header.js +3 -0
- package/addon/components/telematic/details.hbs +40 -16
- package/addon/components/telematic/form.hbs +63 -64
- package/addon/components/telematic/form.js +73 -4
- package/addon/components/vehicle/card.hbs +1 -1
- package/addon/controllers/analytics/reports/index/edit.js +1 -1
- package/addon/controllers/connectivity/devices/index/details.js +22 -1
- package/addon/controllers/connectivity/devices/index/edit.js +66 -1
- package/addon/controllers/connectivity/devices/index.js +51 -9
- package/addon/controllers/connectivity/events/index.js +65 -16
- package/addon/controllers/connectivity/sensors/index/details.js +22 -1
- package/addon/controllers/connectivity/sensors/index/edit.js +66 -1
- package/addon/controllers/connectivity/sensors/index.js +66 -6
- package/addon/controllers/connectivity/telematics/index/details.js +22 -1
- package/addon/controllers/connectivity/telematics/index/edit.js +66 -1
- package/addon/controllers/connectivity/telematics/index.js +20 -11
- package/addon/controllers/management/fleets/index/details.js +26 -21
- package/addon/controllers/management/fleets/index/edit.js +9 -6
- package/addon/controllers/management/vehicles/index/details.js +21 -13
- package/addon/controllers/operations/orders/index/new.js +4 -2
- package/addon/controllers/operations/orders/index.js +50 -45
- package/addon/controllers/settings/custom-fields.js +6 -0
- package/addon/helpers/get-fleet-ops-option-label.js +11 -0
- package/addon/routes/connectivity/devices/index/details.js +27 -1
- package/addon/routes/connectivity/devices/index/edit.js +27 -1
- package/addon/routes/connectivity/sensors/index/details.js +27 -1
- package/addon/routes/connectivity/sensors/index/edit.js +27 -1
- package/addon/routes/connectivity/telematics/index/details.js +27 -1
- package/addon/routes/connectivity/telematics/index/edit.js +27 -1
- package/addon/routes/management/vehicles/index/details/positions.js +3 -0
- package/addon/routes/operations/orders/index.js +0 -3
- package/addon/routes.js +1 -0
- package/addon/services/movement-tracker.js +81 -30
- package/addon/services/order-creation.js +4 -8
- package/addon/services/order-validation.js +3 -3
- package/addon/styles/fleetops-engine.css +192 -0
- package/addon/templates/connectivity/devices/index/details/index.hbs +2 -2
- package/addon/templates/connectivity/devices/index/details.hbs +15 -2
- package/addon/templates/connectivity/devices/index/edit.hbs +1 -1
- package/addon/templates/connectivity/events/index.hbs +1 -1
- package/addon/templates/connectivity/sensors/index/details/index.hbs +2 -2
- package/addon/templates/connectivity/sensors/index/details.hbs +15 -2
- package/addon/templates/connectivity/sensors/index/edit.hbs +1 -1
- package/addon/templates/connectivity/telematics/index/details/index.hbs +2 -2
- package/addon/templates/connectivity/telematics/index/details.hbs +14 -2
- package/addon/templates/connectivity/telematics/index/edit.hbs +1 -1
- package/addon/templates/management/vehicles/index/details/positions.hbs +1 -0
- package/addon/templates/operations/orders/index.hbs +26 -2
- package/addon/utils/fleet-ops-options.js +95 -0
- package/addon/utils/setup-customer-portal.js +7 -0
- package/app/components/device/panel-header.js +1 -0
- package/app/components/map/drawer/device-event-listing.js +1 -0
- package/app/components/map/drawer/position-listing.js +1 -0
- package/app/components/positions-replay.js +1 -0
- package/app/components/sensor/panel-header.js +1 -0
- package/app/helpers/get-fleet-ops-option-label.js +1 -0
- package/app/routes/management/vehicles/index/details/positions.js +1 -0
- package/app/templates/management/vehicles/index/details/positions.js +1 -0
- package/composer.json +1 -1
- package/extension.json +1 -1
- package/package.json +4 -4
- package/server/config/telematics.php +111 -0
- package/server/migrations/2025_10_27_000001_add_telematics_integration_fields.php +70 -0
- package/server/migrations/2025_10_27_171322_fix_device_column_names.php +107 -0
- package/server/migrations/2025_10_27_203023_add_company_uuid_to_device_events_table.php +28 -0
- package/server/src/Console/Commands/ReplayVehicleLocations.php +225 -0
- package/server/src/Contracts/TelematicProviderDescriptor.php +72 -0
- package/server/src/Contracts/TelematicProviderInterface.php +119 -0
- package/server/src/Exceptions/TelematicProviderException.php +14 -0
- package/server/src/Exceptions/TelematicRateLimitExceededException.php +12 -0
- package/server/src/Http/Controllers/Api/v1/DriverController.php +24 -14
- package/server/src/Http/Controllers/Api/v1/VehicleController.php +27 -7
- package/server/src/Http/Controllers/Internal/v1/DeviceController.php +22 -0
- package/server/src/Http/Controllers/Internal/v1/OrderController.php +50 -68
- package/server/src/Http/Controllers/Internal/v1/PositionController.php +240 -0
- package/server/src/Http/Controllers/Internal/v1/SensorController.php +11 -0
- package/server/src/Http/Controllers/Internal/v1/TelematicController.php +141 -0
- package/server/src/Http/Controllers/Internal/v1/TelematicWebhookController.php +170 -0
- package/server/src/Http/Filter/DeviceEventFilter.php +68 -0
- package/server/src/Http/Filter/PositionFilter.php +35 -0
- package/server/src/Http/Resources/v1/Position.php +44 -0
- package/server/src/Jobs/ReplayPositions.php +64 -0
- package/server/src/Jobs/SendPositionReplay.php +65 -0
- package/server/src/Jobs/SyncTelematicDevicesJob.php +106 -0
- package/server/src/Jobs/TestTelematicConnectionJob.php +102 -0
- package/server/src/Models/Device.php +72 -10
- package/server/src/Models/DeviceEvent.php +7 -0
- package/server/src/Models/Driver.php +28 -1
- package/server/src/Models/Payload.php +11 -3
- package/server/src/Models/Place.php +9 -2
- package/server/src/Models/Position.php +17 -17
- package/server/src/Models/Sensor.php +78 -13
- package/server/src/Models/Telematic.php +116 -6
- package/server/src/Models/Vehicle.php +104 -1
- package/server/src/Providers/FleetOpsServiceProvider.php +2 -0
- package/server/src/Support/Telematics/Providers/AbstractProvider.php +151 -0
- package/server/src/Support/Telematics/Providers/FlespiProvider.php +182 -0
- package/server/src/Support/Telematics/Providers/GeotabProvider.php +181 -0
- package/server/src/Support/Telematics/Providers/SamsaraProvider.php +177 -0
- package/server/src/Support/Telematics/TelematicProviderRegistry.php +147 -0
- package/server/src/Support/Telematics/TelematicService.php +223 -0
- package/server/src/Support/Utils.php +1 -1
- package/server/src/routes.php +12 -1
|
@@ -10,17 +10,19 @@ import LeafletTrackingMarkerComponent from '../components/leaflet-tracking-marke
|
|
|
10
10
|
export class EventBuffer {
|
|
11
11
|
@tracked events = [];
|
|
12
12
|
@tracked waitTime = 1000 * 3;
|
|
13
|
+
@tracked callback;
|
|
13
14
|
@tracked intervalId;
|
|
14
15
|
@tracked model;
|
|
15
16
|
|
|
16
|
-
constructor(model, waitTime = 1000 * 3) {
|
|
17
|
+
constructor(model, { callback = null, waitTime = 1000 * 3 }) {
|
|
17
18
|
this.model = model;
|
|
19
|
+
this.callback = callback;
|
|
18
20
|
this.waitTime = waitTime;
|
|
19
21
|
}
|
|
20
22
|
|
|
21
23
|
start() {
|
|
22
24
|
this.intervalId = setInterval(() => {
|
|
23
|
-
const bufferReady = this.process.isIdle && this.events.length;
|
|
25
|
+
const bufferReady = this.process.isIdle && this.events.length > 0;
|
|
24
26
|
if (bufferReady) {
|
|
25
27
|
this.process.perform();
|
|
26
28
|
}
|
|
@@ -32,11 +34,11 @@ export class EventBuffer {
|
|
|
32
34
|
}
|
|
33
35
|
|
|
34
36
|
clear() {
|
|
35
|
-
this.events
|
|
37
|
+
this.events = [];
|
|
36
38
|
}
|
|
37
39
|
|
|
38
40
|
add(event) {
|
|
39
|
-
this.events.
|
|
41
|
+
this.events = [...this.events, event];
|
|
40
42
|
}
|
|
41
43
|
|
|
42
44
|
removeByIndex(index) {
|
|
@@ -44,65 +46,112 @@ export class EventBuffer {
|
|
|
44
46
|
}
|
|
45
47
|
|
|
46
48
|
remove(event) {
|
|
47
|
-
this.events = this.events.
|
|
49
|
+
this.events = this.events.filter((e) => e !== event);
|
|
48
50
|
}
|
|
49
51
|
|
|
50
52
|
@task *process() {
|
|
51
53
|
debug('Processing movement tracker event buffer.');
|
|
54
|
+
|
|
55
|
+
// Take a snapshot of events to process and clear buffer immediately
|
|
56
|
+
// This prevents losing events that arrive during processing
|
|
57
|
+
const eventsToProcess = [...this.events];
|
|
58
|
+
this.events = []; // Clear immediately to accept new events
|
|
59
|
+
|
|
52
60
|
// Sort events by created_at
|
|
53
|
-
|
|
61
|
+
eventsToProcess.sort((a, b) => new Date(a.created_at) - new Date(b.created_at));
|
|
62
|
+
debug(`[MovementTracker EventBuffer processing ${eventsToProcess.length} events]`);
|
|
54
63
|
|
|
55
64
|
// Process sorted events
|
|
56
|
-
for (const output of
|
|
65
|
+
for (const output of eventsToProcess) {
|
|
57
66
|
const { event, data } = output;
|
|
58
67
|
|
|
68
|
+
// get movingObject marker
|
|
69
|
+
const marker = this.model.leafletLayer || this.model._layer || this.model._marker;
|
|
70
|
+
if (!marker || !marker._map) {
|
|
71
|
+
debug('No marker or marker not on map yet');
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
|
|
59
75
|
// log incoming event
|
|
60
76
|
debug(`${event} - ${data.id} ${data.additionalData?.index ? '#' + data.additionalData?.index : ''} (${output.created_at}) [ ${data.location.coordinates.join(' ')} ]`);
|
|
61
77
|
|
|
62
|
-
//
|
|
63
|
-
const
|
|
64
|
-
|
|
65
|
-
|
|
78
|
+
// GeoJSON -> Leaflet [lat, lng]
|
|
79
|
+
const [lng, lat] = data.location.coordinates;
|
|
80
|
+
const nextLatLng = [lat, lng];
|
|
81
|
+
|
|
82
|
+
// Calc speed
|
|
83
|
+
const map = marker._map;
|
|
84
|
+
const prev = marker.getLatLng();
|
|
85
|
+
const meters = map ? map.distance(prev, nextLatLng) : prev.distanceTo(nextLatLng);
|
|
86
|
+
|
|
87
|
+
// Assume payload speed is m/s; if it's km/h, convert: mps = kmh / 3.6
|
|
88
|
+
let mps = Number.isFinite(data.speed) && data.speed > 0 ? data.speed : null;
|
|
89
|
+
|
|
90
|
+
// Reduce animation duration and clamp between 100ms and 500ms
|
|
91
|
+
// This makes animations faster and prevents long delays
|
|
92
|
+
const durationMs = mps ? Math.max(100, Math.min((meters / mps) * 1000, 500)) : 500;
|
|
93
|
+
|
|
94
|
+
try {
|
|
95
|
+
// Apply rotation if heading is valid
|
|
96
|
+
if (typeof marker.setRotationAngle === 'function' && Number.isFinite(data.heading) && data.heading !== -1) {
|
|
66
97
|
marker.setRotationAngle(data.heading);
|
|
67
98
|
}
|
|
68
99
|
|
|
100
|
+
// Move marker with animation
|
|
69
101
|
if (typeof marker.slideTo === 'function') {
|
|
70
|
-
marker.slideTo(
|
|
102
|
+
marker.slideTo(nextLatLng, { duration: durationMs });
|
|
71
103
|
} else {
|
|
72
|
-
marker.setLatLng(
|
|
104
|
+
marker.setLatLng(nextLatLng);
|
|
73
105
|
}
|
|
74
106
|
|
|
75
|
-
|
|
107
|
+
if (typeof this.callback === 'function') {
|
|
108
|
+
this.callback(output, { nextLatLng, duration: durationMs, mps });
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Wait for animation to complete
|
|
112
|
+
yield timeout(durationMs + 50);
|
|
113
|
+
} catch (err) {
|
|
114
|
+
debug('MovementTracker EventBuffer error: ' + err.message);
|
|
76
115
|
}
|
|
77
116
|
}
|
|
78
117
|
|
|
79
|
-
//
|
|
80
|
-
|
|
118
|
+
// Don't clear here - we already cleared at the start
|
|
119
|
+
debug(`[MovementTracker EventBuffer finished processing ${eventsToProcess.length} events]`);
|
|
81
120
|
}
|
|
82
121
|
}
|
|
83
122
|
|
|
84
123
|
export default class MovementTrackerService extends Service {
|
|
85
124
|
@service socket;
|
|
86
125
|
@tracked channels = [];
|
|
126
|
+
@tracked buffers = new Map();
|
|
87
127
|
|
|
88
128
|
constructor() {
|
|
89
129
|
super(...arguments);
|
|
90
130
|
this.registerTrackingMarker();
|
|
91
131
|
}
|
|
92
132
|
|
|
93
|
-
|
|
133
|
+
#getOwner(owner = null) {
|
|
94
134
|
return owner ?? window.Fleetbase ?? getOwner(this);
|
|
95
135
|
}
|
|
96
136
|
|
|
137
|
+
#getBuffer(key, model, opts = {}) {
|
|
138
|
+
let buf = this.buffers.get(key);
|
|
139
|
+
if (!buf) {
|
|
140
|
+
buf = new EventBuffer(model, opts);
|
|
141
|
+
buf.start();
|
|
142
|
+
this.buffers.set(key, buf);
|
|
143
|
+
}
|
|
144
|
+
return buf;
|
|
145
|
+
}
|
|
146
|
+
|
|
97
147
|
registerTrackingMarker(_owner = null) {
|
|
98
|
-
const owner = this
|
|
148
|
+
const owner = this.#getOwner(_owner);
|
|
99
149
|
const emberLeafletService = owner.lookup('service:ember-leaflet');
|
|
100
150
|
|
|
101
151
|
if (emberLeafletService) {
|
|
102
152
|
const alreadyRegistered = emberLeafletService.components.find((registeredComponent) => registeredComponent.name === 'leaflet-tracking-marker');
|
|
103
|
-
if (alreadyRegistered)
|
|
104
|
-
|
|
105
|
-
}
|
|
153
|
+
if (alreadyRegistered) return;
|
|
154
|
+
|
|
106
155
|
// we then invoke the `registerComponent` method
|
|
107
156
|
emberLeafletService.registerComponent('leaflet-tracking-marker', {
|
|
108
157
|
as: 'tracking-marker',
|
|
@@ -123,37 +172,39 @@ export default class MovementTrackerService extends Service {
|
|
|
123
172
|
});
|
|
124
173
|
}
|
|
125
174
|
|
|
126
|
-
async track(model) {
|
|
175
|
+
async track(model, options = {}) {
|
|
127
176
|
// Create socket instance
|
|
128
177
|
const socket = this.socket.instance();
|
|
129
178
|
|
|
130
179
|
// Get model type and identifier
|
|
131
180
|
const type = getModelName(model);
|
|
132
181
|
const identifier = model.id;
|
|
133
|
-
|
|
182
|
+
|
|
183
|
+
// Location events to listen for
|
|
184
|
+
const locationEvents = [`${type}.location_changed`, `${type}.simulated_location_changed`, 'position.changed', 'position.simulated'];
|
|
134
185
|
|
|
135
186
|
// Listen on the specific channel
|
|
136
|
-
const channelId = `${type}.${identifier}`;
|
|
187
|
+
const channelId = options?.channelId ?? `${type}.${identifier}`;
|
|
137
188
|
const channel = socket.subscribe(channelId);
|
|
138
189
|
|
|
190
|
+
// Debug output
|
|
191
|
+
debug(`Tracking movement started for ${type} with id ${identifier}${options?.channelId ? ' on channel ' + channelId : ''}`, model);
|
|
192
|
+
|
|
139
193
|
// Track the channel
|
|
140
|
-
this.channels.
|
|
194
|
+
this.channels = [...this.channels, channel];
|
|
141
195
|
|
|
142
196
|
// Listen to the channel for events
|
|
143
197
|
await channel.listener('subscribe').once();
|
|
144
198
|
|
|
145
199
|
// Create event buffer for tracking model
|
|
146
|
-
const eventBuffer =
|
|
147
|
-
|
|
148
|
-
// Start tracking with event buffer
|
|
149
|
-
eventBuffer.start();
|
|
200
|
+
const eventBuffer = this.#getBuffer(channelId, model, options);
|
|
150
201
|
|
|
151
202
|
// Get incoming data and console out
|
|
152
203
|
(async () => {
|
|
153
204
|
for await (let output of channel) {
|
|
154
205
|
const { event } = output;
|
|
155
206
|
|
|
156
|
-
if (
|
|
207
|
+
if (locationEvents.includes(event)) {
|
|
157
208
|
eventBuffer.add(output);
|
|
158
209
|
debug(`Socket Event : ${event} : Added to EventBuffer : ${JSON.stringify(output)}`);
|
|
159
210
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import Service, { inject as service } from '@ember/service';
|
|
2
2
|
import { tracked } from '@glimmer/tracking';
|
|
3
|
-
import {
|
|
3
|
+
import { next } from '@ember/runloop';
|
|
4
4
|
|
|
5
5
|
export default class OrderCreationService extends Service {
|
|
6
6
|
@service orderActions;
|
|
@@ -12,13 +12,9 @@ export default class OrderCreationService extends Service {
|
|
|
12
12
|
const order = this.orderActions.createNewInstance(attrs);
|
|
13
13
|
this.order = order;
|
|
14
14
|
|
|
15
|
-
|
|
16
|
-
this,
|
|
17
|
-
|
|
18
|
-
this.addContext('order', order);
|
|
19
|
-
},
|
|
20
|
-
0
|
|
21
|
-
);
|
|
15
|
+
next(() => {
|
|
16
|
+
this.addContext('order', order);
|
|
17
|
+
});
|
|
22
18
|
|
|
23
19
|
return order;
|
|
24
20
|
}
|
|
@@ -23,7 +23,7 @@ export default class OrderValidationService extends Service {
|
|
|
23
23
|
const hasWaypoints = order.payload.waypoints.length >= 2;
|
|
24
24
|
const hasPickup = isNotEmpty(order.payload.pickup);
|
|
25
25
|
const hasDropoff = isNotEmpty(order.payload.dropoff);
|
|
26
|
-
const hasValidCustomFields = cfManager ? this.isCustomFieldsValid(cfManager) :
|
|
26
|
+
const hasValidCustomFields = cfManager ? this.isCustomFieldsValid(cfManager) : true;
|
|
27
27
|
|
|
28
28
|
if (hasWaypoints) {
|
|
29
29
|
return hasOrderConfig && hasOrderType && hasValidCustomFields;
|
|
@@ -37,13 +37,13 @@ export default class OrderValidationService extends Service {
|
|
|
37
37
|
}
|
|
38
38
|
|
|
39
39
|
validateCustomFields(cfManager) {
|
|
40
|
-
if (!cfManager) return
|
|
40
|
+
if (!cfManager) return true;
|
|
41
41
|
|
|
42
42
|
return cfManager.validateRequired();
|
|
43
43
|
}
|
|
44
44
|
|
|
45
45
|
isCustomFieldsValid(cfManager) {
|
|
46
|
-
if (!cfManager) return
|
|
46
|
+
if (!cfManager) return true;
|
|
47
47
|
|
|
48
48
|
const { isValid } = this.validateCustomFields(cfManager);
|
|
49
49
|
return isValid;
|
|
@@ -1654,3 +1654,195 @@ button.fleetops-btn-xxs,
|
|
|
1654
1654
|
padding-top: 0.2rem !important;
|
|
1655
1655
|
padding-bottom: 0.2rem !important;
|
|
1656
1656
|
}
|
|
1657
|
+
|
|
1658
|
+
/** css fix for operations index/kanban */
|
|
1659
|
+
main.console-fleet-ops-operations-orders-index-index section.next-view-section {
|
|
1660
|
+
max-width: 100vw;
|
|
1661
|
+
min-width: 0;
|
|
1662
|
+
}
|
|
1663
|
+
|
|
1664
|
+
main.console-fleet-ops-operations-orders-index-index section.next-view-section > .next-view-section-container {
|
|
1665
|
+
display: flex;
|
|
1666
|
+
flex-direction: column;
|
|
1667
|
+
max-width: 100vw;
|
|
1668
|
+
min-width: 0;
|
|
1669
|
+
}
|
|
1670
|
+
|
|
1671
|
+
main.console-fleet-ops-operations-orders-index-index section.next-view-section > .next-view-section-container > .next-view-section-subheader {
|
|
1672
|
+
flex: 0 0 auto;
|
|
1673
|
+
width: 100%;
|
|
1674
|
+
}
|
|
1675
|
+
|
|
1676
|
+
main.console-fleet-ops-operations-orders-index-index section.next-view-section > .next-view-section-container > .next-view-section-body {
|
|
1677
|
+
flex: 1 1 auto;
|
|
1678
|
+
min-width: 0;
|
|
1679
|
+
overflow: auto;
|
|
1680
|
+
}
|
|
1681
|
+
|
|
1682
|
+
main.console-fleet-ops-operations-orders-index-index section.next-view-section > .next-view-section-container > .next-view-section-body > .kanban-board {
|
|
1683
|
+
display: flex;
|
|
1684
|
+
}
|
|
1685
|
+
|
|
1686
|
+
.next-view-section-subheader .next-view-section-subheader-actions .order-board-type-filter.ember-power-select-trigger.ember-basic-dropdown-trigger {
|
|
1687
|
+
height: 2rem;
|
|
1688
|
+
align-items: center;
|
|
1689
|
+
padding-left: 0.5rem;
|
|
1690
|
+
padding-right: 2rem;
|
|
1691
|
+
}
|
|
1692
|
+
|
|
1693
|
+
.positions-replay-component {
|
|
1694
|
+
width: 100%;
|
|
1695
|
+
max-width: 100%;
|
|
1696
|
+
}
|
|
1697
|
+
|
|
1698
|
+
.positions-replay-component .metric-card {
|
|
1699
|
+
padding: 0.75rem;
|
|
1700
|
+
border-radius: 0.5rem;
|
|
1701
|
+
background-color: rgba(0, 0, 0, 2%);
|
|
1702
|
+
transition: all 0.2s ease-in-out;
|
|
1703
|
+
}
|
|
1704
|
+
|
|
1705
|
+
.positions-replay-component .metric-card:hover {
|
|
1706
|
+
background-color: rgba(0, 0, 0, 4%);
|
|
1707
|
+
box-shadow: 0 4px 6px rgba(0, 0, 0, 10%);
|
|
1708
|
+
}
|
|
1709
|
+
|
|
1710
|
+
body[data-theme='dark'] .positions-replay-component .metric-card {
|
|
1711
|
+
background-color: rgba(255, 255, 255, 5%);
|
|
1712
|
+
}
|
|
1713
|
+
|
|
1714
|
+
body[data-theme='dark'] .positions-replay-component .metric-card:hover {
|
|
1715
|
+
background-color: rgba(255, 255, 255, 8%);
|
|
1716
|
+
}
|
|
1717
|
+
|
|
1718
|
+
.positions-replay-component .metric-label {
|
|
1719
|
+
font-size: 0.65rem;
|
|
1720
|
+
text-transform: uppercase;
|
|
1721
|
+
letter-spacing: 0.05em;
|
|
1722
|
+
margin-bottom: 0.25rem;
|
|
1723
|
+
}
|
|
1724
|
+
|
|
1725
|
+
.positions-replay-component .metric-value {
|
|
1726
|
+
font-size: 0.85rem;
|
|
1727
|
+
line-height: 1;
|
|
1728
|
+
}
|
|
1729
|
+
|
|
1730
|
+
.positions-replay-component .positions-replay-map-container {
|
|
1731
|
+
overflow: hidden;
|
|
1732
|
+
height: 100%;
|
|
1733
|
+
width: 100%;
|
|
1734
|
+
}
|
|
1735
|
+
|
|
1736
|
+
.positions-replay-component .replay-controls {
|
|
1737
|
+
display: flex;
|
|
1738
|
+
align-items: center;
|
|
1739
|
+
justify-content: space-between;
|
|
1740
|
+
flex-wrap: wrap;
|
|
1741
|
+
gap: 1rem;
|
|
1742
|
+
}
|
|
1743
|
+
|
|
1744
|
+
.positions-replay-component .speed-control {
|
|
1745
|
+
display: flex;
|
|
1746
|
+
align-items: center;
|
|
1747
|
+
}
|
|
1748
|
+
|
|
1749
|
+
.positions-replay-component .speed-control > select.speed-select {
|
|
1750
|
+
height: 1.7rem;
|
|
1751
|
+
font-size: 0.85rem;
|
|
1752
|
+
line-height: 0.85rem;
|
|
1753
|
+
text-align: center;
|
|
1754
|
+
padding: 0 2rem 0 0.75rem;
|
|
1755
|
+
}
|
|
1756
|
+
|
|
1757
|
+
.positions-replay-component .replay-progress {
|
|
1758
|
+
display: flex;
|
|
1759
|
+
align-items: center;
|
|
1760
|
+
gap: 0.5rem;
|
|
1761
|
+
}
|
|
1762
|
+
|
|
1763
|
+
.positions-replay-component .progress-bar {
|
|
1764
|
+
position: relative;
|
|
1765
|
+
overflow: hidden;
|
|
1766
|
+
border-radius: 9999px;
|
|
1767
|
+
}
|
|
1768
|
+
|
|
1769
|
+
.positions-replay-component .progress-fill {
|
|
1770
|
+
height: 100%;
|
|
1771
|
+
background: linear-gradient(90deg, #3b82f6, #2563eb);
|
|
1772
|
+
transition: width 0.3s ease-in-out;
|
|
1773
|
+
}
|
|
1774
|
+
|
|
1775
|
+
.positions-replay-component .map-wrapper {
|
|
1776
|
+
position: relative;
|
|
1777
|
+
width: 100%;
|
|
1778
|
+
}
|
|
1779
|
+
|
|
1780
|
+
.positions-replay-component .positions-replay-map {
|
|
1781
|
+
width: 100%;
|
|
1782
|
+
height: 100%;
|
|
1783
|
+
}
|
|
1784
|
+
|
|
1785
|
+
.positions-replay-component .positions-replay-timeline {
|
|
1786
|
+
margin-top: 1rem;
|
|
1787
|
+
}
|
|
1788
|
+
|
|
1789
|
+
.positions-replay-component .timeline-container {
|
|
1790
|
+
position: relative;
|
|
1791
|
+
}
|
|
1792
|
+
|
|
1793
|
+
.positions-replay-component .timeline-track {
|
|
1794
|
+
display: flex;
|
|
1795
|
+
padding-bottom: 1rem;
|
|
1796
|
+
min-width: min-content;
|
|
1797
|
+
}
|
|
1798
|
+
|
|
1799
|
+
.positions-replay-component .timeline-item {
|
|
1800
|
+
position: relative;
|
|
1801
|
+
min-width: 80px;
|
|
1802
|
+
padding: 0.5rem;
|
|
1803
|
+
border-radius: 0.375rem;
|
|
1804
|
+
transition: all 0.2s ease-in-out;
|
|
1805
|
+
}
|
|
1806
|
+
|
|
1807
|
+
.positions-replay-component .timeline-item:hover {
|
|
1808
|
+
transform: translateY(-2px);
|
|
1809
|
+
}
|
|
1810
|
+
|
|
1811
|
+
.positions-replay-component .timeline-marker {
|
|
1812
|
+
width: 0.75rem;
|
|
1813
|
+
height: 0.75rem;
|
|
1814
|
+
border-radius: 50%;
|
|
1815
|
+
margin: 0 auto 0.25rem;
|
|
1816
|
+
transition: all 0.2s ease-in-out;
|
|
1817
|
+
}
|
|
1818
|
+
|
|
1819
|
+
.positions-replay-component .timeline-item:hover .timeline-marker {
|
|
1820
|
+
transform: scale(1.3);
|
|
1821
|
+
}
|
|
1822
|
+
|
|
1823
|
+
.positions-replay-component .timeline-content {
|
|
1824
|
+
text-align: center;
|
|
1825
|
+
white-space: nowrap;
|
|
1826
|
+
}
|
|
1827
|
+
|
|
1828
|
+
.positions-replay-component .positions-replay-table table {
|
|
1829
|
+
width: 100%;
|
|
1830
|
+
border-collapse: collapse;
|
|
1831
|
+
}
|
|
1832
|
+
|
|
1833
|
+
.positions-replay-component .positions-replay-table th {
|
|
1834
|
+
position: sticky;
|
|
1835
|
+
top: 0;
|
|
1836
|
+
z-index: 10;
|
|
1837
|
+
white-space: nowrap;
|
|
1838
|
+
text-overflow: ellipsis;
|
|
1839
|
+
font-size: 0.65rem;
|
|
1840
|
+
}
|
|
1841
|
+
|
|
1842
|
+
.positions-replay-component .empty-state {
|
|
1843
|
+
display: flex;
|
|
1844
|
+
flex-direction: column;
|
|
1845
|
+
align-items: center;
|
|
1846
|
+
justify-content: center;
|
|
1847
|
+
min-height: 400px;
|
|
1848
|
+
}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
<Device::Details @resource={{@model}} />
|
|
2
|
+
<Spacer @height="200px" />
|
|
@@ -1,2 +1,15 @@
|
|
|
1
|
-
|
|
2
|
-
{{
|
|
1
|
+
<Layout::Resource::Panel
|
|
2
|
+
@resource={{@model}}
|
|
3
|
+
@controller={{this}}
|
|
4
|
+
@headerComponent={{component "device/panel-header" resource=@model}}
|
|
5
|
+
@headerTitle={{or @model.name @model.serial_number}}
|
|
6
|
+
@actionButtons={{this.actionButtons}}
|
|
7
|
+
@onPressCancel={{transition-to "connectivity.devices.index"}}
|
|
8
|
+
@onOverlayReady={{fn (mut this.overlay)}}
|
|
9
|
+
@headerClass="no-bottom-border"
|
|
10
|
+
@bodyClass="no-scroll"
|
|
11
|
+
>
|
|
12
|
+
<TabNavigation @tabs={{this.tabs}} @contentClass="scrollable" @tablistClass="pl-2">
|
|
13
|
+
{{outlet}}
|
|
14
|
+
</TabNavigation>
|
|
15
|
+
</Layout::Resource::Panel>
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<Layout::Resource::Panel
|
|
2
2
|
@resource={{@model}}
|
|
3
3
|
@controller={{this}}
|
|
4
|
-
@headerTitle={{concat "Edit: " @model.name}}
|
|
4
|
+
@headerTitle={{concat "Edit: " (or @model.name @model.serial_number)}}
|
|
5
5
|
@actionButtons={{this.actionButtons}}
|
|
6
6
|
@saveTask={{this.save}}
|
|
7
7
|
@onPressCancel={{this.cancel}}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
<Sensor::Details @resource={{@model}} />
|
|
2
|
+
<Spacer @height="200px" />
|
|
@@ -1,2 +1,15 @@
|
|
|
1
|
-
|
|
2
|
-
{{
|
|
1
|
+
<Layout::Resource::Panel
|
|
2
|
+
@resource={{@model}}
|
|
3
|
+
@controller={{this}}
|
|
4
|
+
@headerComponent={{component "sensor/panel-header" resource=@model}}
|
|
5
|
+
@headerTitle={{or @model.name @model.serial_number}}
|
|
6
|
+
@actionButtons={{this.actionButtons}}
|
|
7
|
+
@onPressCancel={{transition-to "connectivity.sensors.index"}}
|
|
8
|
+
@onOverlayReady={{fn (mut this.overlay)}}
|
|
9
|
+
@headerClass="no-bottom-border"
|
|
10
|
+
@bodyClass="no-scroll"
|
|
11
|
+
>
|
|
12
|
+
<TabNavigation @tabs={{this.tabs}} @contentClass="scrollable" @tablistClass="pl-2">
|
|
13
|
+
{{outlet}}
|
|
14
|
+
</TabNavigation>
|
|
15
|
+
</Layout::Resource::Panel>
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<Layout::Resource::Panel
|
|
2
2
|
@resource={{@model}}
|
|
3
3
|
@controller={{this}}
|
|
4
|
-
@headerTitle={{concat "Edit: " @model.name}}
|
|
4
|
+
@headerTitle={{concat "Edit: " (or @model.name @model.serial_number)}}
|
|
5
5
|
@actionButtons={{this.actionButtons}}
|
|
6
6
|
@saveTask={{this.save}}
|
|
7
7
|
@onPressCancel={{this.cancel}}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
<Telematic::Details @resource={{@model}} />
|
|
2
|
+
<Spacer @height="200px" />
|
|
@@ -1,2 +1,14 @@
|
|
|
1
|
-
|
|
2
|
-
{{
|
|
1
|
+
<Layout::Resource::Panel
|
|
2
|
+
@resource={{@model}}
|
|
3
|
+
@controller={{this}}
|
|
4
|
+
@headerTitle={{or @model.name @model.provider}}
|
|
5
|
+
@actionButtons={{this.actionButtons}}
|
|
6
|
+
@onPressCancel={{transition-to "connectivity.telematics.index"}}
|
|
7
|
+
@onOverlayReady={{fn (mut this.overlay)}}
|
|
8
|
+
@headerClass="no-bottom-border"
|
|
9
|
+
@bodyClass="no-scroll"
|
|
10
|
+
>
|
|
11
|
+
<TabNavigation @tabs={{this.tabs}} @contentClass="scrollable" @tablistClass="pl-2">
|
|
12
|
+
{{outlet}}
|
|
13
|
+
</TabNavigation>
|
|
14
|
+
</Layout::Resource::Panel>
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<Layout::Resource::Panel
|
|
2
2
|
@resource={{@model}}
|
|
3
3
|
@controller={{this}}
|
|
4
|
-
@headerTitle={{concat "Edit: " @model.name}}
|
|
4
|
+
@headerTitle={{concat "Edit: " (or @model.name @model.provider)}}
|
|
5
5
|
@actionButtons={{this.actionButtons}}
|
|
6
6
|
@saveTask={{this.save}}
|
|
7
7
|
@onPressCancel={{this.cancel}}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<PositionsReplay @resource={{@model}} />
|
|
@@ -53,9 +53,33 @@
|
|
|
53
53
|
{{/if}}
|
|
54
54
|
|
|
55
55
|
{{#if (eq this.layout "kanban")}}
|
|
56
|
-
<Layout::Section::Header @title={{t "menu.order-board"}} @actionsWrapperClass="space-x-1"
|
|
56
|
+
<Layout::Section::Header @title={{t "menu.order-board"}} @subtitle={{@model.length}} @subtitleClass="text-xs text-center font-semibold ml-2 w-6 h-6 rounded-full bg-blue-100 text-blue-900 dark:bg-blue-900 dark:text-blue-100 flex items-center justify-center" @actionsWrapperClass="space-x-1">
|
|
57
|
+
<div class="flex flex-row items-center space-x-2">
|
|
58
|
+
<div class="w-64">
|
|
59
|
+
<div class="fleetbase-model-select fleetbase-power-select ember-model-select">
|
|
60
|
+
<ModelSelect
|
|
61
|
+
@modelName="order-config"
|
|
62
|
+
@selectedModel={{or this.orderConfig this.type}}
|
|
63
|
+
@placeholder={{t "order.fields.order-type-placeholder"}}
|
|
64
|
+
@triggerClass="form-select form-input order-board-type-filter"
|
|
65
|
+
@infiniteScroll={{false}}
|
|
66
|
+
@renderInPlace={{true}}
|
|
67
|
+
@allowClear={{true}}
|
|
68
|
+
@onChange={{fn (mut this.orderConfig)}}
|
|
69
|
+
@onChangeId={{fn (mut this.type)}}
|
|
70
|
+
as |orderConfig|
|
|
71
|
+
>
|
|
72
|
+
<div class="text-sm">
|
|
73
|
+
<div class="font-semibold normalize-in-trigger">{{orderConfig.name}}</div>
|
|
74
|
+
<div class="hide-from-trigger">{{n-a orderConfig.description}}</div>
|
|
75
|
+
</div>
|
|
76
|
+
</ModelSelect>
|
|
77
|
+
</div>
|
|
78
|
+
</div>
|
|
79
|
+
</div>
|
|
80
|
+
</Layout::Section::Header>
|
|
57
81
|
<Layout::Section::Body>
|
|
58
|
-
<Order::Kanban @orders={{@model}} @headerOffset={{160}} />
|
|
82
|
+
<Order::Kanban @orders={{@model}} @headerOffset={{160}} @orderConfig={{this.type}} />
|
|
59
83
|
</Layout::Section::Body>
|
|
60
84
|
{{/if}}
|
|
61
85
|
|