@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,155 @@
|
|
|
1
|
+
<?php
|
|
2
|
+
|
|
3
|
+
namespace Fleetbase\FleetOps\Models;
|
|
4
|
+
|
|
5
|
+
use Fleetbase\Traits\HasUuid;
|
|
6
|
+
use Illuminate\Database\Eloquent\Model;
|
|
7
|
+
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* GeofenceEventLog.
|
|
11
|
+
*
|
|
12
|
+
* Persistent audit log of all geofence events (entered, exited, dwelled).
|
|
13
|
+
* Powers the reporting dashboard, dwell time analytics, and geofence
|
|
14
|
+
* activity history views.
|
|
15
|
+
*
|
|
16
|
+
* @property string $uuid
|
|
17
|
+
* @property string $company_uuid
|
|
18
|
+
* @property string $driver_uuid
|
|
19
|
+
* @property string|null $vehicle_uuid
|
|
20
|
+
* @property string|null $order_uuid
|
|
21
|
+
* @property string $geofence_uuid
|
|
22
|
+
* @property string $geofence_type
|
|
23
|
+
* @property string|null $geofence_name
|
|
24
|
+
* @property string $event_type
|
|
25
|
+
* @property float|null $latitude
|
|
26
|
+
* @property float|null $longitude
|
|
27
|
+
* @property float|null $speed_kmh
|
|
28
|
+
* @property int|null $dwell_duration_minutes
|
|
29
|
+
* @property \Carbon\Carbon $occurred_at
|
|
30
|
+
*/
|
|
31
|
+
class GeofenceEventLog extends Model
|
|
32
|
+
{
|
|
33
|
+
use HasUuid;
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* The database table used by the model.
|
|
37
|
+
*
|
|
38
|
+
* @var string
|
|
39
|
+
*/
|
|
40
|
+
protected $table = 'geofence_events_log';
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* The primary key for the model.
|
|
44
|
+
*
|
|
45
|
+
* @var string
|
|
46
|
+
*/
|
|
47
|
+
protected $primaryKey = 'uuid';
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* The "type" of the primary key ID.
|
|
51
|
+
*
|
|
52
|
+
* @var string
|
|
53
|
+
*/
|
|
54
|
+
protected $keyType = 'string';
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Indicates if the IDs are auto-incrementing.
|
|
58
|
+
*
|
|
59
|
+
* @var bool
|
|
60
|
+
*/
|
|
61
|
+
public $incrementing = false;
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* The attributes that are mass assignable.
|
|
65
|
+
*
|
|
66
|
+
* @var array
|
|
67
|
+
*/
|
|
68
|
+
protected $fillable = [
|
|
69
|
+
'uuid',
|
|
70
|
+
'company_uuid',
|
|
71
|
+
'driver_uuid',
|
|
72
|
+
'vehicle_uuid',
|
|
73
|
+
'order_uuid',
|
|
74
|
+
'subject_uuid',
|
|
75
|
+
'subject_type',
|
|
76
|
+
'subject_name',
|
|
77
|
+
'geofence_uuid',
|
|
78
|
+
'geofence_type',
|
|
79
|
+
'geofence_name',
|
|
80
|
+
'event_type',
|
|
81
|
+
'latitude',
|
|
82
|
+
'longitude',
|
|
83
|
+
'speed_kmh',
|
|
84
|
+
'dwell_duration_minutes',
|
|
85
|
+
'occurred_at',
|
|
86
|
+
];
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* The attributes that should be cast to native types.
|
|
90
|
+
*
|
|
91
|
+
* @var array
|
|
92
|
+
*/
|
|
93
|
+
protected $casts = [
|
|
94
|
+
'occurred_at' => 'datetime',
|
|
95
|
+
'latitude' => 'float',
|
|
96
|
+
'longitude' => 'float',
|
|
97
|
+
'speed_kmh' => 'float',
|
|
98
|
+
'dwell_duration_minutes' => 'integer',
|
|
99
|
+
];
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* The attributes excluded from the model's JSON form.
|
|
103
|
+
*
|
|
104
|
+
* @var array
|
|
105
|
+
*/
|
|
106
|
+
protected $hidden = [];
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Get the driver associated with this event.
|
|
110
|
+
*/
|
|
111
|
+
public function driver(): BelongsTo
|
|
112
|
+
{
|
|
113
|
+
return $this->belongsTo(Driver::class, 'driver_uuid', 'uuid');
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Get the order associated with this event (if any).
|
|
118
|
+
*/
|
|
119
|
+
public function order(): BelongsTo
|
|
120
|
+
{
|
|
121
|
+
return $this->belongsTo(Order::class, 'order_uuid', 'uuid');
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Get the vehicle associated with this event (if any).
|
|
126
|
+
*/
|
|
127
|
+
public function vehicle(): BelongsTo
|
|
128
|
+
{
|
|
129
|
+
return $this->belongsTo(Vehicle::class, 'vehicle_uuid', 'uuid');
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Scope to filter by company.
|
|
134
|
+
*
|
|
135
|
+
* @param \Illuminate\Database\Eloquent\Builder $query
|
|
136
|
+
*
|
|
137
|
+
* @return \Illuminate\Database\Eloquent\Builder
|
|
138
|
+
*/
|
|
139
|
+
public function scopeForCompany($query, string $companyUuid)
|
|
140
|
+
{
|
|
141
|
+
return $query->where('company_uuid', $companyUuid);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Scope to filter by event type.
|
|
146
|
+
*
|
|
147
|
+
* @param \Illuminate\Database\Eloquent\Builder $query
|
|
148
|
+
*
|
|
149
|
+
* @return \Illuminate\Database\Eloquent\Builder
|
|
150
|
+
*/
|
|
151
|
+
public function scopeOfType($query, string $type)
|
|
152
|
+
{
|
|
153
|
+
return $query->where('event_type', $type);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
@@ -60,7 +60,22 @@ class ServiceArea extends Model
|
|
|
60
60
|
*
|
|
61
61
|
* @var array
|
|
62
62
|
*/
|
|
63
|
-
protected $fillable = [
|
|
63
|
+
protected $fillable = [
|
|
64
|
+
'_key',
|
|
65
|
+
'company_uuid',
|
|
66
|
+
'name',
|
|
67
|
+
'type',
|
|
68
|
+
'parent_uuid',
|
|
69
|
+
'border',
|
|
70
|
+
'color',
|
|
71
|
+
'stroke_color',
|
|
72
|
+
'status',
|
|
73
|
+
'country',
|
|
74
|
+
'trigger_on_entry',
|
|
75
|
+
'trigger_on_exit',
|
|
76
|
+
'dwell_threshold_minutes',
|
|
77
|
+
'speed_limit_kmh',
|
|
78
|
+
];
|
|
64
79
|
|
|
65
80
|
/**
|
|
66
81
|
* The attributes that are spatial columns.
|
|
@@ -96,7 +111,11 @@ class ServiceArea extends Model
|
|
|
96
111
|
* @var array
|
|
97
112
|
*/
|
|
98
113
|
protected $casts = [
|
|
99
|
-
'border'
|
|
114
|
+
'border' => MultiPolygonCast::class,
|
|
115
|
+
'trigger_on_entry' => 'boolean',
|
|
116
|
+
'trigger_on_exit' => 'boolean',
|
|
117
|
+
'dwell_threshold_minutes' => 'integer',
|
|
118
|
+
'speed_limit_kmh' => 'integer',
|
|
100
119
|
];
|
|
101
120
|
|
|
102
121
|
/**
|
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
namespace Fleetbase\FleetOps\Models;
|
|
4
4
|
|
|
5
5
|
use Fleetbase\Casts\Json;
|
|
6
|
+
use Fleetbase\FleetOps\Http\Requests\CreateOrderRequest;
|
|
7
|
+
use Fleetbase\FleetOps\Http\Requests\UpdateOrderRequest;
|
|
6
8
|
use Fleetbase\FleetOps\Integrations\Lalamove\Lalamove;
|
|
7
9
|
use Fleetbase\FleetOps\Support\Payment;
|
|
8
10
|
use Fleetbase\FleetOps\Support\Utils;
|
|
@@ -136,7 +138,7 @@ class ServiceQuote extends Model
|
|
|
136
138
|
return Lalamove::serviceQuoteFromQuotation($quotation);
|
|
137
139
|
}
|
|
138
140
|
|
|
139
|
-
public static function resolveFromRequest(Request $request): ?ServiceQuote
|
|
141
|
+
public static function resolveFromRequest(Request|CreateOrderRequest|UpdateOrderRequest $request): ?ServiceQuote
|
|
140
142
|
{
|
|
141
143
|
$serviceQuote = $request->or(['order.service_quote_uuid', 'service_quote', 'service_quote_id', 'order.service_quote']);
|
|
142
144
|
|
|
@@ -62,7 +62,21 @@ class Zone extends Model
|
|
|
62
62
|
*
|
|
63
63
|
* @var array
|
|
64
64
|
*/
|
|
65
|
-
protected $fillable = [
|
|
65
|
+
protected $fillable = [
|
|
66
|
+
'_key',
|
|
67
|
+
'company_uuid',
|
|
68
|
+
'service_area_uuid',
|
|
69
|
+
'name',
|
|
70
|
+
'description',
|
|
71
|
+
'border',
|
|
72
|
+
'color',
|
|
73
|
+
'stroke_color',
|
|
74
|
+
'status',
|
|
75
|
+
'trigger_on_entry',
|
|
76
|
+
'trigger_on_exit',
|
|
77
|
+
'dwell_threshold_minutes',
|
|
78
|
+
'speed_limit_kmh',
|
|
79
|
+
];
|
|
66
80
|
|
|
67
81
|
/**
|
|
68
82
|
* The attributes that should be cast to native types.
|
|
@@ -70,7 +84,11 @@ class Zone extends Model
|
|
|
70
84
|
* @var array
|
|
71
85
|
*/
|
|
72
86
|
protected $casts = [
|
|
73
|
-
'border'
|
|
87
|
+
'border' => PolygonCast::class,
|
|
88
|
+
'trigger_on_entry' => 'boolean',
|
|
89
|
+
'trigger_on_exit' => 'boolean',
|
|
90
|
+
'dwell_threshold_minutes' => 'integer',
|
|
91
|
+
'speed_limit_kmh' => 'integer',
|
|
74
92
|
];
|
|
75
93
|
|
|
76
94
|
/**
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
<?php
|
|
2
|
+
|
|
3
|
+
namespace Fleetbase\FleetOps\Notifications;
|
|
4
|
+
|
|
5
|
+
use Fleetbase\FleetOps\Models\Order;
|
|
6
|
+
use Illuminate\Bus\Queueable;
|
|
7
|
+
use Illuminate\Notifications\Messages\MailMessage;
|
|
8
|
+
use Illuminate\Notifications\Notification;
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* DriverArrivedAtGeofence.
|
|
12
|
+
*
|
|
13
|
+
* Sent to the order customer when a driver enters the geofence associated
|
|
14
|
+
* with the order's current destination waypoint.
|
|
15
|
+
*
|
|
16
|
+
* Delivery channels: mail and database.
|
|
17
|
+
*/
|
|
18
|
+
class DriverArrivedAtGeofence extends Notification
|
|
19
|
+
{
|
|
20
|
+
use Queueable;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* The order associated with the arrival.
|
|
24
|
+
*/
|
|
25
|
+
protected Order $order;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* The geofence that was entered (Zone or ServiceArea).
|
|
29
|
+
*/
|
|
30
|
+
protected $geofence;
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Create a new DriverArrivedAtGeofence notification.
|
|
34
|
+
*
|
|
35
|
+
* @param mixed $geofence Zone or ServiceArea
|
|
36
|
+
*/
|
|
37
|
+
public function __construct(Order $order, $geofence)
|
|
38
|
+
{
|
|
39
|
+
$this->order = $order;
|
|
40
|
+
$this->geofence = $geofence;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Get the notification's delivery channels.
|
|
45
|
+
*/
|
|
46
|
+
public function via($notifiable): array
|
|
47
|
+
{
|
|
48
|
+
return ['mail', 'database'];
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Get the mail representation of the notification.
|
|
53
|
+
*/
|
|
54
|
+
public function toMail($notifiable): MailMessage
|
|
55
|
+
{
|
|
56
|
+
$geofenceName = $this->geofence->name ?? 'your location';
|
|
57
|
+
$orderId = $this->order->public_id ?? $this->order->uuid;
|
|
58
|
+
|
|
59
|
+
return (new MailMessage())
|
|
60
|
+
->subject("Your driver has arrived — Order #{$orderId}")
|
|
61
|
+
->greeting('Good news!')
|
|
62
|
+
->line("Your driver has arrived at {$geofenceName} for order #{$orderId}.")
|
|
63
|
+
->line('Please be ready to receive your delivery.')
|
|
64
|
+
->action('Track Your Order', url("/tracking/{$this->order->tracking_number}"))
|
|
65
|
+
->line('Thank you for using our service.');
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Get the array representation of the notification (database channel).
|
|
70
|
+
*/
|
|
71
|
+
public function toArray($notifiable): array
|
|
72
|
+
{
|
|
73
|
+
return [
|
|
74
|
+
'event' => 'driver.arrived_at_geofence',
|
|
75
|
+
'order_id' => $this->order->public_id,
|
|
76
|
+
'order_uuid' => $this->order->uuid,
|
|
77
|
+
'geofence_id' => $this->geofence->public_id ?? null,
|
|
78
|
+
'geofence_name' => $this->geofence->name ?? null,
|
|
79
|
+
'message' => sprintf(
|
|
80
|
+
'Your driver has arrived at %s for order #%s.',
|
|
81
|
+
$this->geofence->name ?? 'your location',
|
|
82
|
+
$this->order->public_id
|
|
83
|
+
),
|
|
84
|
+
];
|
|
85
|
+
}
|
|
86
|
+
}
|
|
@@ -70,9 +70,9 @@ class OrchestrationPayloadBuilder
|
|
|
70
70
|
|
|
71
71
|
foreach ($payload->entities as $entity) {
|
|
72
72
|
// --- Weight ---
|
|
73
|
-
$rawWeight
|
|
73
|
+
$rawWeight = (float) ($entity->weight ?? 0);
|
|
74
74
|
$weightUnit = strtolower($entity->weight_unit ?? 'kg');
|
|
75
|
-
$weightKg
|
|
75
|
+
$weightKg = match ($weightUnit) {
|
|
76
76
|
'g', 'gram', 'grams' => $rawWeight / 1000,
|
|
77
77
|
'lb', 'lbs', 'pound', 'pounds' => $rawWeight * 0.453592,
|
|
78
78
|
'oz', 'ounce', 'ounces' => $rawWeight * 0.0283495,
|
|
@@ -23,6 +23,25 @@ class EventServiceProvider extends ServiceProvider
|
|
|
23
23
|
\Fleetbase\FleetOps\Events\OrderFailed::class => [\Fleetbase\Listeners\SendResourceLifecycleWebhook::class, \Fleetbase\FleetOps\Listeners\NotifyOrderEvent::class],
|
|
24
24
|
\Fleetbase\FleetOps\Events\OrderReady::class => [\Fleetbase\FleetOps\Listeners\HandleOrderReady::class],
|
|
25
25
|
|
|
26
|
+
/*
|
|
27
|
+
* Geofence Events
|
|
28
|
+
*
|
|
29
|
+
* Each event is handled by a domain listener (business logic, event log)
|
|
30
|
+
* and the generic SendResourceLifecycleWebhook listener (webhook delivery).
|
|
31
|
+
*/
|
|
32
|
+
\Fleetbase\FleetOps\Events\GeofenceEntered::class => [
|
|
33
|
+
\Fleetbase\FleetOps\Listeners\HandleGeofenceEntered::class,
|
|
34
|
+
\Fleetbase\Listeners\SendResourceLifecycleWebhook::class,
|
|
35
|
+
],
|
|
36
|
+
\Fleetbase\FleetOps\Events\GeofenceExited::class => [
|
|
37
|
+
\Fleetbase\FleetOps\Listeners\HandleGeofenceExited::class,
|
|
38
|
+
\Fleetbase\Listeners\SendResourceLifecycleWebhook::class,
|
|
39
|
+
],
|
|
40
|
+
\Fleetbase\FleetOps\Events\GeofenceDwelled::class => [
|
|
41
|
+
\Fleetbase\FleetOps\Listeners\HandleGeofenceDwelled::class,
|
|
42
|
+
\Fleetbase\Listeners\SendResourceLifecycleWebhook::class,
|
|
43
|
+
],
|
|
44
|
+
|
|
26
45
|
/*
|
|
27
46
|
* Core Events
|
|
28
47
|
*/
|
|
@@ -61,6 +61,7 @@ class FleetOpsServiceProvider extends CoreServiceProvider
|
|
|
61
61
|
\Fleetbase\FleetOps\Console\Commands\PurgeUnpurchasedServiceQuotes::class,
|
|
62
62
|
\Fleetbase\FleetOps\Console\Commands\SendDriverNotification::class,
|
|
63
63
|
\Fleetbase\FleetOps\Console\Commands\ReplayVehicleLocations::class,
|
|
64
|
+
\Fleetbase\FleetOps\Console\Commands\SimulateGeofenceEvents::class,
|
|
64
65
|
\Fleetbase\FleetOps\Console\Commands\TestEmail::class,
|
|
65
66
|
\Fleetbase\FleetOps\Console\Commands\ProcessMaintenanceTriggers::class,
|
|
66
67
|
\Fleetbase\FleetOps\Console\Commands\SendMaintenanceReminders::class,
|
|
@@ -84,6 +85,14 @@ class FleetOpsServiceProvider extends CoreServiceProvider
|
|
|
84
85
|
$this->app->register(CoreServiceProvider::class);
|
|
85
86
|
$this->app->register(ReportSchemaServiceProvider::class);
|
|
86
87
|
|
|
88
|
+
// Register the GeofenceIntersectionService as a singleton so that
|
|
89
|
+
// the same instance is reused across the request lifecycle, avoiding
|
|
90
|
+
// repeated instantiation on high-frequency location update calls.
|
|
91
|
+
$this->app->singleton(
|
|
92
|
+
\Fleetbase\FleetOps\Support\GeofenceIntersectionService::class,
|
|
93
|
+
fn () => new \Fleetbase\FleetOps\Support\GeofenceIntersectionService()
|
|
94
|
+
);
|
|
95
|
+
|
|
87
96
|
// Register the OrchestrationEngineRegistry as a singleton so that engines
|
|
88
97
|
// registered from any service provider share the same instance.
|
|
89
98
|
$this->app->singleton(
|
|
@@ -155,6 +164,7 @@ class FleetOpsServiceProvider extends CoreServiceProvider
|
|
|
155
164
|
\Fleetbase\FleetOps\Notifications\OrderPing::class,
|
|
156
165
|
\Fleetbase\FleetOps\Notifications\OrderFailed::class,
|
|
157
166
|
\Fleetbase\FleetOps\Notifications\OrderCompleted::class,
|
|
167
|
+
\Fleetbase\FleetOps\Notifications\DriverArrivedAtGeofence::class,
|
|
158
168
|
]);
|
|
159
169
|
|
|
160
170
|
// Register Notifiables
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
<?php
|
|
2
|
+
|
|
3
|
+
namespace Fleetbase\FleetOps\Support;
|
|
4
|
+
|
|
5
|
+
use Fleetbase\FleetOps\Models\Driver;
|
|
6
|
+
use Fleetbase\FleetOps\Models\ServiceArea;
|
|
7
|
+
use Fleetbase\FleetOps\Models\Vehicle;
|
|
8
|
+
use Fleetbase\FleetOps\Models\Zone;
|
|
9
|
+
use Fleetbase\LaravelMysqlSpatial\Types\Point;
|
|
10
|
+
use Illuminate\Support\Facades\DB;
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* GeofenceIntersectionService.
|
|
14
|
+
*
|
|
15
|
+
* The core spatial engine for the active geofencing system.
|
|
16
|
+
*
|
|
17
|
+
* On every driver location update, this service:
|
|
18
|
+
* 1. Queries all zones and service areas containing the new point using
|
|
19
|
+
* a two-pass MySQL spatial strategy (MBRContains for index-assisted
|
|
20
|
+
* bounding-box pre-filter, then ST_Contains for precise polygon check).
|
|
21
|
+
* 2. Compares the result against the driver's current state in the
|
|
22
|
+
* driver_geofence_states table.
|
|
23
|
+
* 3. Returns a list of crossing events (entered / exited) for the caller
|
|
24
|
+
* to dispatch as Laravel events.
|
|
25
|
+
*
|
|
26
|
+
* This class is registered as a singleton in FleetOpsServiceProvider.
|
|
27
|
+
*/
|
|
28
|
+
class GeofenceIntersectionService
|
|
29
|
+
{
|
|
30
|
+
/**
|
|
31
|
+
* Detect geofence boundary crossings for a driver given their new location.
|
|
32
|
+
*
|
|
33
|
+
* Returns an array of crossing events. Each element is an associative array:
|
|
34
|
+
* - 'type': 'entered' | 'exited'
|
|
35
|
+
* - 'geofence': Zone or ServiceArea model instance
|
|
36
|
+
* - 'geofence_type': 'zone' | 'service_area'
|
|
37
|
+
*/
|
|
38
|
+
public function detectCrossings(Driver|Vehicle $subject, Point $newLocation): array
|
|
39
|
+
{
|
|
40
|
+
if ($subject instanceof Driver) {
|
|
41
|
+
return $this->detectDriverCrossings($subject, $newLocation);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return $this->detectVehicleCrossings($subject, $newLocation);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
public function detectDriverCrossings(Driver $driver, Point $newLocation): array
|
|
48
|
+
{
|
|
49
|
+
return $this->detectSubjectCrossings($driver->company_uuid, $newLocation, 'driver_geofence_states', 'driver_uuid', $driver->uuid);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
public function detectVehicleCrossings(Vehicle $vehicle, Point $newLocation): array
|
|
53
|
+
{
|
|
54
|
+
return $this->detectSubjectCrossings($vehicle->company_uuid, $newLocation, 'vehicle_geofence_states', 'vehicle_uuid', $vehicle->uuid);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
protected function detectSubjectCrossings(string $companyUuid, Point $newLocation, string $stateTable, string $subjectColumn, string $subjectUuid): array
|
|
58
|
+
{
|
|
59
|
+
$crossings = [];
|
|
60
|
+
|
|
61
|
+
// Build the WKT point string. MySQL ST_GeomFromText expects (lng lat) order.
|
|
62
|
+
$wkt = sprintf('POINT(%s %s)', $newLocation->getLng(), $newLocation->getLat());
|
|
63
|
+
|
|
64
|
+
// ----------------------------------------------------------------
|
|
65
|
+
// 1. Find all Zones the driver is currently inside.
|
|
66
|
+
//
|
|
67
|
+
// Two-pass strategy for MySQL performance:
|
|
68
|
+
// - MBRContains uses the SPATIAL index to filter by minimum
|
|
69
|
+
// bounding rectangle (fast, may include false positives).
|
|
70
|
+
// - ST_Contains performs the precise polygon containment check
|
|
71
|
+
// on the reduced candidate set (accurate, no index needed).
|
|
72
|
+
// ----------------------------------------------------------------
|
|
73
|
+
$insideZones = Zone::where('company_uuid', $companyUuid)
|
|
74
|
+
->whereNotNull('border')
|
|
75
|
+
->where(function ($q) {
|
|
76
|
+
$q->where('trigger_on_entry', true)
|
|
77
|
+
->orWhere('trigger_on_exit', true)
|
|
78
|
+
->orWhereNotNull('dwell_threshold_minutes');
|
|
79
|
+
})
|
|
80
|
+
->whereRaw('MBRContains(`border`, ST_GeomFromText(?))', [$wkt])
|
|
81
|
+
->whereRaw('ST_Contains(`border`, ST_GeomFromText(?))', [$wkt])
|
|
82
|
+
->get();
|
|
83
|
+
|
|
84
|
+
// ----------------------------------------------------------------
|
|
85
|
+
// 2. Find all Service Areas the driver is currently inside.
|
|
86
|
+
//
|
|
87
|
+
// Service areas use MultiPolygon borders, so we use
|
|
88
|
+
// ST_Contains which handles both Polygon and MultiPolygon.
|
|
89
|
+
// ----------------------------------------------------------------
|
|
90
|
+
$insideServiceAreas = ServiceArea::where('company_uuid', $companyUuid)
|
|
91
|
+
->whereNotNull('border')
|
|
92
|
+
->where(function ($q) {
|
|
93
|
+
$q->where('trigger_on_entry', true)
|
|
94
|
+
->orWhere('trigger_on_exit', true)
|
|
95
|
+
->orWhereNotNull('dwell_threshold_minutes');
|
|
96
|
+
})
|
|
97
|
+
->whereRaw('MBRContains(`border`, ST_GeomFromText(?))', [$wkt])
|
|
98
|
+
->whereRaw('ST_Contains(`border`, ST_GeomFromText(?))', [$wkt])
|
|
99
|
+
->get();
|
|
100
|
+
|
|
101
|
+
// Merge into a unified collection with a type discriminator
|
|
102
|
+
$currentlyInside = collect()
|
|
103
|
+
->merge($insideZones->map(fn ($z) => ['model' => $z, 'geofence_type' => 'zone']))
|
|
104
|
+
->merge($insideServiceAreas->map(fn ($sa) => ['model' => $sa, 'geofence_type' => 'service_area']));
|
|
105
|
+
|
|
106
|
+
$currentlyInsideUuids = $currentlyInside->pluck('model.uuid')->toArray();
|
|
107
|
+
|
|
108
|
+
// ----------------------------------------------------------------
|
|
109
|
+
// 3. Load the driver's current geofence state records.
|
|
110
|
+
// ----------------------------------------------------------------
|
|
111
|
+
$currentStates = DB::table($stateTable)
|
|
112
|
+
->where($subjectColumn, $subjectUuid)
|
|
113
|
+
->get()
|
|
114
|
+
->keyBy('geofence_uuid');
|
|
115
|
+
|
|
116
|
+
// ----------------------------------------------------------------
|
|
117
|
+
// 4. Detect ENTRIES: geofences the driver is now inside but
|
|
118
|
+
// was not inside before (or has no state record yet).
|
|
119
|
+
// ----------------------------------------------------------------
|
|
120
|
+
foreach ($currentlyInside as $item) {
|
|
121
|
+
$geofence = $item['model'];
|
|
122
|
+
$state = $currentStates->get($geofence->uuid);
|
|
123
|
+
|
|
124
|
+
if (!$state || !$state->is_inside) {
|
|
125
|
+
$crossings[] = [
|
|
126
|
+
'type' => 'entered',
|
|
127
|
+
'geofence' => $geofence,
|
|
128
|
+
'geofence_type' => $item['geofence_type'],
|
|
129
|
+
];
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// ----------------------------------------------------------------
|
|
134
|
+
// 5. Detect EXITS: geofences the driver was inside but is no
|
|
135
|
+
// longer inside (not in the current ST_Contains result set).
|
|
136
|
+
// ----------------------------------------------------------------
|
|
137
|
+
foreach ($currentStates as $geofenceUuid => $state) {
|
|
138
|
+
if ($state->is_inside && !in_array($geofenceUuid, $currentlyInsideUuids)) {
|
|
139
|
+
$geofence = $state->geofence_type === 'service_area'
|
|
140
|
+
? ServiceArea::where('uuid', $geofenceUuid)->first()
|
|
141
|
+
: Zone::where('uuid', $geofenceUuid)->first();
|
|
142
|
+
|
|
143
|
+
if ($geofence) {
|
|
144
|
+
$crossings[] = [
|
|
145
|
+
'type' => 'exited',
|
|
146
|
+
'geofence' => $geofence,
|
|
147
|
+
'geofence_type' => $state->geofence_type,
|
|
148
|
+
];
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return $crossings;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Check if a driver is currently recorded as inside a specific geofence.
|
|
158
|
+
*
|
|
159
|
+
* @param mixed $geofence Zone or ServiceArea
|
|
160
|
+
*/
|
|
161
|
+
public function isDriverInsideGeofence(Driver $driver, $geofence): bool
|
|
162
|
+
{
|
|
163
|
+
return DB::table('driver_geofence_states')
|
|
164
|
+
->where('driver_uuid', $driver->uuid)
|
|
165
|
+
->where('geofence_uuid', $geofence->uuid)
|
|
166
|
+
->where('is_inside', true)
|
|
167
|
+
->exists();
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
public function isVehicleInsideGeofence(Vehicle $vehicle, $geofence): bool
|
|
171
|
+
{
|
|
172
|
+
return DB::table('vehicle_geofence_states')
|
|
173
|
+
->where('vehicle_uuid', $vehicle->uuid)
|
|
174
|
+
->where('geofence_uuid', $geofence->uuid)
|
|
175
|
+
->where('is_inside', true)
|
|
176
|
+
->exists();
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Clear all geofence state records for a driver.
|
|
181
|
+
* Called when a driver goes offline or completes a shift.
|
|
182
|
+
*/
|
|
183
|
+
public function clearDriverState(Driver $driver): void
|
|
184
|
+
{
|
|
185
|
+
DB::table('driver_geofence_states')
|
|
186
|
+
->where('driver_uuid', $driver->uuid)
|
|
187
|
+
->update([
|
|
188
|
+
'is_inside' => false,
|
|
189
|
+
'exited_at' => now(),
|
|
190
|
+
'dwell_job_id' => null,
|
|
191
|
+
'updated_at' => now(),
|
|
192
|
+
]);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
public function clearVehicleState(Vehicle $vehicle): void
|
|
196
|
+
{
|
|
197
|
+
DB::table('vehicle_geofence_states')
|
|
198
|
+
->where('vehicle_uuid', $vehicle->uuid)
|
|
199
|
+
->update([
|
|
200
|
+
'is_inside' => false,
|
|
201
|
+
'exited_at' => now(),
|
|
202
|
+
'dwell_job_id' => null,
|
|
203
|
+
'updated_at' => now(),
|
|
204
|
+
]);
|
|
205
|
+
}
|
|
206
|
+
}
|
package/server/src/routes.php
CHANGED
|
@@ -145,6 +145,13 @@ Route::prefix(config('fleetops.api.routing.prefix', null))->namespace('Fleetbase
|
|
|
145
145
|
$router->put('{id}', 'ServiceAreaController@update');
|
|
146
146
|
$router->delete('{id}', 'ServiceAreaController@delete');
|
|
147
147
|
});
|
|
148
|
+
// geofences routes
|
|
149
|
+
$router->group(['prefix' => 'geofences'], function () use ($router) {
|
|
150
|
+
$router->get('events', 'GeofenceController@events');
|
|
151
|
+
$router->get('inventory', 'GeofenceController@inventory');
|
|
152
|
+
$router->get('dwell-report', 'GeofenceController@dwellReport');
|
|
153
|
+
$router->get('driver/{driverUuid}/history', 'GeofenceController@driverHistory');
|
|
154
|
+
});
|
|
148
155
|
// service-rates routes
|
|
149
156
|
$router->group(['prefix' => 'service-rates'], function () use ($router) {
|
|
150
157
|
$router->post('/', 'ServiceRateController@create');
|
|
@@ -375,6 +382,15 @@ Route::prefix(config('fleetops.api.routing.prefix', null))->namespace('Fleetbase
|
|
|
375
382
|
$router->delete('bulk-delete', $controller('bulkDelete'));
|
|
376
383
|
}
|
|
377
384
|
);
|
|
385
|
+
$router->group(
|
|
386
|
+
['prefix' => 'geofences'],
|
|
387
|
+
function () use ($router) {
|
|
388
|
+
$router->get('events', 'GeofenceController@events');
|
|
389
|
+
$router->get('inventory', 'GeofenceController@inventory');
|
|
390
|
+
$router->get('dwell-report', 'GeofenceController@dwellReport');
|
|
391
|
+
$router->get('driver/{driverUuid}/history', 'GeofenceController@driverHistory');
|
|
392
|
+
}
|
|
393
|
+
);
|
|
378
394
|
$router->fleetbaseRoutes('zones');
|
|
379
395
|
$router->fleetbaseRoutes(
|
|
380
396
|
'service-quotes',
|
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
<Modal::Default @modalIsOpened={{@modalIsOpened}} @options={{@options}} @confirm={{@onConfirm}} @decline={{@onDecline}}>
|
|
2
|
-
<div class="modal-body-container">
|
|
3
|
-
<div class="flex px-3 py-2 mb-6 rounded-md shadow-sm bg-blue-200">
|
|
4
|
-
<div>
|
|
5
|
-
<FaIcon @icon="info-circle" class="text-blue-900 mr-4" />
|
|
6
|
-
</div>
|
|
7
|
-
<p class="flex-1 text-sm text-blue-900 dark:text-blue-900">
|
|
8
|
-
{{t "modals.set-password.message"}}
|
|
9
|
-
</p>
|
|
10
|
-
</div>
|
|
11
|
-
|
|
12
|
-
<InputGroup
|
|
13
|
-
@name={{t "modals.set-password.new-password"}}
|
|
14
|
-
@value={{@options.password}}
|
|
15
|
-
@type="password"
|
|
16
|
-
@inputClass="form-input-lg"
|
|
17
|
-
@helpText={{t "modals.set-password.help-text"}}
|
|
18
|
-
/>
|
|
19
|
-
<InputGroup
|
|
20
|
-
@name={{t "modals.set-password.confirm-password"}}
|
|
21
|
-
@value={{@options.password_confirmation}}
|
|
22
|
-
@type="password"
|
|
23
|
-
@inputClass="form-input-lg"
|
|
24
|
-
@helpText={{t "modals.set-password.help-text"}}
|
|
25
|
-
/>
|
|
26
|
-
</div>
|
|
27
|
-
</Modal::Default>
|