Skip to content

Commit 579c654

Browse files
committed
full complete geofencing feature
1 parent 467eb58 commit 579c654

30 files changed

Lines changed: 1327 additions & 432 deletions

addon/components/map/drawer.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ export default class MapDrawerComponent extends Component {
1515
this.universe._createMenuItem('Drivers', null, { icon: 'id-card', component: 'map/drawer/driver-listing' }),
1616
this.universe._createMenuItem('Places', null, { icon: 'building', component: 'map/drawer/place-listing' }),
1717
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' }),
1819
this.universe._createMenuItem('Events', null, { icon: 'stream', component: 'map/drawer/device-event-listing' }),
1920
...(isArray(registeredTabs) ? registeredTabs : []),
2021
];
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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>
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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+
}

addon/components/map/leaflet-live-map.js

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ export default class MapLeafletLiveMapComponent extends Component {
3030
@service universe;
3131
@service('universe/menu-service') menuService;
3232
@service geofenceEventBus;
33+
@service currentUser;
3334

3435
/** properties */
3536
id = guidFor(this);
@@ -59,6 +60,13 @@ export default class MapLeafletLiveMapComponent extends Component {
5960
// Ensure we have valid coordinates on initialization
6061
this.#updateCoordinatesFromLocation();
6162

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+
6270
// Subscribe to geofence events so the live map can react to boundary crossings
6371
this._geofenceEnteredHandler = this.#handleGeofenceEntered.bind(this);
6472
this._geofenceExitedHandler = this.#handleGeofenceExited.bind(this);
@@ -74,6 +82,10 @@ export default class MapLeafletLiveMapComponent extends Component {
7482
this.universe.off('user.located', this._locationUpdateHandler);
7583
this._locationUpdateHandler = null;
7684
}
85+
if (this._currentUserLoadedHandler) {
86+
this.currentUser.off('user.loaded', this._currentUserLoadedHandler);
87+
this._currentUserLoadedHandler = null;
88+
}
7789
if (this._geofenceEnteredHandler) {
7890
this.universe.off('fleet-ops.geofence.entered', this._geofenceEnteredHandler);
7991
this._geofenceEnteredHandler = null;

addon/components/map/toolbar/geofence-events-panel.hbs

Lines changed: 0 additions & 80 deletions
This file was deleted.

0 commit comments

Comments
 (0)