@fleetbase/fleetops-engine 0.6.42 → 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/{modals/set-password.js → map/drawer/geofence-event-listing.js} +1 -1
- 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_08_000001_add_orchestrator_columns_to_vehicles_table.php +3 -0
- package/server/migrations/2026_04_08_000004_add_orchestrator_columns_to_payloads_table.php +3 -0
- package/server/migrations/2026_04_14_000001_drop_redundant_capacity_columns_from_payloads_table.php +2 -0
- package/server/migrations/2026_04_14_000002_rename_capacity_columns_on_vehicles_table.php +2 -0
- package/server/migrations/2026_04_21_081353_add_internal_id_column_to_vehicles_table.php +27 -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/Orchestration/Support/OrchestrationPayloadBuilder.php +2 -2
- 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
- package/addon/components/modals/set-password.hbs +0 -27
|
@@ -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
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export { default } from '@fleetbase/fleetops-engine/components/
|
|
1
|
+
export { default } from '@fleetbase/fleetops-engine/components/map/drawer/geofence-event-listing';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default } from '@fleetbase/fleetops-engine/services/geofence-event-bus';
|
package/composer.json
CHANGED
package/extension.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fleetbase/fleetops-engine",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.44",
|
|
4
4
|
"description": "Fleet & Transport Management Extension for Fleetbase",
|
|
5
5
|
"fleetbase": {
|
|
6
6
|
"route": "fleet-ops"
|
|
@@ -44,7 +44,7 @@
|
|
|
44
44
|
"@babel/core": "^7.23.2",
|
|
45
45
|
"@fleetbase/ember-core": "^0.3.18",
|
|
46
46
|
"@fleetbase/ember-ui": "^0.3.26",
|
|
47
|
-
"@fleetbase/fleetops-data": "^0.1.
|
|
47
|
+
"@fleetbase/fleetops-data": "^0.1.29",
|
|
48
48
|
"@fleetbase/leaflet-routing-machine": "^3.2.17",
|
|
49
49
|
"@fortawesome/ember-fontawesome": "^2.0.0",
|
|
50
50
|
"@fortawesome/fontawesome-svg-core": "6.4.0",
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
<?php
|
|
2
|
+
|
|
3
|
+
use Illuminate\Database\Migrations\Migration;
|
|
4
|
+
use Illuminate\Database\Schema\Blueprint;
|
|
5
|
+
use Illuminate\Support\Facades\Schema;
|
|
6
|
+
|
|
7
|
+
return new class extends Migration {
|
|
8
|
+
/**
|
|
9
|
+
* Run the migrations.
|
|
10
|
+
*
|
|
11
|
+
* Adds geofence trigger configuration columns to the zones table,
|
|
12
|
+
* enabling each zone to be configured as an active geofence with
|
|
13
|
+
* entry/exit/dwell event triggers.
|
|
14
|
+
*/
|
|
15
|
+
public function up(): void
|
|
16
|
+
{
|
|
17
|
+
Schema::table('zones', function (Blueprint $table) {
|
|
18
|
+
// Whether to fire a GeofenceEntered event when a driver enters this zone
|
|
19
|
+
$table->boolean('trigger_on_entry')->default(true)->after('stroke_color');
|
|
20
|
+
|
|
21
|
+
// Whether to fire a GeofenceExited event when a driver exits this zone
|
|
22
|
+
$table->boolean('trigger_on_exit')->default(true)->after('trigger_on_entry');
|
|
23
|
+
|
|
24
|
+
// Minutes a driver must remain inside before a GeofenceDwelled event fires.
|
|
25
|
+
// NULL disables dwell tracking for this zone.
|
|
26
|
+
$table->unsignedSmallInteger('dwell_threshold_minutes')->nullable()->after('trigger_on_exit');
|
|
27
|
+
|
|
28
|
+
// Optional speed limit (km/h) enforced within this zone.
|
|
29
|
+
// NULL means no speed limit is configured.
|
|
30
|
+
$table->unsignedSmallInteger('speed_limit_kmh')->nullable()->after('dwell_threshold_minutes');
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Reverse the migrations.
|
|
36
|
+
*/
|
|
37
|
+
public function down(): void
|
|
38
|
+
{
|
|
39
|
+
Schema::table('zones', function (Blueprint $table) {
|
|
40
|
+
$table->dropColumn([
|
|
41
|
+
'trigger_on_entry',
|
|
42
|
+
'trigger_on_exit',
|
|
43
|
+
'dwell_threshold_minutes',
|
|
44
|
+
'speed_limit_kmh',
|
|
45
|
+
]);
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
};
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
<?php
|
|
2
|
+
|
|
3
|
+
use Illuminate\Database\Migrations\Migration;
|
|
4
|
+
use Illuminate\Database\Schema\Blueprint;
|
|
5
|
+
use Illuminate\Support\Facades\Schema;
|
|
6
|
+
|
|
7
|
+
return new class extends Migration {
|
|
8
|
+
/**
|
|
9
|
+
* Run the migrations.
|
|
10
|
+
*
|
|
11
|
+
* Adds geofence trigger configuration columns to the service_areas table,
|
|
12
|
+
* enabling each service area to act as an active geofence boundary.
|
|
13
|
+
*/
|
|
14
|
+
public function up(): void
|
|
15
|
+
{
|
|
16
|
+
Schema::table('service_areas', function (Blueprint $table) {
|
|
17
|
+
$table->boolean('trigger_on_entry')->default(true)->after('stroke_color');
|
|
18
|
+
$table->boolean('trigger_on_exit')->default(true)->after('trigger_on_entry');
|
|
19
|
+
$table->unsignedSmallInteger('dwell_threshold_minutes')->nullable()->after('trigger_on_exit');
|
|
20
|
+
$table->unsignedSmallInteger('speed_limit_kmh')->nullable()->after('dwell_threshold_minutes');
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Reverse the migrations.
|
|
26
|
+
*/
|
|
27
|
+
public function down(): void
|
|
28
|
+
{
|
|
29
|
+
Schema::table('service_areas', function (Blueprint $table) {
|
|
30
|
+
$table->dropColumn([
|
|
31
|
+
'trigger_on_entry',
|
|
32
|
+
'trigger_on_exit',
|
|
33
|
+
'dwell_threshold_minutes',
|
|
34
|
+
'speed_limit_kmh',
|
|
35
|
+
]);
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
};
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
<?php
|
|
2
|
+
|
|
3
|
+
use Illuminate\Database\Migrations\Migration;
|
|
4
|
+
use Illuminate\Database\Schema\Blueprint;
|
|
5
|
+
use Illuminate\Support\Facades\Schema;
|
|
6
|
+
|
|
7
|
+
return new class extends Migration {
|
|
8
|
+
/**
|
|
9
|
+
* Run the migrations.
|
|
10
|
+
*
|
|
11
|
+
* Creates the driver_geofence_states table which acts as the state machine
|
|
12
|
+
* store for the geofencing engine. It tracks whether each driver is currently
|
|
13
|
+
* inside each geofence, enabling the system to detect transitions (entry/exit)
|
|
14
|
+
* rather than just evaluating current position in isolation.
|
|
15
|
+
*
|
|
16
|
+
* One record exists per driver-geofence pair. Records are upserted on each
|
|
17
|
+
* location update where a crossing is detected.
|
|
18
|
+
*/
|
|
19
|
+
public function up(): void
|
|
20
|
+
{
|
|
21
|
+
Schema::create('driver_geofence_states', function (Blueprint $table) {
|
|
22
|
+
$table->id();
|
|
23
|
+
$table->uuid('driver_uuid')->index();
|
|
24
|
+
$table->uuid('geofence_uuid')->index();
|
|
25
|
+
|
|
26
|
+
// Discriminator: 'zone' or 'service_area'
|
|
27
|
+
$table->string('geofence_type', 50)->default('zone');
|
|
28
|
+
|
|
29
|
+
// Whether the driver is currently inside this geofence
|
|
30
|
+
$table->boolean('is_inside')->default(false)->index();
|
|
31
|
+
|
|
32
|
+
// Timestamp of when the driver most recently entered this geofence
|
|
33
|
+
$table->timestamp('entered_at')->nullable();
|
|
34
|
+
|
|
35
|
+
// Timestamp of when the driver most recently exited this geofence
|
|
36
|
+
$table->timestamp('exited_at')->nullable();
|
|
37
|
+
|
|
38
|
+
// Queue job ID of the pending CheckGeofenceDwell job.
|
|
39
|
+
// Stored so it can be cancelled if the driver exits before the dwell threshold.
|
|
40
|
+
$table->string('dwell_job_id')->nullable();
|
|
41
|
+
|
|
42
|
+
$table->timestamps();
|
|
43
|
+
|
|
44
|
+
// Composite unique key: one state record per driver-geofence pair
|
|
45
|
+
$table->unique(['driver_uuid', 'geofence_uuid'], 'driver_geofence_unique');
|
|
46
|
+
|
|
47
|
+
// Cascade delete when the driver is deleted
|
|
48
|
+
$table->foreign('driver_uuid')
|
|
49
|
+
->references('uuid')
|
|
50
|
+
->on('drivers')
|
|
51
|
+
->onDelete('cascade');
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Reverse the migrations.
|
|
57
|
+
*/
|
|
58
|
+
public function down(): void
|
|
59
|
+
{
|
|
60
|
+
Schema::dropIfExists('driver_geofence_states');
|
|
61
|
+
}
|
|
62
|
+
};
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
<?php
|
|
2
|
+
|
|
3
|
+
use Illuminate\Database\Migrations\Migration;
|
|
4
|
+
use Illuminate\Database\Schema\Blueprint;
|
|
5
|
+
use Illuminate\Support\Facades\Schema;
|
|
6
|
+
|
|
7
|
+
return new class extends Migration {
|
|
8
|
+
/**
|
|
9
|
+
* Run the migrations.
|
|
10
|
+
*
|
|
11
|
+
* Creates the geofence_events_log table which provides a persistent,
|
|
12
|
+
* queryable audit trail of all geofence events (entered, exited, dwelled).
|
|
13
|
+
* This table powers the reporting dashboard, dwell time analytics, and
|
|
14
|
+
* geofence activity history views.
|
|
15
|
+
*/
|
|
16
|
+
public function up(): void
|
|
17
|
+
{
|
|
18
|
+
Schema::create('geofence_events_log', function (Blueprint $table) {
|
|
19
|
+
$table->id();
|
|
20
|
+
$table->uuid('uuid')->unique()->index();
|
|
21
|
+
$table->uuid('company_uuid')->index();
|
|
22
|
+
$table->uuid('driver_uuid')->index();
|
|
23
|
+
$table->uuid('vehicle_uuid')->nullable()->index();
|
|
24
|
+
$table->uuid('order_uuid')->nullable()->index();
|
|
25
|
+
$table->uuid('geofence_uuid')->index();
|
|
26
|
+
|
|
27
|
+
// Discriminator: 'zone' or 'service_area'
|
|
28
|
+
$table->string('geofence_type', 50)->default('zone');
|
|
29
|
+
|
|
30
|
+
// Snapshot of the geofence name at the time of the event
|
|
31
|
+
$table->string('geofence_name')->nullable();
|
|
32
|
+
|
|
33
|
+
// The type of geofence event
|
|
34
|
+
$table->enum('event_type', ['entered', 'exited', 'dwelled'])->index();
|
|
35
|
+
|
|
36
|
+
// Driver location at the time of the event
|
|
37
|
+
$table->decimal('latitude', 10, 7)->nullable();
|
|
38
|
+
$table->decimal('longitude', 10, 7)->nullable();
|
|
39
|
+
|
|
40
|
+
// Driver speed at the time of the event (km/h)
|
|
41
|
+
$table->decimal('speed_kmh', 8, 2)->nullable();
|
|
42
|
+
|
|
43
|
+
// Populated for 'exited' and 'dwelled' events
|
|
44
|
+
$table->unsignedInteger('dwell_duration_minutes')->nullable();
|
|
45
|
+
|
|
46
|
+
// The precise time the event occurred (not the DB insert time)
|
|
47
|
+
$table->timestamp('occurred_at')->index();
|
|
48
|
+
|
|
49
|
+
$table->timestamps();
|
|
50
|
+
|
|
51
|
+
// Composite indexes for common query patterns
|
|
52
|
+
$table->index(['company_uuid', 'occurred_at'], 'gel_company_occurred_idx');
|
|
53
|
+
$table->index(['geofence_uuid', 'occurred_at'], 'gel_geofence_occurred_idx');
|
|
54
|
+
$table->index(['driver_uuid', 'occurred_at'], 'gel_driver_occurred_idx');
|
|
55
|
+
$table->index(['order_uuid', 'event_type'], 'gel_order_event_idx');
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Reverse the migrations.
|
|
61
|
+
*/
|
|
62
|
+
public function down(): void
|
|
63
|
+
{
|
|
64
|
+
Schema::dropIfExists('geofence_events_log');
|
|
65
|
+
}
|
|
66
|
+
};
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
<?php
|
|
2
|
+
|
|
2
3
|
use Illuminate\Database\Migrations\Migration;
|
|
3
4
|
use Illuminate\Database\Schema\Blueprint;
|
|
4
5
|
use Illuminate\Support\Facades\Schema;
|
|
6
|
+
|
|
5
7
|
/**
|
|
6
8
|
* Add Orchestrator constraint columns to the vehicles table.
|
|
7
9
|
*
|
|
@@ -34,6 +36,7 @@ return new class extends Migration {
|
|
|
34
36
|
$table->boolean('return_to_depot')->default(true)->after('time_window_end');
|
|
35
37
|
});
|
|
36
38
|
}
|
|
39
|
+
|
|
37
40
|
public function down(): void
|
|
38
41
|
{
|
|
39
42
|
Schema::table('vehicles', function (Blueprint $table) {
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
<?php
|
|
2
|
+
|
|
2
3
|
use Illuminate\Database\Migrations\Migration;
|
|
3
4
|
use Illuminate\Database\Schema\Blueprint;
|
|
4
5
|
use Illuminate\Support\Facades\Schema;
|
|
6
|
+
|
|
5
7
|
/**
|
|
6
8
|
* Add Orchestrator capacity columns to the payloads table.
|
|
7
9
|
*
|
|
@@ -24,6 +26,7 @@ return new class extends Migration {
|
|
|
24
26
|
$table->unsignedInteger('capacity_parcels')->nullable()->after('capacity_pallets');
|
|
25
27
|
});
|
|
26
28
|
}
|
|
29
|
+
|
|
27
30
|
public function down(): void
|
|
28
31
|
{
|
|
29
32
|
Schema::table('payloads', function (Blueprint $table) {
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
<?php
|
|
2
|
+
|
|
3
|
+
use Illuminate\Database\Migrations\Migration;
|
|
4
|
+
use Illuminate\Database\Schema\Blueprint;
|
|
5
|
+
use Illuminate\Support\Facades\Schema;
|
|
6
|
+
|
|
7
|
+
return new class extends Migration {
|
|
8
|
+
public function up(): void
|
|
9
|
+
{
|
|
10
|
+
if (!Schema::hasColumn('vehicles', 'internal_id')) {
|
|
11
|
+
Schema::table('vehicles', function (Blueprint $table) {
|
|
12
|
+
$table->string('internal_id')
|
|
13
|
+
->nullable()
|
|
14
|
+
->after('public_id');
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
public function down(): void
|
|
20
|
+
{
|
|
21
|
+
if (Schema::hasColumn('vehicles', 'internal_id')) {
|
|
22
|
+
Schema::table('vehicles', function (Blueprint $table) {
|
|
23
|
+
$table->dropColumn('internal_id');
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
};
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
<?php
|
|
2
|
+
|
|
3
|
+
use Illuminate\Database\Migrations\Migration;
|
|
4
|
+
use Illuminate\Database\Schema\Blueprint;
|
|
5
|
+
use Illuminate\Support\Facades\Schema;
|
|
6
|
+
|
|
7
|
+
return new class extends Migration {
|
|
8
|
+
/**
|
|
9
|
+
* Run the migrations.
|
|
10
|
+
*/
|
|
11
|
+
public function up(): void
|
|
12
|
+
{
|
|
13
|
+
Schema::table('geofence_events_log', function (Blueprint $table) {
|
|
14
|
+
$table->uuid('subject_uuid')->nullable()->after('order_uuid')->index();
|
|
15
|
+
$table->string('subject_type', 50)->default('driver')->after('subject_uuid')->index();
|
|
16
|
+
$table->string('subject_name')->nullable()->after('subject_type');
|
|
17
|
+
$table->uuid('driver_uuid')->nullable()->change();
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Reverse the migrations.
|
|
23
|
+
*/
|
|
24
|
+
public function down(): void
|
|
25
|
+
{
|
|
26
|
+
Schema::table('geofence_events_log', function (Blueprint $table) {
|
|
27
|
+
$table->dropColumn(['subject_uuid', 'subject_type', 'subject_name']);
|
|
28
|
+
$table->uuid('driver_uuid')->nullable(false)->change();
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
};
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
<?php
|
|
2
|
+
|
|
3
|
+
use Illuminate\Database\Migrations\Migration;
|
|
4
|
+
use Illuminate\Database\Schema\Blueprint;
|
|
5
|
+
use Illuminate\Support\Facades\Schema;
|
|
6
|
+
|
|
7
|
+
return new class extends Migration {
|
|
8
|
+
/**
|
|
9
|
+
* Run the migrations.
|
|
10
|
+
*/
|
|
11
|
+
public function up(): void
|
|
12
|
+
{
|
|
13
|
+
Schema::create('vehicle_geofence_states', function (Blueprint $table) {
|
|
14
|
+
$table->id();
|
|
15
|
+
$table->uuid('vehicle_uuid')->index();
|
|
16
|
+
$table->uuid('geofence_uuid')->index();
|
|
17
|
+
$table->string('geofence_type', 50)->default('zone');
|
|
18
|
+
$table->boolean('is_inside')->default(false)->index();
|
|
19
|
+
$table->timestamp('entered_at')->nullable();
|
|
20
|
+
$table->timestamp('exited_at')->nullable();
|
|
21
|
+
$table->string('dwell_job_id')->nullable();
|
|
22
|
+
$table->timestamps();
|
|
23
|
+
|
|
24
|
+
$table->unique(['vehicle_uuid', 'geofence_uuid'], 'vehicle_geofence_unique');
|
|
25
|
+
$table->foreign('vehicle_uuid')
|
|
26
|
+
->references('uuid')
|
|
27
|
+
->on('vehicles')
|
|
28
|
+
->onDelete('cascade');
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Reverse the migrations.
|
|
34
|
+
*/
|
|
35
|
+
public function down(): void
|
|
36
|
+
{
|
|
37
|
+
Schema::dropIfExists('vehicle_geofence_states');
|
|
38
|
+
}
|
|
39
|
+
};
|