@fleetbase/fleetops-engine 0.6.43 → 0.6.44
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/map/drawer/geofence-event-listing.hbs +123 -0
- package/addon/components/map/drawer/geofence-event-listing.js +100 -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/service-area/form.hbs +59 -0
- package/addon/components/zone/form.hbs +60 -1
- package/addon/services/geofence-event-bus.js +223 -0
- package/app/components/map/drawer/geofence-event-listing.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 +8 -3
- package/server/src/Http/Controllers/Api/v1/ServiceAreaController.php +2 -2
- 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/CreateZoneRequest.php +8 -4
- 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/GeofenceEventLog.php +155 -0
- package/server/src/Models/ServiceArea.php +21 -2
- package/server/src/Models/ServiceQuote.php +3 -1
- package/server/src/Models/Zone.php +20 -2
- package/server/src/Notifications/DriverArrivedAtGeofence.php +86 -0
- package/server/src/Providers/EventServiceProvider.php +19 -0
- package/server/src/Providers/FleetOpsServiceProvider.php +10 -0
- package/server/src/Support/GeofenceIntersectionService.php +206 -0
- package/server/src/routes.php +16 -0
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
<div class="geofence-events-panel flex flex-col h-full" ...attributes>
|
|
2
|
+
<div class="flex items-center justify-between px-4 py-2.5 border-b border-gray-200 dark:border-gray-700">
|
|
3
|
+
<div class="flex items-center space-x-2">
|
|
4
|
+
<FaIcon @icon="map-pin" class="text-blue-500 w-4 h-4" />
|
|
5
|
+
<h3 class="text-sm font-semibold text-gray-900 dark:text-white">Geofence Events</h3>
|
|
6
|
+
{{#if this.events.length}}
|
|
7
|
+
<span class="inline-flex items-center px-2 py-0.5 rounded-full text-[11px] font-medium bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-300">
|
|
8
|
+
{{this.events.length}}
|
|
9
|
+
</span>
|
|
10
|
+
{{/if}}
|
|
11
|
+
</div>
|
|
12
|
+
<button
|
|
13
|
+
type="button"
|
|
14
|
+
class="text-[11px] text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 transition-colors"
|
|
15
|
+
{{on "click" this.clearFeed}}
|
|
16
|
+
>
|
|
17
|
+
Clear
|
|
18
|
+
</button>
|
|
19
|
+
</div>
|
|
20
|
+
|
|
21
|
+
{{#if this.isLoading}}
|
|
22
|
+
<div class="flex items-center justify-center py-8">
|
|
23
|
+
<Spinner class="w-5 h-5 text-blue-500" />
|
|
24
|
+
<span class="ml-2 text-sm text-gray-500">Loading events…</span>
|
|
25
|
+
</div>
|
|
26
|
+
{{else if (eq this.events.length 0)}}
|
|
27
|
+
<div class="flex flex-col items-center justify-center py-10 px-4 text-center">
|
|
28
|
+
<FaIcon @icon="map-pin" class="w-8 h-8 text-gray-300 dark:text-gray-600 mb-3" />
|
|
29
|
+
<p class="text-sm font-medium text-gray-500 dark:text-gray-400">No geofence events yet</p>
|
|
30
|
+
<p class="text-xs text-gray-400 dark:text-gray-500 mt-1">
|
|
31
|
+
Events will appear here in real time as drivers and vehicles cross zone and service area boundaries.
|
|
32
|
+
</p>
|
|
33
|
+
</div>
|
|
34
|
+
{{else}}
|
|
35
|
+
<div class="flex-1 overflow-y-auto divide-y divide-gray-100 dark:divide-gray-800">
|
|
36
|
+
{{#each this.events as |event|}}
|
|
37
|
+
<div class="px-4 py-2.5 hover:bg-gray-50 dark:hover:bg-gray-800 transition-colors {{if event.isNew 'bg-blue-50 dark:bg-blue-900/10'}}">
|
|
38
|
+
<div class="flex items-start gap-2.5">
|
|
39
|
+
<div class="flex h-7 w-7 flex-shrink-0 items-center justify-center rounded-md bg-gray-100 text-gray-500 dark:bg-gray-800 dark:text-gray-300">
|
|
40
|
+
<FaIcon @icon={{this.iconFor event.eventType}} @size="xs" />
|
|
41
|
+
</div>
|
|
42
|
+
|
|
43
|
+
<div class="min-w-0 flex-1">
|
|
44
|
+
<div class="flex items-start justify-between gap-3">
|
|
45
|
+
<div class="min-w-0 flex-1">
|
|
46
|
+
<div class="flex items-center gap-2 text-[11px] leading-4">
|
|
47
|
+
<span class="inline-flex items-center rounded px-1.5 py-0.5 font-medium {{this.badgeColorFor event.eventType}}">
|
|
48
|
+
{{this.labelFor event.eventType}}
|
|
49
|
+
</span>
|
|
50
|
+
<span class="inline-flex items-center gap-1 text-gray-400 dark:text-gray-500">
|
|
51
|
+
<FaIcon @icon={{this.subjectIconFor event.subjectType}} @size="xs" />
|
|
52
|
+
<span class="capitalize">{{event.subjectType}}</span>
|
|
53
|
+
</span>
|
|
54
|
+
<span class="text-gray-300 dark:text-gray-600">•</span>
|
|
55
|
+
<span class="text-gray-500 dark:text-gray-400">{{this.geofenceTypeLabel event.geofenceType}}</span>
|
|
56
|
+
</div>
|
|
57
|
+
|
|
58
|
+
<div class="mt-1 flex items-center gap-1.5 text-sm leading-5">
|
|
59
|
+
<span class="truncate font-medium text-gray-900 dark:text-white">{{event.subjectName}}</span>
|
|
60
|
+
<FaIcon @icon="arrow-right" @size="xs" class="text-gray-300 dark:text-gray-600" />
|
|
61
|
+
<span class="truncate text-gray-600 dark:text-gray-300">{{event.geofenceName}}</span>
|
|
62
|
+
</div>
|
|
63
|
+
|
|
64
|
+
<div class="mt-1 flex flex-wrap items-center gap-x-3 gap-y-1 text-[11px] leading-4 text-gray-500 dark:text-gray-400">
|
|
65
|
+
{{#if event.driverName}}
|
|
66
|
+
{{#unless (eq event.subjectType "driver")}}
|
|
67
|
+
<span class="inline-flex items-center gap-1">
|
|
68
|
+
<FaIcon @icon="id-card" @size="xs" class="text-gray-400" />
|
|
69
|
+
{{event.driverName}}
|
|
70
|
+
</span>
|
|
71
|
+
{{/unless}}
|
|
72
|
+
{{/if}}
|
|
73
|
+
|
|
74
|
+
{{#if event.vehiclePlate}}
|
|
75
|
+
<span class="inline-flex items-center gap-1">
|
|
76
|
+
<FaIcon @icon="car" @size="xs" class="text-gray-400" />
|
|
77
|
+
{{event.vehiclePlate}}
|
|
78
|
+
</span>
|
|
79
|
+
{{/if}}
|
|
80
|
+
|
|
81
|
+
{{#if event.orderPublicId}}
|
|
82
|
+
<span class="inline-flex items-center gap-1">
|
|
83
|
+
<FaIcon @icon="hashtag" @size="xs" class="text-gray-400" />
|
|
84
|
+
{{event.orderPublicId}}
|
|
85
|
+
</span>
|
|
86
|
+
{{/if}}
|
|
87
|
+
|
|
88
|
+
{{#if event.dwellMinutes}}
|
|
89
|
+
<span class="inline-flex items-center gap-1">
|
|
90
|
+
<FaIcon @icon="clock" @size="xs" class="text-gray-400" />
|
|
91
|
+
{{event.dwellMinutes}} min
|
|
92
|
+
</span>
|
|
93
|
+
{{/if}}
|
|
94
|
+
|
|
95
|
+
{{#if event.latitude}}
|
|
96
|
+
{{#if event.longitude}}
|
|
97
|
+
<span class="inline-flex items-center gap-1">
|
|
98
|
+
<FaIcon @icon="location-dot" @size="xs" class="text-gray-400" />
|
|
99
|
+
{{event.latitude}}, {{event.longitude}}
|
|
100
|
+
</span>
|
|
101
|
+
{{/if}}
|
|
102
|
+
{{/if}}
|
|
103
|
+
</div>
|
|
104
|
+
</div>
|
|
105
|
+
|
|
106
|
+
<div class="flex-shrink-0 text-right">
|
|
107
|
+
<div class="text-[11px] leading-4 text-gray-500 dark:text-gray-400">
|
|
108
|
+
{{format-date-fns event.occurredAt "dd MMM, HH:mm"}}
|
|
109
|
+
</div>
|
|
110
|
+
{{#if event.isNew}}
|
|
111
|
+
<div class="mt-1 text-[10px] font-medium uppercase tracking-wide text-blue-500 dark:text-blue-300">
|
|
112
|
+
Live
|
|
113
|
+
</div>
|
|
114
|
+
{{/if}}
|
|
115
|
+
</div>
|
|
116
|
+
</div>
|
|
117
|
+
</div>
|
|
118
|
+
</div>
|
|
119
|
+
</div>
|
|
120
|
+
{{/each}}
|
|
121
|
+
</div>
|
|
122
|
+
{{/if}}
|
|
123
|
+
</div>
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import Component from '@glimmer/component';
|
|
2
|
+
import { tracked } from '@glimmer/tracking';
|
|
3
|
+
import { inject as service } from '@ember/service';
|
|
4
|
+
import { action } from '@ember/object';
|
|
5
|
+
|
|
6
|
+
export default class MapDrawerGeofenceEventListingComponent extends Component {
|
|
7
|
+
@service fetch;
|
|
8
|
+
@service geofenceEventBus;
|
|
9
|
+
@service currentUser;
|
|
10
|
+
|
|
11
|
+
@tracked isLoading = false;
|
|
12
|
+
|
|
13
|
+
constructor() {
|
|
14
|
+
super(...arguments);
|
|
15
|
+
this.geofenceEventBus.subscribe(this.currentUser.companyId);
|
|
16
|
+
this.loadRecentEvents();
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
get events() {
|
|
20
|
+
return this.geofenceEventBus.events;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
@action
|
|
24
|
+
async loadRecentEvents() {
|
|
25
|
+
this.isLoading = true;
|
|
26
|
+
|
|
27
|
+
try {
|
|
28
|
+
const response = await this.fetch.get('geofences/events', { per_page: 20 });
|
|
29
|
+
|
|
30
|
+
if (response?.data) {
|
|
31
|
+
this.geofenceEventBus.seedEvents(response.data.map((event) => this.geofenceEventBus.normalizeEvent(event.event_type, event)));
|
|
32
|
+
}
|
|
33
|
+
} catch (error) {
|
|
34
|
+
// The live socket feed will still populate events if history loading fails.
|
|
35
|
+
} finally {
|
|
36
|
+
this.isLoading = false;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
@action
|
|
41
|
+
badgeColorFor(eventType) {
|
|
42
|
+
const colors = {
|
|
43
|
+
'geofence.entered': 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-300',
|
|
44
|
+
'geofence.exited': 'bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-300',
|
|
45
|
+
'geofence.dwelled': 'bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-300',
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
return colors[eventType] ?? 'bg-gray-100 text-gray-800';
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
@action
|
|
52
|
+
labelFor(eventType) {
|
|
53
|
+
const labels = {
|
|
54
|
+
'geofence.entered': 'Entered',
|
|
55
|
+
'geofence.exited': 'Exited',
|
|
56
|
+
'geofence.dwelled': 'Dwelled',
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
return labels[eventType] ?? eventType;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
@action
|
|
63
|
+
iconFor(eventType) {
|
|
64
|
+
const icons = {
|
|
65
|
+
'geofence.entered': 'arrow-right-to-bracket',
|
|
66
|
+
'geofence.exited': 'arrow-right-from-bracket',
|
|
67
|
+
'geofence.dwelled': 'clock',
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
return icons[eventType] ?? 'map-pin';
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
@action
|
|
74
|
+
subjectIconFor(subjectType) {
|
|
75
|
+
return subjectType === 'vehicle' ? 'car' : 'id-card';
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
@action
|
|
79
|
+
geofenceTypeLabel(type) {
|
|
80
|
+
if (type === 'service_area') {
|
|
81
|
+
return 'Service Area';
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (type === 'zone') {
|
|
85
|
+
return 'Zone';
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return type ?? 'Geofence';
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
@action
|
|
92
|
+
hasSecondaryMeta(event) {
|
|
93
|
+
return Boolean(event.vehiclePlate || event.orderPublicId || event.dwellMinutes || event.latitude || event.longitude);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
@action
|
|
97
|
+
clearFeed() {
|
|
98
|
+
this.geofenceEventBus.clearFeed();
|
|
99
|
+
}
|
|
100
|
+
}
|
|
@@ -15,6 +15,7 @@ export default class MapDrawerComponent extends Component {
|
|
|
15
15
|
this.universe._createMenuItem('Drivers', null, { icon: 'id-card', component: 'map/drawer/driver-listing' }),
|
|
16
16
|
this.universe._createMenuItem('Places', null, { icon: 'building', component: 'map/drawer/place-listing' }),
|
|
17
17
|
this.universe._createMenuItem('Positions', null, { icon: 'map-marker', component: 'map/drawer/position-listing' }),
|
|
18
|
+
this.universe._createMenuItem('Geofences', null, { icon: 'map-pin', component: 'map/drawer/geofence-event-listing' }),
|
|
18
19
|
this.universe._createMenuItem('Events', null, { icon: 'stream', component: 'map/drawer/device-event-listing' }),
|
|
19
20
|
...(isArray(registeredTabs) ? registeredTabs : []),
|
|
20
21
|
];
|
|
@@ -29,6 +29,8 @@ export default class MapLeafletLiveMapComponent extends Component {
|
|
|
29
29
|
@service intl;
|
|
30
30
|
@service universe;
|
|
31
31
|
@service('universe/menu-service') menuService;
|
|
32
|
+
@service geofenceEventBus;
|
|
33
|
+
@service currentUser;
|
|
32
34
|
|
|
33
35
|
/** properties */
|
|
34
36
|
id = guidFor(this);
|
|
@@ -57,6 +59,19 @@ export default class MapLeafletLiveMapComponent extends Component {
|
|
|
57
59
|
|
|
58
60
|
// Ensure we have valid coordinates on initialization
|
|
59
61
|
this.#updateCoordinatesFromLocation();
|
|
62
|
+
|
|
63
|
+
if (this.currentUser.companyId) {
|
|
64
|
+
this.geofenceEventBus.subscribe(this.currentUser.companyId);
|
|
65
|
+
} else {
|
|
66
|
+
this._currentUserLoadedHandler = () => this.geofenceEventBus.subscribe(this.currentUser.companyId);
|
|
67
|
+
this.currentUser.on('user.loaded', this._currentUserLoadedHandler);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Subscribe to geofence events so the live map can react to boundary crossings
|
|
71
|
+
this._geofenceEnteredHandler = this.#handleGeofenceEntered.bind(this);
|
|
72
|
+
this._geofenceExitedHandler = this.#handleGeofenceExited.bind(this);
|
|
73
|
+
this.universe.on('fleet-ops.geofence.entered', this._geofenceEnteredHandler);
|
|
74
|
+
this.universe.on('fleet-ops.geofence.exited', this._geofenceExitedHandler);
|
|
60
75
|
}
|
|
61
76
|
|
|
62
77
|
willDestroy() {
|
|
@@ -67,6 +82,18 @@ export default class MapLeafletLiveMapComponent extends Component {
|
|
|
67
82
|
this.universe.off('user.located', this._locationUpdateHandler);
|
|
68
83
|
this._locationUpdateHandler = null;
|
|
69
84
|
}
|
|
85
|
+
if (this._currentUserLoadedHandler) {
|
|
86
|
+
this.currentUser.off('user.loaded', this._currentUserLoadedHandler);
|
|
87
|
+
this._currentUserLoadedHandler = null;
|
|
88
|
+
}
|
|
89
|
+
if (this._geofenceEnteredHandler) {
|
|
90
|
+
this.universe.off('fleet-ops.geofence.entered', this._geofenceEnteredHandler);
|
|
91
|
+
this._geofenceEnteredHandler = null;
|
|
92
|
+
}
|
|
93
|
+
if (this._geofenceExitedHandler) {
|
|
94
|
+
this.universe.off('fleet-ops.geofence.exited', this._geofenceExitedHandler);
|
|
95
|
+
this._geofenceExitedHandler = null;
|
|
96
|
+
}
|
|
70
97
|
}
|
|
71
98
|
|
|
72
99
|
@action didLoad({ target: map }) {
|
|
@@ -259,6 +286,62 @@ export default class MapLeafletLiveMapComponent extends Component {
|
|
|
259
286
|
return 14;
|
|
260
287
|
}
|
|
261
288
|
|
|
289
|
+
/**
|
|
290
|
+
* Handles a geofence.entered event from the GeofenceEventBus.
|
|
291
|
+
* Briefly highlights the geofence layer on the map to provide visual feedback.
|
|
292
|
+
*
|
|
293
|
+
* @param {Object} event - Normalised geofence event object
|
|
294
|
+
*/
|
|
295
|
+
#handleGeofenceEntered(event) {
|
|
296
|
+
debug(`[LiveMap] geofence.entered — driver: ${event.driverName}, geofence: ${event.geofenceName}`);
|
|
297
|
+
this.#flashGeofenceLayer(event.geofenceUuid, '#22c55e'); // green
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* Handles a geofence.exited event from the GeofenceEventBus.
|
|
302
|
+
* Briefly highlights the geofence layer on the map to provide visual feedback.
|
|
303
|
+
*
|
|
304
|
+
* @param {Object} event - Normalised geofence event object
|
|
305
|
+
*/
|
|
306
|
+
#handleGeofenceExited(event) {
|
|
307
|
+
debug(`[LiveMap] geofence.exited — driver: ${event.driverName}, geofence: ${event.geofenceName}`);
|
|
308
|
+
this.#flashGeofenceLayer(event.geofenceUuid, '#ef4444'); // red
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* Briefly changes the fill colour of a geofence polygon layer on the map
|
|
313
|
+
* to provide visual feedback when a driver enters or exits.
|
|
314
|
+
*
|
|
315
|
+
* @param {string} geofenceUuid - UUID of the zone or service area
|
|
316
|
+
* @param {string} flashColor - Hex colour to flash
|
|
317
|
+
*/
|
|
318
|
+
#flashGeofenceLayer(geofenceUuid, flashColor) {
|
|
319
|
+
if (!geofenceUuid || !this.map) {
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// Iterate over all Leaflet layers to find the matching geofence polygon
|
|
324
|
+
this.map.eachLayer((layer) => {
|
|
325
|
+
const model = layer._model;
|
|
326
|
+
if (model && model.uuid === geofenceUuid && typeof layer.setStyle === 'function') {
|
|
327
|
+
const originalStyle = {
|
|
328
|
+
color: layer.options.color,
|
|
329
|
+
fillColor: layer.options.fillColor,
|
|
330
|
+
weight: layer.options.weight,
|
|
331
|
+
};
|
|
332
|
+
|
|
333
|
+
// Flash to the event colour
|
|
334
|
+
layer.setStyle({ color: flashColor, fillColor: flashColor, weight: 3 });
|
|
335
|
+
|
|
336
|
+
// Restore original style after 2 seconds
|
|
337
|
+
setTimeout(() => {
|
|
338
|
+
if (!layer._map) return; // layer may have been removed
|
|
339
|
+
layer.setStyle(originalStyle);
|
|
340
|
+
}, 2000);
|
|
341
|
+
}
|
|
342
|
+
});
|
|
343
|
+
}
|
|
344
|
+
|
|
262
345
|
/**
|
|
263
346
|
* Handles location updates from the location service
|
|
264
347
|
* @param {Object} coordinates - The new coordinates
|
|
@@ -223,76 +223,6 @@ const ALIASES = {
|
|
|
223
223
|
entity_destination: ['entity destination', 'item destination', 'deliver to', 'waypoint'],
|
|
224
224
|
};
|
|
225
225
|
|
|
226
|
-
/** Sample row for the downloadable template. */
|
|
227
|
-
const SAMPLE_ROW = {
|
|
228
|
-
order_type: 'pickup_dropoff',
|
|
229
|
-
order_ref: 'ORD-001',
|
|
230
|
-
internal_id: 'INT-001',
|
|
231
|
-
type: 'default',
|
|
232
|
-
status: 'created',
|
|
233
|
-
scheduled_at: '2026-05-01 09:00',
|
|
234
|
-
notes: 'Leave at front door',
|
|
235
|
-
priority: '50',
|
|
236
|
-
time_window_start: '08:00',
|
|
237
|
-
time_window_end: '12:00',
|
|
238
|
-
service_time_min: '10',
|
|
239
|
-
required_skills: '',
|
|
240
|
-
pickup_name: 'Warehouse A',
|
|
241
|
-
pickup_street1: '123 Warehouse Rd',
|
|
242
|
-
pickup_street2: '',
|
|
243
|
-
pickup_city: 'London',
|
|
244
|
-
pickup_state: 'England',
|
|
245
|
-
pickup_postal_code: 'E1 6RF',
|
|
246
|
-
pickup_country: 'GB',
|
|
247
|
-
pickup_phone: '+44 20 7946 0000',
|
|
248
|
-
pickup_lat: '',
|
|
249
|
-
pickup_lng: '',
|
|
250
|
-
dropoff_name: 'John Smith',
|
|
251
|
-
dropoff_street1: '456 High Street',
|
|
252
|
-
dropoff_street2: 'Apt 3B',
|
|
253
|
-
dropoff_city: 'London',
|
|
254
|
-
dropoff_state: 'England',
|
|
255
|
-
dropoff_postal_code: 'SW1A 1AA',
|
|
256
|
-
dropoff_country: 'GB',
|
|
257
|
-
dropoff_phone: '+44 7700 900000',
|
|
258
|
-
dropoff_lat: '',
|
|
259
|
-
dropoff_lng: '',
|
|
260
|
-
weight_kg: '5.2',
|
|
261
|
-
volume_m3: '0.04',
|
|
262
|
-
parcels: '1',
|
|
263
|
-
cod_amount: '',
|
|
264
|
-
cod_currency: '',
|
|
265
|
-
customer_name: 'John Smith',
|
|
266
|
-
customer_email: 'john.smith@example.com',
|
|
267
|
-
customer_phone: '+44 7700 900000',
|
|
268
|
-
customer_type: 'contact',
|
|
269
|
-
facilitator_name: '',
|
|
270
|
-
facilitator_email: '',
|
|
271
|
-
facilitator_phone: '',
|
|
272
|
-
facilitator_type: 'vendor',
|
|
273
|
-
vehicle_plate: '',
|
|
274
|
-
driver_name: '',
|
|
275
|
-
driver_phone: '',
|
|
276
|
-
driver_email: '',
|
|
277
|
-
entity_name: 'Widget A',
|
|
278
|
-
entity_type: 'parcel',
|
|
279
|
-
entity_description: '1x Widget A in box',
|
|
280
|
-
entity_sku: 'WGT-001',
|
|
281
|
-
entity_barcode: '',
|
|
282
|
-
entity_internal_id: '',
|
|
283
|
-
entity_declared_value: '25.00',
|
|
284
|
-
entity_currency: 'USD',
|
|
285
|
-
entity_price: '25.00',
|
|
286
|
-
entity_sale_price: '',
|
|
287
|
-
entity_weight: '0.5',
|
|
288
|
-
entity_weight_unit: 'kg',
|
|
289
|
-
entity_length: '',
|
|
290
|
-
entity_width: '',
|
|
291
|
-
entity_height: '',
|
|
292
|
-
entity_dimensions_unit: 'cm',
|
|
293
|
-
entity_destination: 'dropoff',
|
|
294
|
-
};
|
|
295
|
-
|
|
296
226
|
export default class OrchestratorImportComponent extends Component {
|
|
297
227
|
@service fetch;
|
|
298
228
|
@service notifications;
|
|
@@ -795,13 +725,7 @@ export default class OrchestratorImportComponent extends Component {
|
|
|
795
725
|
return this.args.options.onImportTemplate();
|
|
796
726
|
}
|
|
797
727
|
|
|
798
|
-
//
|
|
799
|
-
|
|
800
|
-
const headers = TARGET_FIELDS.map((f) => f.key);
|
|
801
|
-
const sampleValues = headers.map((k) => SAMPLE_ROW[k] ?? '');
|
|
802
|
-
const ws = XLSX.utils.aoa_to_sheet([headers, sampleValues]);
|
|
803
|
-
const wb = XLSX.utils.book_new();
|
|
804
|
-
XLSX.utils.book_append_sheet(wb, ws, 'Orders');
|
|
805
|
-
XLSX.writeFile(wb, 'Fleetbase_Order_Import_Template.xlsx');
|
|
728
|
+
// Open the import template URL for download
|
|
729
|
+
window.open('https://flb-assets.s3.ap-southeast-1.amazonaws.com/import-templates/Fleetbase_Order_Import_Template.xlsx');
|
|
806
730
|
}
|
|
807
731
|
}
|
|
@@ -41,4 +41,63 @@
|
|
|
41
41
|
</InputGroup>
|
|
42
42
|
</div>
|
|
43
43
|
</ContentPanel>
|
|
44
|
+
|
|
45
|
+
<ContentPanel @title="Geofence Triggers" @open={{true}} @wrapperClass="bordered-top">
|
|
46
|
+
<div class="space-y-4">
|
|
47
|
+
<p class="text-sm text-gray-500 dark:text-gray-400">
|
|
48
|
+
Configure when this service area fires active geofence events. Entry and exit events are dispatched to
|
|
49
|
+
webhooks, WebSocket channels, and the notification system whenever a driver crosses the service area boundary.
|
|
50
|
+
</p>
|
|
51
|
+
|
|
52
|
+
<div class="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
|
53
|
+
{{! Entry trigger toggle }}
|
|
54
|
+
<div class="flex items-start space-x-3 p-3 rounded-lg border border-gray-200 dark:border-gray-700">
|
|
55
|
+
<div class="flex-shrink-0 mt-0.5">
|
|
56
|
+
<Toggle
|
|
57
|
+
@isToggled={{@resource.trigger_on_entry}}
|
|
58
|
+
@onToggle={{fn (mut @resource.trigger_on_entry)}}
|
|
59
|
+
/>
|
|
60
|
+
</div>
|
|
61
|
+
<div>
|
|
62
|
+
<p class="text-sm font-medium text-gray-900 dark:text-white">Trigger on Entry</p>
|
|
63
|
+
<p class="text-xs text-gray-500 dark:text-gray-400 mt-0.5">
|
|
64
|
+
Fire a <code class="font-mono">geofence.entered</code> event when a driver enters this service area.
|
|
65
|
+
</p>
|
|
66
|
+
</div>
|
|
67
|
+
</div>
|
|
68
|
+
|
|
69
|
+
{{! Exit trigger toggle }}
|
|
70
|
+
<div class="flex items-start space-x-3 p-3 rounded-lg border border-gray-200 dark:border-gray-700">
|
|
71
|
+
<div class="flex-shrink-0 mt-0.5">
|
|
72
|
+
<Toggle
|
|
73
|
+
@isToggled={{@resource.trigger_on_exit}}
|
|
74
|
+
@onToggle={{fn (mut @resource.trigger_on_exit)}}
|
|
75
|
+
/>
|
|
76
|
+
</div>
|
|
77
|
+
<div>
|
|
78
|
+
<p class="text-sm font-medium text-gray-900 dark:text-white">Trigger on Exit</p>
|
|
79
|
+
<p class="text-xs text-gray-500 dark:text-gray-400 mt-0.5">
|
|
80
|
+
Fire a <code class="font-mono">geofence.exited</code> event when a driver leaves this service area.
|
|
81
|
+
</p>
|
|
82
|
+
</div>
|
|
83
|
+
</div>
|
|
84
|
+
</div>
|
|
85
|
+
|
|
86
|
+
<InputGroup
|
|
87
|
+
@name="Dwell Threshold (minutes)"
|
|
88
|
+
@value={{@resource.dwell_threshold_minutes}}
|
|
89
|
+
@type="number"
|
|
90
|
+
@placeholder="e.g. 10"
|
|
91
|
+
@helpText="Fire a geofence.dwelled event after a driver has been inside this service area for this many minutes. Leave blank to disable dwell tracking."
|
|
92
|
+
/>
|
|
93
|
+
|
|
94
|
+
<InputGroup
|
|
95
|
+
@name="Speed Limit (km/h)"
|
|
96
|
+
@value={{@resource.speed_limit_kmh}}
|
|
97
|
+
@type="number"
|
|
98
|
+
@placeholder="e.g. 50"
|
|
99
|
+
@helpText="Optional speed limit enforced within this service area. Leave blank for no limit."
|
|
100
|
+
/>
|
|
101
|
+
</div>
|
|
102
|
+
</ContentPanel>
|
|
44
103
|
</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>
|