@fleetbase/fleetops-engine 0.6.43 → 0.6.45
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/entity/form.hbs +24 -0
- package/addon/components/entity/form.js +30 -0
- package/addon/components/leaflet-draw-control.js +1 -1
- package/addon/components/map/drawer/geofence-event-listing.hbs +123 -0
- package/addon/components/map/drawer/geofence-event-listing.js +103 -0
- package/addon/components/map/drawer.js +1 -0
- package/addon/components/map/leaflet-live-map.js +83 -0
- package/addon/components/modals/orchestrator-import.js +2 -78
- package/addon/components/order/form/service-rate.hbs +1 -1
- package/addon/components/service-area/form.hbs +59 -0
- package/addon/components/service-rate/details.hbs +6 -6
- package/addon/components/service-rate/form.hbs +28 -15
- package/addon/components/zone/form.hbs +60 -1
- package/addon/controllers/operations/orders/index/details/virtual.js +5 -0
- package/addon/controllers/operations/orders/index/details.js +27 -2
- package/addon/controllers/operations/service-rates/index/edit.js +1 -0
- package/addon/controllers/operations/service-rates/index/new.js +1 -0
- package/addon/helpers/f-to-int.js +6 -0
- package/addon/routes/application.js +3 -3
- package/addon/routes/operations/orders/index/details/virtual.js +1 -0
- package/addon/routes/operations/orders/index/details.js +19 -3
- package/addon/services/equipment-actions.js +4 -6
- package/addon/services/geofence-event-bus.js +223 -0
- package/addon/services/leaflet-map-manager.js +4 -0
- package/addon/services/maintenance-actions.js +4 -6
- package/addon/services/part-actions.js +4 -6
- package/addon/services/service-rate-actions.js +8 -3
- package/addon/templates/operations/orders/index/details/virtual.hbs +7 -1
- package/addon/utils/fleet-ops-options.js +33 -0
- package/addon/utils/serialize-payload.js +1 -1
- package/app/components/map/drawer/geofence-event-listing.js +1 -0
- package/app/controllers/operations/orders/index/details/virtual.js +1 -0
- package/app/helpers/f-to-int.js +1 -0
- package/app/services/geofence-event-bus.js +1 -0
- package/composer.json +1 -1
- package/extension.json +1 -1
- package/package.json +2 -2
- package/server/migrations/2026_04_05_000001_add_geofence_config_to_zones_table.php +48 -0
- package/server/migrations/2026_04_05_000002_add_geofence_config_to_service_areas_table.php +38 -0
- package/server/migrations/2026_04_05_000003_create_driver_geofence_states_table.php +62 -0
- package/server/migrations/2026_04_05_000004_create_geofence_events_log_table.php +66 -0
- package/server/migrations/2026_04_22_000001_add_vehicle_support_to_geofence_events_log.php +31 -0
- package/server/migrations/2026_04_22_000002_create_vehicle_geofence_states_table.php +39 -0
- package/server/src/Console/Commands/SimulateGeofenceEvents.php +278 -0
- package/server/src/Events/GeofenceDwelled.php +208 -0
- package/server/src/Events/GeofenceEntered.php +207 -0
- package/server/src/Events/GeofenceExited.php +213 -0
- package/server/src/Http/Controllers/Api/v1/DriverController.php +100 -0
- package/server/src/Http/Controllers/Api/v1/GeofenceController.php +263 -0
- package/server/src/Http/Controllers/Api/v1/OrderController.php +13 -8
- package/server/src/Http/Controllers/Api/v1/PurchaseRateController.php +11 -3
- package/server/src/Http/Controllers/Api/v1/ServiceAreaController.php +2 -2
- package/server/src/Http/Controllers/Api/v1/ServiceQuoteController.php +4 -0
- package/server/src/Http/Controllers/Api/v1/ServiceRateController.php +6 -1
- package/server/src/Http/Controllers/Api/v1/VehicleController.php +88 -0
- package/server/src/Http/Controllers/Api/v1/ZoneController.php +2 -2
- package/server/src/Http/Controllers/Internal/v1/GeofenceController.php +7 -0
- package/server/src/Http/Requests/CreateServiceAreaRequest.php +8 -4
- package/server/src/Http/Requests/CreateServiceRateRequest.php +2 -2
- package/server/src/Http/Requests/CreateZoneRequest.php +8 -4
- package/server/src/Http/Resources/v1/Order.php +0 -1
- package/server/src/Http/Resources/v1/ServiceQuote.php +2 -2
- package/server/src/Jobs/CheckGeofenceDwell.php +139 -0
- package/server/src/Listeners/HandleGeofenceDwelled.php +56 -0
- package/server/src/Listeners/HandleGeofenceEntered.php +187 -0
- package/server/src/Listeners/HandleGeofenceExited.php +56 -0
- package/server/src/Models/Entity.php +7 -0
- package/server/src/Models/GeofenceEventLog.php +155 -0
- package/server/src/Models/Order.php +39 -7
- package/server/src/Models/ServiceArea.php +21 -2
- package/server/src/Models/ServiceQuote.php +3 -1
- package/server/src/Models/ServiceRate.php +122 -30
- package/server/src/Models/Zone.php +20 -2
- package/server/src/Notifications/DriverArrivedAtGeofence.php +86 -0
- package/server/src/Observers/PurchaseRateObserver.php +3 -5
- package/server/src/Providers/EventServiceProvider.php +19 -0
- package/server/src/Providers/FleetOpsServiceProvider.php +10 -0
- package/server/src/Rules/ComputableAlgo.php +1 -3
- package/server/src/Support/Algo.php +87 -1
- package/server/src/Support/GeofenceIntersectionService.php +206 -0
- package/server/src/Support/Utils.php +35 -0
- package/server/src/routes.php +16 -0
|
@@ -176,7 +176,7 @@
|
|
|
176
176
|
</tr>
|
|
177
177
|
</thead>
|
|
178
178
|
<tbody>
|
|
179
|
-
{{#each @resource.
|
|
179
|
+
{{#each @resource.rateFees as |rateFee|}}
|
|
180
180
|
<tr>
|
|
181
181
|
<td>
|
|
182
182
|
<Input
|
|
@@ -252,25 +252,38 @@
|
|
|
252
252
|
{{else if @resource.isAlgorithm}}
|
|
253
253
|
<ContentPanel @title={{t "service-rate.fields.custom-algorithm-title"}} @open={{true}} @wrapperClass="bordered-top">
|
|
254
254
|
<InfoBlock class="mb-5">
|
|
255
|
-
|
|
256
|
-
{{t "service-rate.fields.custom-algorithm-info-second-message"}}
|
|
255
|
+
Define a custom formula for this service rate using variables wrapped in a single pair of curly braces.
|
|
257
256
|
|
|
258
257
|
<div class="block my-4 break-text">
|
|
259
258
|
<ul class="list-disc space-y-2 pl-16">
|
|
260
|
-
<li class="leading-5"><
|
|
261
|
-
|
|
262
|
-
<li class="leading-5"><
|
|
263
|
-
|
|
259
|
+
<li class="leading-5"><code>{distance}</code>, <code>{distance_m}</code>, <code>{distance_km}</code>, <code>{distance_mi}</code>: route distance in meters, kilometers, or miles.</li>
|
|
260
|
+
<li class="leading-5"><code>{time}</code>, <code>{time_s}</code>, <code>{time_min}</code>: route time in seconds or minutes.</li>
|
|
261
|
+
<li class="leading-5"><code>{stops}</code>: total service stops including pickup, dropoff, and intermediate waypoints.</li>
|
|
262
|
+
<li class="leading-5"><code>{waypoints}</code>: intermediate waypoint count only.</li>
|
|
263
|
+
<li class="leading-5"><code>{parcels}</code>, <code>{entities}</code>: parcel count and total payload entity count.</li>
|
|
264
|
+
<li class="leading-5"><code>{base_fee}</code>: the configured base fee for this service rate.</li>
|
|
265
|
+
<li class="leading-5">Supported functions: <code>max(a,b)</code>, <code>min(a,b)</code>, <code>ceil(x)</code>, <code>floor(x)</code>, <code>round(x)</code>.</li>
|
|
264
266
|
</ul>
|
|
265
267
|
</div>
|
|
266
268
|
|
|
267
|
-
<div>
|
|
268
|
-
<
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
269
|
+
<div class="space-y-3">
|
|
270
|
+
<div>
|
|
271
|
+
<h4 class="mb-1 text-sm font-semibold">{{t "service-rate.fields.example"}} 1</h4>
|
|
272
|
+
<div class="mb-2 text-inherit">Charge by distance in miles after the first 15 miles, plus the base fee.</div>
|
|
273
|
+
<code class="text-xs font-mono">max({distance_mi} - 15, 0) * 5 + {base_fee}</code>
|
|
274
|
+
</div>
|
|
275
|
+
|
|
276
|
+
<div>
|
|
277
|
+
<h4 class="mb-1 text-sm font-semibold">{{t "service-rate.fields.example"}} 2</h4>
|
|
278
|
+
<div class="mb-2 text-inherit">Charge extra for stops after the first 2 service stops.</div>
|
|
279
|
+
<code class="text-xs font-mono">max({stops} - 2, 0) * 10 + {base_fee}</code>
|
|
280
|
+
</div>
|
|
281
|
+
|
|
282
|
+
<div>
|
|
283
|
+
<h4 class="mb-1 text-sm font-semibold">{{t "service-rate.fields.example"}} 3</h4>
|
|
284
|
+
<div class="mb-2 text-inherit">Combine waypoint, parcel, and distance surcharges in one formula.</div>
|
|
285
|
+
<code class="text-xs font-mono">max({waypoints}, 0) * 15 + max({parcels} - 3, 0) * 2 + max({distance_km} - 25, 0) * 0.5 + {base_fee}</code>
|
|
272
286
|
</div>
|
|
273
|
-
<code class="text-xs font-mono">(( {distance} / 50 ) * .05 ) + (( {time} / 60 ) * .01)</code>
|
|
274
287
|
</div>
|
|
275
288
|
</InfoBlock>
|
|
276
289
|
|
|
@@ -287,7 +300,7 @@
|
|
|
287
300
|
{{else if @resource.isParcelService}}
|
|
288
301
|
<ContentPanel @title={{t "service-rate.fields.percel-fee-title"}} @open={{true}} @wrapperClass="bordered-top">
|
|
289
302
|
<div class="space-y-2">
|
|
290
|
-
{{#each @resource.
|
|
303
|
+
{{#each @resource.parcelFees as |parcelFee|}}
|
|
291
304
|
<div class="dark:text-gray-100">
|
|
292
305
|
<div class="grid grid-cols-7">
|
|
293
306
|
<div class="flex flex-col items-start justify-start pt-3">
|
|
@@ -541,4 +554,4 @@
|
|
|
541
554
|
</InputGroup>
|
|
542
555
|
{{/if}}
|
|
543
556
|
</ContentPanel>
|
|
544
|
-
</div>
|
|
557
|
+
</div>
|
|
@@ -30,4 +30,63 @@
|
|
|
30
30
|
</InputGroup>
|
|
31
31
|
</div>
|
|
32
32
|
</ContentPanel>
|
|
33
|
-
|
|
33
|
+
|
|
34
|
+
<ContentPanel @title="Geofence Triggers" @open={{true}} @wrapperClass="bordered-top">
|
|
35
|
+
<div class="space-y-4">
|
|
36
|
+
<p class="text-sm text-gray-500 dark:text-gray-400">
|
|
37
|
+
Configure when this zone fires active geofence events. Entry and exit events are dispatched to webhooks,
|
|
38
|
+
WebSocket channels, and the notification system whenever a driver crosses the zone boundary.
|
|
39
|
+
</p>
|
|
40
|
+
|
|
41
|
+
<div class="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
|
42
|
+
{{! Entry trigger toggle }}
|
|
43
|
+
<div class="flex items-start space-x-3 p-3 rounded-lg border border-gray-200 dark:border-gray-700">
|
|
44
|
+
<div class="flex-shrink-0 mt-0.5">
|
|
45
|
+
<Toggle
|
|
46
|
+
@isToggled={{@resource.trigger_on_entry}}
|
|
47
|
+
@onToggle={{fn (mut @resource.trigger_on_entry)}}
|
|
48
|
+
/>
|
|
49
|
+
</div>
|
|
50
|
+
<div>
|
|
51
|
+
<p class="text-sm font-medium text-gray-900 dark:text-white">Trigger on Entry</p>
|
|
52
|
+
<p class="text-xs text-gray-500 dark:text-gray-400 mt-0.5">
|
|
53
|
+
Fire a <code class="font-mono">geofence.entered</code> event when a driver enters this zone.
|
|
54
|
+
</p>
|
|
55
|
+
</div>
|
|
56
|
+
</div>
|
|
57
|
+
|
|
58
|
+
{{! Exit trigger toggle }}
|
|
59
|
+
<div class="flex items-start space-x-3 p-3 rounded-lg border border-gray-200 dark:border-gray-700">
|
|
60
|
+
<div class="flex-shrink-0 mt-0.5">
|
|
61
|
+
<Toggle
|
|
62
|
+
@isToggled={{@resource.trigger_on_exit}}
|
|
63
|
+
@onToggle={{fn (mut @resource.trigger_on_exit)}}
|
|
64
|
+
/>
|
|
65
|
+
</div>
|
|
66
|
+
<div>
|
|
67
|
+
<p class="text-sm font-medium text-gray-900 dark:text-white">Trigger on Exit</p>
|
|
68
|
+
<p class="text-xs text-gray-500 dark:text-gray-400 mt-0.5">
|
|
69
|
+
Fire a <code class="font-mono">geofence.exited</code> event when a driver leaves this zone.
|
|
70
|
+
</p>
|
|
71
|
+
</div>
|
|
72
|
+
</div>
|
|
73
|
+
</div>
|
|
74
|
+
|
|
75
|
+
<InputGroup
|
|
76
|
+
@name="Dwell Threshold (minutes)"
|
|
77
|
+
@value={{@resource.dwell_threshold_minutes}}
|
|
78
|
+
@type="number"
|
|
79
|
+
@placeholder="e.g. 10"
|
|
80
|
+
@helpText="Fire a geofence.dwelled event after a driver has been inside this zone for this many minutes. Leave blank to disable dwell tracking."
|
|
81
|
+
/>
|
|
82
|
+
|
|
83
|
+
<InputGroup
|
|
84
|
+
@name="Speed Limit (km/h)"
|
|
85
|
+
@value={{@resource.speed_limit_kmh}}
|
|
86
|
+
@type="number"
|
|
87
|
+
@placeholder="e.g. 50"
|
|
88
|
+
@helpText="Optional speed limit enforced within this zone. Leave blank for no limit."
|
|
89
|
+
/>
|
|
90
|
+
</div>
|
|
91
|
+
</ContentPanel>
|
|
92
|
+
</div>
|
|
@@ -16,6 +16,7 @@ export default class OperationsOrdersIndexDetailsController extends Controller {
|
|
|
16
16
|
@service universe;
|
|
17
17
|
@service sidebar;
|
|
18
18
|
@tracked routingControl;
|
|
19
|
+
@tracked routingCompleted = false;
|
|
19
20
|
|
|
20
21
|
get tabs() {
|
|
21
22
|
const registeredTabs = this.menuService.getMenuItems('fleet-ops:component:order:details');
|
|
@@ -109,7 +110,19 @@ export default class OperationsOrdersIndexDetailsController extends Controller {
|
|
|
109
110
|
@action async setup() {
|
|
110
111
|
// Change to map layout and display order route
|
|
111
112
|
this.index.changeLayout('map');
|
|
112
|
-
this.
|
|
113
|
+
this.routingCompleted = false;
|
|
114
|
+
this.routingControl = await this.leafletMapManager.addRoutingControl(this.model.routeWaypoints, {
|
|
115
|
+
onRouteFound: () => {
|
|
116
|
+
this.routingCompleted = true;
|
|
117
|
+
},
|
|
118
|
+
onRoutingError: () => {
|
|
119
|
+
this.routingCompleted = true;
|
|
120
|
+
},
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
if (!this.routingControl) {
|
|
124
|
+
this.routingCompleted = true;
|
|
125
|
+
}
|
|
113
126
|
|
|
114
127
|
// Hide sidebar
|
|
115
128
|
this.sidebar.hideNow();
|
|
@@ -125,7 +138,19 @@ export default class OperationsOrdersIndexDetailsController extends Controller {
|
|
|
125
138
|
async (_msg, { reloadable }) => {
|
|
126
139
|
if (reloadable) {
|
|
127
140
|
await this.hostRouter.refresh();
|
|
128
|
-
this.
|
|
141
|
+
this.routingCompleted = false;
|
|
142
|
+
this.routingControl = await this.leafletMapManager.replaceRoutingControl(this.model.routeWaypoints, this.routingControl, {
|
|
143
|
+
onRouteFound: () => {
|
|
144
|
+
this.routingCompleted = true;
|
|
145
|
+
},
|
|
146
|
+
onRoutingError: () => {
|
|
147
|
+
this.routingCompleted = true;
|
|
148
|
+
},
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
if (!this.routingControl) {
|
|
152
|
+
this.routingCompleted = true;
|
|
153
|
+
}
|
|
129
154
|
}
|
|
130
155
|
},
|
|
131
156
|
{ debounceMs: 250 }
|
|
@@ -20,14 +20,14 @@ export default class ApplicationRoute extends Route {
|
|
|
20
20
|
});
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
-
beforeModel() {
|
|
23
|
+
async beforeModel() {
|
|
24
24
|
if (this.abilities.cannot('fleet-ops see extension')) {
|
|
25
25
|
this.notifications.warning(this.intl.t('common.unauthorized-access'));
|
|
26
26
|
return this.hostRouter.transitionTo('console');
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
-
this.location.getUserLocation();
|
|
30
|
-
this.#loadRoutingSettings();
|
|
29
|
+
await this.location.getUserLocation();
|
|
30
|
+
await this.#loadRoutingSettings();
|
|
31
31
|
}
|
|
32
32
|
|
|
33
33
|
async #loadRoutingSettings() {
|
|
@@ -19,5 +19,6 @@ export default class OperationsOrdersIndexDetailsVirtualRoute extends Route {
|
|
|
19
19
|
setupController(controller) {
|
|
20
20
|
super.setupController(...arguments);
|
|
21
21
|
controller.resource = this.modelFor('operations.orders.index.details');
|
|
22
|
+
controller.detailsController = this.controllerFor('operations.orders.index.details');
|
|
22
23
|
}
|
|
23
24
|
}
|
|
@@ -15,9 +15,12 @@ export default class OperationsOrdersIndexDetailsRoute extends Route {
|
|
|
15
15
|
@action willTransition(transition) {
|
|
16
16
|
const fromName = transition.from?.name;
|
|
17
17
|
const toName = transition.to?.name;
|
|
18
|
+
const orderDetailsRoute = 'console.fleet-ops.operations.orders.index.details';
|
|
18
19
|
|
|
19
|
-
// only cleanup when
|
|
20
|
-
|
|
20
|
+
// only cleanup when leaving the order details route tree entirely
|
|
21
|
+
const isLeavingOrderDetails = fromName?.startsWith(orderDetailsRoute) && !toName?.startsWith(orderDetailsRoute);
|
|
22
|
+
|
|
23
|
+
if (isLeavingOrderDetails) {
|
|
21
24
|
const controller = this.controllerFor('operations.orders.index.details');
|
|
22
25
|
const rc = controller.routingControl;
|
|
23
26
|
|
|
@@ -55,7 +58,20 @@ export default class OperationsOrdersIndexDetailsRoute extends Route {
|
|
|
55
58
|
return this.store.queryRecord('order', {
|
|
56
59
|
public_id,
|
|
57
60
|
single: true,
|
|
58
|
-
with: [
|
|
61
|
+
with: [
|
|
62
|
+
'payload',
|
|
63
|
+
'driverAssigned',
|
|
64
|
+
'orderConfig',
|
|
65
|
+
'customer',
|
|
66
|
+
'facilitator',
|
|
67
|
+
'trackingStatuses',
|
|
68
|
+
'trackingNumber',
|
|
69
|
+
'purchaseRate',
|
|
70
|
+
'purchaseRate.serviceQuote',
|
|
71
|
+
'purchaseRate.serviceQuote.items',
|
|
72
|
+
'comments',
|
|
73
|
+
'files',
|
|
74
|
+
],
|
|
59
75
|
});
|
|
60
76
|
}
|
|
61
77
|
|
|
@@ -1,7 +1,9 @@
|
|
|
1
|
-
import ResourceActionService
|
|
1
|
+
import ResourceActionService from '@fleetbase/ember-core/services/resource-action';
|
|
2
2
|
|
|
3
3
|
export default class EquipmentActionsService extends ResourceActionService {
|
|
4
|
-
|
|
4
|
+
get defaultCurrency() {
|
|
5
|
+
return this.currentUser?.company?.currency || this.currentUser.currency || 'USD';
|
|
6
|
+
}
|
|
5
7
|
|
|
6
8
|
constructor() {
|
|
7
9
|
super(...arguments);
|
|
@@ -12,10 +14,6 @@ export default class EquipmentActionsService extends ResourceActionService {
|
|
|
12
14
|
});
|
|
13
15
|
}
|
|
14
16
|
|
|
15
|
-
get defaultCurrency() {
|
|
16
|
-
return this.currentUser?.company?.currency || 'USD';
|
|
17
|
-
}
|
|
18
|
-
|
|
19
17
|
transition = {
|
|
20
18
|
view: (equipment) => this.transitionTo('maintenance.equipment.index.details', equipment),
|
|
21
19
|
edit: (equipment) => this.transitionTo('maintenance.equipment.index.edit', equipment),
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
import Service from '@ember/service';
|
|
2
|
+
import { inject as service } from '@ember/service';
|
|
3
|
+
import { tracked } from '@glimmer/tracking';
|
|
4
|
+
import { action } from '@ember/object';
|
|
5
|
+
import { debug } from '@ember/debug';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* GeofenceEventBus
|
|
9
|
+
*
|
|
10
|
+
* A singleton service that subscribes to the company-level geofence WebSocket
|
|
11
|
+
* channel and maintains a live feed of geofence events (entered, exited, dwelled).
|
|
12
|
+
*
|
|
13
|
+
* Other components (e.g. the live map, the geofence events panel) inject this
|
|
14
|
+
* service and observe `this.events` to react to incoming events.
|
|
15
|
+
*
|
|
16
|
+
* Usage:
|
|
17
|
+
* @service geofenceEventBus;
|
|
18
|
+
*
|
|
19
|
+
* // In a template: {{#each this.geofenceEventBus.events as |event|}} ...
|
|
20
|
+
* // In JS: this.geofenceEventBus.on('geofence.entered', this.handleEntry);
|
|
21
|
+
*/
|
|
22
|
+
export default class GeofenceEventBusService extends Service {
|
|
23
|
+
@service socket;
|
|
24
|
+
@service currentUser;
|
|
25
|
+
@service universe;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Live event feed — most recent events at the front.
|
|
29
|
+
* Capped at MAX_EVENTS to prevent unbounded memory growth.
|
|
30
|
+
*
|
|
31
|
+
* @type {Array}
|
|
32
|
+
*/
|
|
33
|
+
@tracked events = [];
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Whether the service has successfully subscribed to the socket channel.
|
|
37
|
+
*
|
|
38
|
+
* @type {boolean}
|
|
39
|
+
*/
|
|
40
|
+
@tracked isSubscribed = false;
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Maximum number of events to retain in the live feed.
|
|
44
|
+
*/
|
|
45
|
+
MAX_EVENTS = 100;
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* The geofence event types to listen for.
|
|
49
|
+
*/
|
|
50
|
+
GEOFENCE_EVENTS = ['geofence.entered', 'geofence.exited', 'geofence.dwelled'];
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Internal map of event type → array of handler functions.
|
|
54
|
+
* Supports the simple pub/sub API.
|
|
55
|
+
*
|
|
56
|
+
* @type {Map}
|
|
57
|
+
*/
|
|
58
|
+
#handlers = new Map();
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* The active socket channel subscription.
|
|
62
|
+
*/
|
|
63
|
+
#channel = null;
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Subscribe to the company geofence channel.
|
|
67
|
+
* Should be called once after the user session is established.
|
|
68
|
+
*
|
|
69
|
+
* @param {string} companyUuid - The company UUID to subscribe to
|
|
70
|
+
*/
|
|
71
|
+
@action
|
|
72
|
+
async subscribe(companyUuid) {
|
|
73
|
+
if (this.isSubscribed || !companyUuid) {
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
try {
|
|
78
|
+
const socketInstance = this.socket.instance();
|
|
79
|
+
const channelId = `company.${companyUuid}`;
|
|
80
|
+
|
|
81
|
+
this.#channel = socketInstance.subscribe(channelId);
|
|
82
|
+
if (this.#channel.state !== 'subscribed') {
|
|
83
|
+
await this.#channel.subscribe();
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
this.isSubscribed = true;
|
|
87
|
+
debug(`[GeofenceEventBus] Subscribed to channel: ${channelId}`);
|
|
88
|
+
|
|
89
|
+
// Start consuming events from the channel
|
|
90
|
+
(async () => {
|
|
91
|
+
for await (const output of this.#channel) {
|
|
92
|
+
const { event, data } = output;
|
|
93
|
+
|
|
94
|
+
if (this.GEOFENCE_EVENTS.includes(event)) {
|
|
95
|
+
this.#handleIncomingEvent(event, data);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
})();
|
|
99
|
+
} catch (error) {
|
|
100
|
+
debug(`[GeofenceEventBus] Failed to subscribe: ${error.message}`);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Unsubscribe from the geofence channel and clear state.
|
|
106
|
+
* Called on user logout or session end.
|
|
107
|
+
*/
|
|
108
|
+
@action
|
|
109
|
+
unsubscribe() {
|
|
110
|
+
if (this.#channel) {
|
|
111
|
+
this.#channel.close();
|
|
112
|
+
this.#channel = null;
|
|
113
|
+
}
|
|
114
|
+
this.isSubscribed = false;
|
|
115
|
+
this.events = [];
|
|
116
|
+
this.#handlers.clear();
|
|
117
|
+
debug('[GeofenceEventBus] Unsubscribed.');
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Register a handler function for a specific geofence event type.
|
|
122
|
+
*
|
|
123
|
+
* @param {string} eventType - 'geofence.entered' | 'geofence.exited' | 'geofence.dwelled'
|
|
124
|
+
* @param {Function} handler - Called with the normalised event object
|
|
125
|
+
*/
|
|
126
|
+
on(eventType, handler) {
|
|
127
|
+
if (!this.#handlers.has(eventType)) {
|
|
128
|
+
this.#handlers.set(eventType, []);
|
|
129
|
+
}
|
|
130
|
+
this.#handlers.get(eventType).push(handler);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Unregister a previously registered handler.
|
|
135
|
+
*
|
|
136
|
+
* @param {string} eventType
|
|
137
|
+
* @param {Function} handler
|
|
138
|
+
*/
|
|
139
|
+
off(eventType, handler) {
|
|
140
|
+
if (!this.#handlers.has(eventType)) {
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
const handlers = this.#handlers.get(eventType).filter((h) => h !== handler);
|
|
144
|
+
this.#handlers.set(eventType, handlers);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Clear the live event feed.
|
|
149
|
+
*/
|
|
150
|
+
@action
|
|
151
|
+
clearFeed() {
|
|
152
|
+
this.events = [];
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
@action
|
|
156
|
+
seedEvents(events = []) {
|
|
157
|
+
this.events = Array.isArray(events) ? events.slice(0, this.MAX_EVENTS).map((event) => ({ ...event, isNew: false })) : [];
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Process an incoming geofence event from the WebSocket channel.
|
|
162
|
+
*
|
|
163
|
+
* @param {string} eventType
|
|
164
|
+
* @param {Object} data
|
|
165
|
+
*/
|
|
166
|
+
#handleIncomingEvent(eventType, data) {
|
|
167
|
+
const normalised = this.normalizeEvent(eventType, data);
|
|
168
|
+
|
|
169
|
+
// Prepend and cap
|
|
170
|
+
this.events = [normalised, ...this.events].slice(0, this.MAX_EVENTS);
|
|
171
|
+
|
|
172
|
+
setTimeout(() => {
|
|
173
|
+
this.events = this.events.map((event) => (event.id === normalised.id ? { ...event, isNew: false } : event));
|
|
174
|
+
}, 3000);
|
|
175
|
+
|
|
176
|
+
debug(`[GeofenceEventBus] ${eventType} — driver: ${normalised.driverName}, geofence: ${normalised.geofenceName}`);
|
|
177
|
+
|
|
178
|
+
// Notify registered handlers
|
|
179
|
+
const handlers = this.#handlers.get(eventType) ?? [];
|
|
180
|
+
handlers.forEach((handler) => {
|
|
181
|
+
try {
|
|
182
|
+
handler(normalised);
|
|
183
|
+
} catch (e) {
|
|
184
|
+
debug(`[GeofenceEventBus] Handler error for ${eventType}: ${e.message}`);
|
|
185
|
+
}
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
// Also emit on the universe bus so any component can react
|
|
189
|
+
this.universe.trigger(`fleet-ops.geofence.${eventType.replace('geofence.', '')}`, normalised);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Normalise a raw WebSocket payload into a consistent display object.
|
|
194
|
+
*
|
|
195
|
+
* @param {string} eventType
|
|
196
|
+
* @param {Object} raw
|
|
197
|
+
* @returns {Object}
|
|
198
|
+
*/
|
|
199
|
+
normalizeEvent(eventType, raw) {
|
|
200
|
+
const subject = raw.subject ?? null;
|
|
201
|
+
const subjectType = subject?.type ?? (raw.vehicle && !raw.driver ? 'vehicle' : 'driver');
|
|
202
|
+
const subjectName = subject?.name ?? raw.driver?.name ?? raw.vehicle?.name ?? raw.vehicle?.plate ?? 'Unknown Subject';
|
|
203
|
+
return {
|
|
204
|
+
id: raw.id ?? raw.uuid ?? `${Date.now()}-${Math.random()}`,
|
|
205
|
+
eventType,
|
|
206
|
+
occurredAt: raw.occurred_at ?? new Date().toISOString(),
|
|
207
|
+
subjectType,
|
|
208
|
+
subjectName,
|
|
209
|
+
driverName: raw.driver?.name ?? null,
|
|
210
|
+
driverUuid: raw.driver?.uuid ?? null,
|
|
211
|
+
vehicleName: raw.vehicle?.name ?? null,
|
|
212
|
+
vehiclePlate: raw.vehicle?.plate ?? null,
|
|
213
|
+
geofenceName: raw.geofence?.name ?? 'Unknown Geofence',
|
|
214
|
+
geofenceUuid: raw.geofence?.uuid ?? null,
|
|
215
|
+
geofenceType: raw.geofence?.type ?? 'zone',
|
|
216
|
+
orderPublicId: raw.order?.id ?? null,
|
|
217
|
+
latitude: raw.location?.latitude ?? null,
|
|
218
|
+
longitude: raw.location?.longitude ?? null,
|
|
219
|
+
dwellMinutes: raw.dwell_duration_minutes ?? null,
|
|
220
|
+
isNew: true,
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
}
|
|
@@ -270,6 +270,10 @@ export default class LeafletMapManagerService extends Service {
|
|
|
270
270
|
this.route = routes[0];
|
|
271
271
|
});
|
|
272
272
|
|
|
273
|
+
routingControl.on('routingerror', (event) => {
|
|
274
|
+
options?.onRoutingError?.(event);
|
|
275
|
+
});
|
|
276
|
+
|
|
273
277
|
this.positionWaypoints(waypoints);
|
|
274
278
|
|
|
275
279
|
return routingControl;
|
|
@@ -1,7 +1,9 @@
|
|
|
1
|
-
import ResourceActionService
|
|
1
|
+
import ResourceActionService from '@fleetbase/ember-core/services/resource-action';
|
|
2
2
|
|
|
3
3
|
export default class MaintenanceActionsService extends ResourceActionService {
|
|
4
|
-
|
|
4
|
+
get defaultCurrency() {
|
|
5
|
+
return this.currentUser?.company?.currency || this.currentUser.currency || 'USD';
|
|
6
|
+
}
|
|
5
7
|
|
|
6
8
|
constructor() {
|
|
7
9
|
super(...arguments);
|
|
@@ -12,10 +14,6 @@ export default class MaintenanceActionsService extends ResourceActionService {
|
|
|
12
14
|
});
|
|
13
15
|
}
|
|
14
16
|
|
|
15
|
-
get defaultCurrency() {
|
|
16
|
-
return this.currentUser?.company?.currency || 'USD';
|
|
17
|
-
}
|
|
18
|
-
|
|
19
17
|
transition = {
|
|
20
18
|
view: (maintenance) => this.transitionTo('maintenance.maintenances.index.details', maintenance),
|
|
21
19
|
edit: (maintenance) => this.transitionTo('maintenance.maintenances.index.edit', maintenance),
|
|
@@ -1,7 +1,9 @@
|
|
|
1
|
-
import ResourceActionService
|
|
1
|
+
import ResourceActionService from '@fleetbase/ember-core/services/resource-action';
|
|
2
2
|
|
|
3
3
|
export default class PartActionsService extends ResourceActionService {
|
|
4
|
-
|
|
4
|
+
get defaultCurrency() {
|
|
5
|
+
return this.currentUser?.company?.currency || this.currentUser.currency || 'USD';
|
|
6
|
+
}
|
|
5
7
|
|
|
6
8
|
constructor() {
|
|
7
9
|
super(...arguments);
|
|
@@ -12,10 +14,6 @@ export default class PartActionsService extends ResourceActionService {
|
|
|
12
14
|
});
|
|
13
15
|
}
|
|
14
16
|
|
|
15
|
-
get defaultCurrency() {
|
|
16
|
-
return this.currentUser?.company?.currency || 'USD';
|
|
17
|
-
}
|
|
18
|
-
|
|
19
17
|
transition = {
|
|
20
18
|
view: (part) => this.transitionTo('maintenance.parts.index.details', part),
|
|
21
19
|
edit: (part) => this.transitionTo('maintenance.parts.index.edit', part),
|
|
@@ -6,13 +6,17 @@ import serializePayload from '../utils/serialize-payload';
|
|
|
6
6
|
export default class ServiceRateActionsService extends ResourceActionService {
|
|
7
7
|
modelNamePath = 'service_name';
|
|
8
8
|
|
|
9
|
+
get defaultCurrency() {
|
|
10
|
+
return this.currentUser?.company?.currency || this.currentUser.currency || 'USD';
|
|
11
|
+
}
|
|
12
|
+
|
|
9
13
|
constructor() {
|
|
10
14
|
super(...arguments);
|
|
11
15
|
this.initialize('service-rate', {
|
|
12
16
|
defaultAttributes: {
|
|
13
17
|
rate_calculation_method: 'per_meter',
|
|
14
18
|
per_meter_unit: 'm',
|
|
15
|
-
currency: this.
|
|
19
|
+
currency: this.defaultCurrency,
|
|
16
20
|
parcel_fees: this.#getDefaultParcelFees(),
|
|
17
21
|
},
|
|
18
22
|
});
|
|
@@ -118,12 +122,13 @@ export default class ServiceRateActionsService extends ResourceActionService {
|
|
|
118
122
|
|
|
119
123
|
const hasFacilitator = !isNone(order.facilitator);
|
|
120
124
|
const facilitatorServiceType = order.facilitator?.get('service_types.firstObject.key') ?? order.type;
|
|
125
|
+
const routeSummary = order.route?.summary ?? order.route?.details?.summary ?? {};
|
|
121
126
|
|
|
122
127
|
try {
|
|
123
128
|
const serviceQuotes = yield this.fetch.post('service-quotes/preliminary', {
|
|
124
129
|
payload: serializePayload(order.payload),
|
|
125
|
-
distance:
|
|
126
|
-
time:
|
|
130
|
+
distance: routeSummary.totalDistance,
|
|
131
|
+
time: routeSummary.totalTime,
|
|
127
132
|
service_type: hasFacilitator ? facilitatorServiceType : order.type,
|
|
128
133
|
facilitator: order.facilitator?.public_id,
|
|
129
134
|
scheduled_at: order.scheduled_at,
|
|
@@ -1 +1,7 @@
|
|
|
1
|
-
{{
|
|
1
|
+
{{#if this.parent.routingCompleted}}
|
|
2
|
+
{{component (lazy-engine-component @model.component) resource=this.resource order=this.resource params=@model.componentParams}}
|
|
3
|
+
{{else}}
|
|
4
|
+
<div class="flex items-center justify-center py-12">
|
|
5
|
+
<Spinner />
|
|
6
|
+
</div>
|
|
7
|
+
{{/if}}
|