@fleetbase/fleetops-engine 0.6.46 → 0.6.48
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/admin/routing-settings.hbs +71 -0
- package/addon/components/admin/routing-settings.js +124 -0
- package/addon/components/customer/orders.js +1 -1
- package/addon/components/display-place.hbs +1 -1
- package/addon/components/map/order-list-overlay/order.hbs +18 -3
- package/addon/components/map/toolbar.hbs +2 -2
- package/addon/components/map/toolbar.js +10 -2
- package/addon/components/orchestrator/phase-builder.hbs +28 -5
- package/addon/components/orchestrator/phase-builder.js +21 -1
- package/addon/components/orchestrator/resource-panel.hbs +19 -1
- package/addon/components/orchestrator/resource-panel.js +4 -0
- package/addon/components/orchestrator-workbench.js +3 -1
- package/addon/components/order/details/purchase-rate.hbs +19 -22
- package/addon/components/order/details/route.hbs +26 -1
- package/addon/components/order/details/route.js +50 -0
- package/addon/components/order/details/tracking.hbs +171 -27
- package/addon/components/order/details/tracking.js +457 -0
- package/addon/components/order/form/details.hbs +1 -2
- package/addon/components/order/form/details.js +6 -0
- package/addon/components/order/form/route.js +9 -1
- package/addon/components/order/kanban-card.hbs +18 -3
- package/addon/components/order-list-overlay/order.hbs +18 -3
- package/addon/components/order-progress-card.hbs +21 -6
- package/addon/components/order-tracking-lookup.hbs +7 -7
- package/addon/components/order-tracking-lookup.js +1 -1
- package/addon/components/route-list.hbs +78 -8
- package/addon/components/route-list.js +48 -6
- package/addon/components/tracking-stop-progress.hbs +45 -0
- package/addon/components/tracking-stop-progress.js +79 -0
- package/addon/controllers/settings/payments/index.js +1 -1
- package/addon/controllers/settings/routing.js +98 -1
- package/addon/helpers/format-duration.js +38 -0
- package/addon/routes/application.js +6 -0
- package/addon/services/order-list-overlay.js +2 -2
- package/addon/styles/fleetops-engine.css +733 -0
- package/addon/templates/settings/routing.hbs +60 -2
- package/app/components/tracking-stop-progress.js +1 -0
- package/app/helpers/format-duration.js +1 -0
- package/composer.json +1 -1
- package/extension.json +1 -1
- package/package.json +3 -3
- package/pnpm-workspace.yaml +7 -0
- package/server/config/fleetops.php +32 -11
- package/server/migrations/2026_05_13_000001_repair_customer_contact_user_types.php +86 -0
- package/server/seeders/VroomOrchestrationSeeder.php +402 -0
- package/server/src/Http/Controllers/Api/v1/OrchestrationController.php +64 -0
- package/server/src/Http/Controllers/Api/v1/OrderController.php +26 -26
- package/server/src/Http/Controllers/Api/v1/PayloadController.php +10 -10
- package/server/src/Http/Controllers/Api/v1/ServiceQuoteController.php +1 -1
- package/server/src/Http/Controllers/Internal/v1/ContactController.php +41 -0
- package/server/src/Http/Controllers/Internal/v1/LiveController.php +6 -42
- package/server/src/Http/Controllers/Internal/v1/OrchestrationController.php +1 -1
- package/server/src/Http/Controllers/Internal/v1/OrderController.php +69 -15
- package/server/src/Http/Controllers/Internal/v1/ServiceQuoteController.php +1 -1
- package/server/src/Http/Controllers/Internal/v1/SettingController.php +119 -12
- package/server/src/Http/Resources/v1/Index/Payload.php +2 -2
- package/server/src/Models/Contact.php +122 -13
- package/server/src/Models/Order.php +1 -1
- package/server/src/Models/Payload.php +8 -8
- package/server/src/Models/Place.php +0 -3
- package/server/src/Models/ServiceRate.php +1 -1
- package/server/src/Observers/ContactObserver.php +8 -0
- package/server/src/Orchestration/Engines/CapacityAllocationEngine.php +220 -0
- package/server/src/Orchestration/Engines/VroomOrchestrationEngine.php +330 -38
- package/server/src/Orchestration/Support/OrchestrationPayloadBuilder.php +259 -51
- package/server/src/Providers/FleetOpsServiceProvider.php +27 -0
- package/server/src/Support/LiveOrderQuery.php +75 -0
- package/server/src/Support/Metrics.php +15 -0
- package/server/src/Support/OSRM.php +11 -6
- package/server/src/Support/OrderTracker.php +7 -582
- package/server/src/Support/Utils.php +1 -1
- package/server/src/Tracking/Contracts/TrackingProviderInterface.php +19 -0
- package/server/src/Tracking/Providers/CalculatedTrackingProvider.php +65 -0
- package/server/src/Tracking/Providers/GoogleRoutesTrackingProvider.php +118 -0
- package/server/src/Tracking/Providers/OsrmTrackingProvider.php +71 -0
- package/server/src/Tracking/README.md +25 -0
- package/server/src/Tracking/Support/FakeTrackingProvider.php +42 -0
- package/server/src/Tracking/TrackingContext.php +63 -0
- package/server/src/Tracking/TrackingContextBuilder.php +204 -0
- package/server/src/Tracking/TrackingIntelligenceService.php +180 -0
- package/server/src/Tracking/TrackingOptions.php +49 -0
- package/server/src/Tracking/TrackingProviderCapabilities.php +30 -0
- package/server/src/Tracking/TrackingProviderManager.php +89 -0
- package/server/src/Tracking/TrackingProviderRegistry.php +35 -0
- package/server/src/Tracking/TrackingProviderResult.php +20 -0
- package/server/src/Tracking/TrackingStop.php +59 -0
- package/server/src/routes.php +11 -0
- package/server/tests/CapacityAllocationEngineTest.php +108 -0
- package/server/tests/LiveOrderQueryTest.php +34 -0
- package/server/tests/OrchestrationPayloadBuilderTest.php +132 -0
- package/server/tests/OrchestratorConsumableApiTest.php +74 -0
- package/server/tests/PingDriverEndpointTest.php +19 -0
- package/server/tests/TrackingIntelligenceTest.php +176 -0
- package/server/tests/VroomOrchestrationEngineTest.php +159 -0
- package/translations/en-us.yaml +4 -0
|
@@ -2,598 +2,23 @@
|
|
|
2
2
|
|
|
3
3
|
namespace Fleetbase\FleetOps\Support;
|
|
4
4
|
|
|
5
|
-
use Fleetbase\FleetOps\Models\Driver;
|
|
6
5
|
use Fleetbase\FleetOps\Models\Order;
|
|
7
|
-
use Fleetbase\FleetOps\
|
|
8
|
-
use Fleetbase\FleetOps\
|
|
9
|
-
use Fleetbase\FleetOps\Models\Waypoint;
|
|
10
|
-
use Fleetbase\LaravelMysqlSpatial\Types\Point;
|
|
11
|
-
use Illuminate\Support\Arr;
|
|
12
|
-
use Illuminate\Support\Carbon;
|
|
13
|
-
use Illuminate\Support\Collection;
|
|
14
|
-
use Illuminate\Support\Facades\Cache;
|
|
15
|
-
use Illuminate\Support\Facades\Log;
|
|
16
|
-
use Illuminate\Support\Str;
|
|
6
|
+
use Fleetbase\FleetOps\Tracking\TrackingIntelligenceService;
|
|
7
|
+
use Fleetbase\FleetOps\Tracking\TrackingOptions;
|
|
17
8
|
|
|
18
|
-
/**
|
|
19
|
-
* Class OrderTracker.
|
|
20
|
-
*
|
|
21
|
-
* Provides functionality to track an order's progress, calculate ETA, and retrieve various order-related data.
|
|
22
|
-
*/
|
|
23
9
|
class OrderTracker
|
|
24
10
|
{
|
|
25
|
-
|
|
26
|
-
protected Order $order;
|
|
27
|
-
|
|
28
|
-
/** @var Payload The payload associated with the order */
|
|
29
|
-
protected Payload $payload;
|
|
30
|
-
|
|
31
|
-
/** @var Driver The driver assigned to the order */
|
|
32
|
-
protected ?Driver $driver;
|
|
33
|
-
|
|
34
|
-
/** @var bool Flag to indicate if the order has multiple dropoff points */
|
|
35
|
-
protected bool $isMultipleDropOrder = false;
|
|
36
|
-
|
|
37
|
-
/**
|
|
38
|
-
* Constructor for OrderTracker.
|
|
39
|
-
*
|
|
40
|
-
* @param Order $order the order to track
|
|
41
|
-
*/
|
|
42
|
-
public function __construct(Order $order)
|
|
43
|
-
{
|
|
44
|
-
$order->loadAssignedDriver();
|
|
45
|
-
|
|
46
|
-
$this->order = $order;
|
|
47
|
-
$this->payload = $order->getPayload(function ($payload) {
|
|
48
|
-
$payload->loadMissing(['pickup', 'dropoff', 'waypoints', 'waypointMarkers']);
|
|
49
|
-
});
|
|
50
|
-
$this->isMultipleDropOrder = $this->payload->isMultipleDropOrder;
|
|
51
|
-
$this->driver = $order->driverAssigned;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
/**
|
|
55
|
-
* Get the current location of the driver or starting point.
|
|
56
|
-
*
|
|
57
|
-
* @return Point|null the current location of the driver, or null if unavailable
|
|
58
|
-
*/
|
|
59
|
-
public function getDriverCurrentLocation(): ?Point
|
|
60
|
-
{
|
|
61
|
-
if ($this->driver) {
|
|
62
|
-
return $this->driver->location;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
$startingPoint = $this->payload->getPickupOrCurrentWaypoint();
|
|
66
|
-
if ($startingPoint) {
|
|
67
|
-
return $startingPoint->location;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
return null;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
/**
|
|
74
|
-
* Get the percentage progress of the order.
|
|
75
|
-
*
|
|
76
|
-
* @return int|float the percentage of the order progress
|
|
77
|
-
*/
|
|
78
|
-
public function getOrderProgressPercentage(): int|float
|
|
79
|
-
{
|
|
80
|
-
$totalDistance = $this->getTotalDistance();
|
|
81
|
-
$completedDistance = $this->getCompletedDistance();
|
|
82
|
-
$cannotUseDistance = $totalDistance == -1 || $completedDistance == -1 || $completedDistance === 0;
|
|
83
|
-
|
|
84
|
-
// Get order percentage by activity if distance-based progress is not available
|
|
85
|
-
if ($cannotUseDistance) {
|
|
86
|
-
/** @var Collection $activities */
|
|
87
|
-
$activities = $this->order->orderConfig ? $this->order->orderConfig->activities() : collect();
|
|
88
|
-
$totalActivity = $activities->count();
|
|
89
|
-
if ($totalActivity === 0) {
|
|
90
|
-
return 100; // No activities, so treat it as 100% complete
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
// Calculate completed activities
|
|
94
|
-
$completedActivity = $activities->filter(function ($activity) {
|
|
95
|
-
return $activity->isCompleted($this->order);
|
|
96
|
-
})->count();
|
|
97
|
-
|
|
98
|
-
// Return progress percentage based on completed activities
|
|
99
|
-
return round(($completedActivity / $totalActivity) * 100, 2);
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
if ($totalDistance === 0) {
|
|
103
|
-
return 100;
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
return round(($completedDistance / $totalDistance) * 100, 2);
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
/**
|
|
110
|
-
* Get the total distance for the order.
|
|
111
|
-
*
|
|
112
|
-
* @return int|float the total distance of the order in meters
|
|
113
|
-
*/
|
|
114
|
-
public function getTotalDistance(): int|float
|
|
115
|
-
{
|
|
116
|
-
$points = $this->getAllDestinationPoints()->toArray();
|
|
117
|
-
if (count($points) < 2) {
|
|
118
|
-
return -1;
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
try {
|
|
122
|
-
$response = OSRM::getRouteFromPoints($points);
|
|
123
|
-
if (isset($response['code']) && $response['code'] === 'Ok') {
|
|
124
|
-
$route = Arr::first($response['routes']);
|
|
125
|
-
if ($route) {
|
|
126
|
-
return data_get($route, 'distance', 0);
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
} catch (\Exception $e) {
|
|
130
|
-
Log::warning('Error loading OSRM routes from order points.', [$e]);
|
|
131
|
-
|
|
132
|
-
return -1;
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
return -1;
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
/**
|
|
139
|
-
* Get the completed distance by the driver.
|
|
140
|
-
*
|
|
141
|
-
* @return float the completed distance in meters
|
|
142
|
-
*/
|
|
143
|
-
public function getCompletedDistance(): float
|
|
144
|
-
{
|
|
145
|
-
$points = $this->getCompletedDestinationPoints()->toArray();
|
|
146
|
-
if (count($points) < 2) {
|
|
147
|
-
return -1;
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
try {
|
|
151
|
-
$response = OSRM::getRouteFromPoints($points);
|
|
152
|
-
if (isset($response['code']) && $response['code'] === 'Ok') {
|
|
153
|
-
$route = Arr::first($response['routes']);
|
|
154
|
-
if ($route) {
|
|
155
|
-
return data_get($route, 'distance', 0);
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
} catch (\Exception $e) {
|
|
159
|
-
Log::warning('Error loading OSRM routes from order points.', [$e]);
|
|
160
|
-
|
|
161
|
-
return -1;
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
return -1;
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
/**
|
|
168
|
-
* Get the estimated time of arrival (ETA) for the current destination.
|
|
169
|
-
*
|
|
170
|
-
* @return float the ETA in seconds or -1 if unable to calculate
|
|
171
|
-
*/
|
|
172
|
-
public function getCurrentDestinationETA(): float
|
|
173
|
-
{
|
|
174
|
-
$start = $this->getDriverCurrentLocation();
|
|
175
|
-
$currentDestination = $this->getCurrentDestination();
|
|
176
|
-
$end = $currentDestination ? $currentDestination->location : null;
|
|
177
|
-
if ($start == $end) {
|
|
178
|
-
$nextDestination = $this->getNextDestination();
|
|
179
|
-
if ($nextDestination) {
|
|
180
|
-
$end = $nextDestination->location;
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
if (!$start || !$end) {
|
|
185
|
-
return -1;
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
// Convert SpatialExpression to Point objects
|
|
189
|
-
$start = Utils::getPointFromMixed($start);
|
|
190
|
-
$end = Utils::getPointFromMixed($end);
|
|
191
|
-
|
|
192
|
-
if (!$start || !$end) {
|
|
193
|
-
return -1;
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
try {
|
|
197
|
-
$response = OSRM::getRoute($start, $end);
|
|
198
|
-
if (isset($response['code']) && $response['code'] === 'Ok') {
|
|
199
|
-
$route = Arr::first($response['routes']);
|
|
200
|
-
if ($route) {
|
|
201
|
-
return data_get($route, 'duration', -1);
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
} catch (\Exception $e) {
|
|
205
|
-
Log::warning('Error loading OSRM route from start and end points.', [$e]);
|
|
206
|
-
|
|
207
|
-
return -1;
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
return -1;
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
public function getWaypointETA(Waypoint|Place $waypoint): float
|
|
214
|
-
{
|
|
215
|
-
if ($waypoint instanceof Waypoint) {
|
|
216
|
-
$waypoint->loadMissing('place');
|
|
217
|
-
$waypoint = $waypoint->place;
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
$start = $this->getDriverCurrentLocation();
|
|
221
|
-
$end = $waypoint->location;
|
|
222
|
-
|
|
223
|
-
// Convert SpatialExpression to Point objects
|
|
224
|
-
$start = Utils::getPointFromMixed($start);
|
|
225
|
-
$end = Utils::getPointFromMixed($end);
|
|
226
|
-
|
|
227
|
-
if (!$start || !$end) {
|
|
228
|
-
return -1;
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
try {
|
|
232
|
-
$response = OSRM::getRoute($start, $end);
|
|
233
|
-
if (isset($response['code']) && $response['code'] === 'Ok') {
|
|
234
|
-
$route = Arr::first($response['routes']);
|
|
235
|
-
if ($route) {
|
|
236
|
-
return data_get($route, 'duration', -1);
|
|
237
|
-
}
|
|
238
|
-
}
|
|
239
|
-
} catch (\Exception $e) {
|
|
240
|
-
Log::warning('Error loading OSRM routes from start and end points.', [$e]);
|
|
241
|
-
|
|
242
|
-
return -1;
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
return -1;
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
/**
|
|
249
|
-
* Get the estimated time of arrival (ETA) for the completion of the order.
|
|
250
|
-
*
|
|
251
|
-
* @return float the ETA in seconds or -1 if unable to calculate
|
|
252
|
-
*/
|
|
253
|
-
public function getCompletionETA(): float
|
|
254
|
-
{
|
|
255
|
-
$start = $this->getDriverCurrentLocation();
|
|
256
|
-
$end = $this->payload->getDropoffOrLastWaypoint()->location;
|
|
257
|
-
|
|
258
|
-
// Convert SpatialExpression to Point objects
|
|
259
|
-
$start = Utils::getPointFromMixed($start);
|
|
260
|
-
$end = Utils::getPointFromMixed($end);
|
|
261
|
-
|
|
262
|
-
if (!$start || !$end) {
|
|
263
|
-
return -1;
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
try {
|
|
267
|
-
$response = OSRM::getRoute($start, $end);
|
|
268
|
-
if (isset($response['code']) && $response['code'] === 'Ok') {
|
|
269
|
-
$route = Arr::first($response['routes']);
|
|
270
|
-
if ($route) {
|
|
271
|
-
return data_get($route, 'duration', -1);
|
|
272
|
-
}
|
|
273
|
-
}
|
|
274
|
-
} catch (\Exception $e) {
|
|
275
|
-
Log::warning('Error loading OSRM routes from start and end points.', [$e]);
|
|
276
|
-
|
|
277
|
-
return -1;
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
return -1;
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
/**
|
|
284
|
-
* Get the estimated completion time of the order.
|
|
285
|
-
*
|
|
286
|
-
* @return Carbon the estimated completion time
|
|
287
|
-
*/
|
|
288
|
-
public function getEstimatedCompletionTime(): ?Carbon
|
|
289
|
-
{
|
|
290
|
-
$currentEtaCompletion = $this->getCompletionETA();
|
|
291
|
-
|
|
292
|
-
return now()->addSeconds($currentEtaCompletion);
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
/**
|
|
296
|
-
* Get the start time of the order.
|
|
297
|
-
*
|
|
298
|
-
* @return Carbon the time the order started
|
|
299
|
-
*/
|
|
300
|
-
public function getOrderStartTime(): ?Carbon
|
|
11
|
+
public function __construct(protected Order $order)
|
|
301
12
|
{
|
|
302
|
-
return $this->order->started_at;
|
|
303
13
|
}
|
|
304
14
|
|
|
305
|
-
|
|
306
|
-
* Get the completion time of the order, if completed.
|
|
307
|
-
*
|
|
308
|
-
* @return Carbon|null the time the order was completed, or null if not completed
|
|
309
|
-
*/
|
|
310
|
-
public function getOrderCompletionTime(): ?Carbon
|
|
15
|
+
public function eta(array $options = []): array
|
|
311
16
|
{
|
|
312
|
-
return $this->order
|
|
17
|
+
return app(TrackingIntelligenceService::class)->eta($this->order, TrackingOptions::fromArray($options));
|
|
313
18
|
}
|
|
314
19
|
|
|
315
|
-
|
|
316
|
-
* Get all destinations (pickup, waypoints, dropoff) for the order.
|
|
317
|
-
*
|
|
318
|
-
* @return Collection a collection of all destination places
|
|
319
|
-
*/
|
|
320
|
-
public function getAllDestinations(): Collection
|
|
20
|
+
public function toArray(array $options = []): array
|
|
321
21
|
{
|
|
322
|
-
return
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
/**
|
|
326
|
-
* Get all destination points (pickup, waypoints, dropoff) as spatial points.
|
|
327
|
-
*
|
|
328
|
-
* @return Collection a collection of Point objects for each destination
|
|
329
|
-
*/
|
|
330
|
-
public function getAllDestinationPoints(): Collection
|
|
331
|
-
{
|
|
332
|
-
return $this->getAllDestinations()->map(function (Place $place) {
|
|
333
|
-
return $place->location;
|
|
334
|
-
})->filter()->values();
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
/**
|
|
338
|
-
* Get the current destination the driver is heading to.
|
|
339
|
-
*
|
|
340
|
-
* @return Place|null the current destination place object, or null if not available
|
|
341
|
-
*/
|
|
342
|
-
public function getCurrentDestination(): ?Place
|
|
343
|
-
{
|
|
344
|
-
if ($this->isMultipleDropOrder) {
|
|
345
|
-
if ($this->payload->waypoints) {
|
|
346
|
-
$destination = null;
|
|
347
|
-
|
|
348
|
-
if (Str::isUuid($this->payload->current_waypoint_uuid)) {
|
|
349
|
-
$destination = $this->payload->waypoints->firstWhere('uuid', $this->payload->current_waypoint_uuid);
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
if (!$destination) {
|
|
353
|
-
$destination = $this->payload->waypoints->first();
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
return $destination;
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
return null;
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
if ($this->order->status === 'created' || $this->order->status === 'dispatched' || $this->order->status === 'pending') {
|
|
363
|
-
return $this->payload->pickup;
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
return $this->payload->dropoff;
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
/**
|
|
370
|
-
* Get the completed destinations of the order.
|
|
371
|
-
*
|
|
372
|
-
* @return Collection a collection of completed destination places
|
|
373
|
-
*/
|
|
374
|
-
public function getCompletedDestinations(): Collection
|
|
375
|
-
{
|
|
376
|
-
if ($this->isMultipleDropOrder) {
|
|
377
|
-
return $this->payload->waypointMarkers->filter(
|
|
378
|
-
function (Waypoint $waypoint) {
|
|
379
|
-
return $this->isWaypointCompleted($waypoint);
|
|
380
|
-
}
|
|
381
|
-
)->map(
|
|
382
|
-
function (Waypoint $waypoint) {
|
|
383
|
-
$waypoint->loadMissing('place');
|
|
384
|
-
|
|
385
|
-
return $waypoint->place;
|
|
386
|
-
}
|
|
387
|
-
);
|
|
388
|
-
}
|
|
389
|
-
|
|
390
|
-
if ($this->order->status === 'created' || $this->order->status === 'dispatched' || $this->order->status === 'pending') {
|
|
391
|
-
return collect();
|
|
392
|
-
}
|
|
393
|
-
|
|
394
|
-
if ($this->order->status === 'completed') {
|
|
395
|
-
return collect([$this->getPickup(), $this->getDropoff()]);
|
|
396
|
-
}
|
|
397
|
-
|
|
398
|
-
return collect([$this->getPickup()]);
|
|
399
|
-
}
|
|
400
|
-
|
|
401
|
-
/**
|
|
402
|
-
* Get the completed destination points as spatial points.
|
|
403
|
-
*
|
|
404
|
-
* @return Collection a collection of Point objects representing the completed destination points
|
|
405
|
-
*/
|
|
406
|
-
public function getCompletedDestinationPoints(): Collection
|
|
407
|
-
{
|
|
408
|
-
return $this->getCompletedDestinations()->map(function (Place $place) {
|
|
409
|
-
return $place->location;
|
|
410
|
-
})->filter()->values();
|
|
411
|
-
}
|
|
412
|
-
|
|
413
|
-
/**
|
|
414
|
-
* Get the next destination that the driver should go to.
|
|
415
|
-
*
|
|
416
|
-
* @return Place|null the next destination place, or null if not available
|
|
417
|
-
*/
|
|
418
|
-
public function getNextDestination(): ?Place
|
|
419
|
-
{
|
|
420
|
-
if ($this->isMultipleDropOrder) {
|
|
421
|
-
/** @var Waypoint $nextWaypoint */
|
|
422
|
-
$nextWaypoint = $this->payload->waypointMarkers->filter(function (Waypoint $waypoint) {
|
|
423
|
-
return $this->isWaypointNotCompleted($waypoint) && $this->isWaypointNotCurrentDestination($waypoint);
|
|
424
|
-
})->first();
|
|
425
|
-
|
|
426
|
-
if ($nextWaypoint) {
|
|
427
|
-
$nextWaypoint->loadMissing('place');
|
|
428
|
-
|
|
429
|
-
return $nextWaypoint->place;
|
|
430
|
-
}
|
|
431
|
-
}
|
|
432
|
-
|
|
433
|
-
return $this->payload->dropoff;
|
|
434
|
-
}
|
|
435
|
-
|
|
436
|
-
/**
|
|
437
|
-
* Check if a waypoint has been completed.
|
|
438
|
-
*
|
|
439
|
-
* @param Waypoint $waypoint the waypoint to check
|
|
440
|
-
*
|
|
441
|
-
* @return bool true if the waypoint is completed or canceled, otherwise false
|
|
442
|
-
*/
|
|
443
|
-
public function isWaypointCompleted(Waypoint $waypoint): bool
|
|
444
|
-
{
|
|
445
|
-
$waypoint->loadMissing(['trackingNumber', 'trackingNumber.status']);
|
|
446
|
-
$status = strtolower($waypoint->status_code);
|
|
447
|
-
|
|
448
|
-
return $status === 'completed' || $status === 'canceled';
|
|
449
|
-
}
|
|
450
|
-
|
|
451
|
-
/**
|
|
452
|
-
* Check if a waypoint is not completed.
|
|
453
|
-
*
|
|
454
|
-
* @param Waypoint $waypoint the waypoint to check
|
|
455
|
-
*
|
|
456
|
-
* @return bool true if the waypoint is not completed, otherwise false
|
|
457
|
-
*/
|
|
458
|
-
public function isWaypointNotCompleted(Waypoint $waypoint): bool
|
|
459
|
-
{
|
|
460
|
-
return !$this->isWaypointCompleted($waypoint);
|
|
461
|
-
}
|
|
462
|
-
|
|
463
|
-
/**
|
|
464
|
-
* Check if a waypoint is the current destination.
|
|
465
|
-
*
|
|
466
|
-
* @param Waypoint $waypoint the waypoint to check
|
|
467
|
-
*
|
|
468
|
-
* @return bool true if the waypoint is the current destination, otherwise false
|
|
469
|
-
*/
|
|
470
|
-
public function isWaypointCurrentDestination(Waypoint $waypoint): bool
|
|
471
|
-
{
|
|
472
|
-
if (!$this->payload->current_waypoint_uuid) {
|
|
473
|
-
$currentDestination = $this->getCurrentDestination();
|
|
474
|
-
|
|
475
|
-
return $currentDestination->uuid === $waypoint->place_uuid;
|
|
476
|
-
}
|
|
477
|
-
|
|
478
|
-
return $waypoint->place_uuid === $this->payload->current_waypoint_uuid;
|
|
479
|
-
}
|
|
480
|
-
|
|
481
|
-
/**
|
|
482
|
-
* Check if a waypoint is not the current destination.
|
|
483
|
-
*
|
|
484
|
-
* @param Waypoint $waypoint the waypoint to check
|
|
485
|
-
*
|
|
486
|
-
* @return bool true if the waypoint is not the current destination, otherwise false
|
|
487
|
-
*/
|
|
488
|
-
public function isWaypointNotCurrentDestination(Waypoint $waypoint): bool
|
|
489
|
-
{
|
|
490
|
-
return !$this->isWaypointCurrentDestination($waypoint);
|
|
491
|
-
}
|
|
492
|
-
|
|
493
|
-
/**
|
|
494
|
-
* Get the pickup location of the order.
|
|
495
|
-
*
|
|
496
|
-
* @return Place the pickup place of the order
|
|
497
|
-
*/
|
|
498
|
-
public function getPickup(): ?Place
|
|
499
|
-
{
|
|
500
|
-
return $this->payload->pickup;
|
|
501
|
-
}
|
|
502
|
-
|
|
503
|
-
/**
|
|
504
|
-
* Get the dropoff location of the order.
|
|
505
|
-
*
|
|
506
|
-
* @return Place the dropoff place of the order
|
|
507
|
-
*/
|
|
508
|
-
public function getDropoff(): ?Place
|
|
509
|
-
{
|
|
510
|
-
return $this->payload->dropoff;
|
|
511
|
-
}
|
|
512
|
-
|
|
513
|
-
public function eta(): array
|
|
514
|
-
{
|
|
515
|
-
// Generate cache key based on order UUID and updated_at timestamp
|
|
516
|
-
$cacheKey = 'order_eta:' . $this->order->uuid . ':' . optional($this->order->updated_at)->timestamp;
|
|
517
|
-
|
|
518
|
-
// Return cached data if available
|
|
519
|
-
return Cache::remember($cacheKey, 60, function () {
|
|
520
|
-
// Load missing waypoints and places
|
|
521
|
-
$waypoints = $this->payload->getAllStops();
|
|
522
|
-
|
|
523
|
-
// ETA's
|
|
524
|
-
$eta = [];
|
|
525
|
-
foreach ($waypoints as $waypoint) {
|
|
526
|
-
$eta[$waypoint->uuid] = $this->getWaypointETA($waypoint);
|
|
527
|
-
}
|
|
528
|
-
|
|
529
|
-
return $eta;
|
|
530
|
-
});
|
|
531
|
-
}
|
|
532
|
-
|
|
533
|
-
/**
|
|
534
|
-
* Get all key tracker information as an array.
|
|
535
|
-
* Cached for 60 seconds to avoid repeated OSRM calls.
|
|
536
|
-
*/
|
|
537
|
-
public function toArray(): array
|
|
538
|
-
{
|
|
539
|
-
// Generate cache key based on order UUID and updated_at timestamp
|
|
540
|
-
$cacheKey = 'order_tracker:' . $this->order->uuid . ':' . optional($this->order->updated_at)->timestamp;
|
|
541
|
-
|
|
542
|
-
// Return cached data if available
|
|
543
|
-
return Cache::remember($cacheKey, 60, function () {
|
|
544
|
-
// Early return for completed orders - skip expensive OSRM calls
|
|
545
|
-
if (in_array($this->order->status, ['completed', 'canceled'])) {
|
|
546
|
-
return [
|
|
547
|
-
'driver_current_location' => null,
|
|
548
|
-
'progress_percentage' => 100,
|
|
549
|
-
'total_distance' => 0,
|
|
550
|
-
'completed_distance' => 0,
|
|
551
|
-
'current_destination_eta' => 0,
|
|
552
|
-
'completion_eta' => 0,
|
|
553
|
-
'estimated_completion_time' => null,
|
|
554
|
-
'estimated_completion_time_formatted' => null,
|
|
555
|
-
'start_time' => $this->getOrderStartTime(),
|
|
556
|
-
'completion_time' => $this->getOrderCompletionTime(),
|
|
557
|
-
'current_destination' => null,
|
|
558
|
-
'next_destination' => null,
|
|
559
|
-
'first_waypoint_completed' => true,
|
|
560
|
-
'last_waypoint_completed' => true,
|
|
561
|
-
];
|
|
562
|
-
}
|
|
563
|
-
|
|
564
|
-
// Wrap OSRM-dependent calculations in try-catch for graceful degradation
|
|
565
|
-
try {
|
|
566
|
-
$totalDistance = $this->getTotalDistance();
|
|
567
|
-
$completedDistance = $this->getCompletedDistance();
|
|
568
|
-
$currentDestinationEta = $this->getCurrentDestinationETA();
|
|
569
|
-
$completionEta = $this->getCompletionETA();
|
|
570
|
-
} catch (\Exception $e) {
|
|
571
|
-
Log::warning('OrderTracker: Failed to calculate distances/ETAs', ['error' => $e->getMessage()]);
|
|
572
|
-
$totalDistance = 0;
|
|
573
|
-
$completedDistance = 0;
|
|
574
|
-
$currentDestinationEta = 0;
|
|
575
|
-
$completionEta = 0;
|
|
576
|
-
}
|
|
577
|
-
|
|
578
|
-
$estimatedCompletionTime = $this->getEstimatedCompletionTime();
|
|
579
|
-
$orderProgressPercentage = $this->getOrderProgressPercentage();
|
|
580
|
-
|
|
581
|
-
return [
|
|
582
|
-
'driver_current_location' => $this->getDriverCurrentLocation(),
|
|
583
|
-
'progress_percentage' => $orderProgressPercentage,
|
|
584
|
-
'total_distance' => $totalDistance,
|
|
585
|
-
'completed_distance' => $completedDistance,
|
|
586
|
-
'current_destination_eta' => $currentDestinationEta,
|
|
587
|
-
'completion_eta' => $completionEta,
|
|
588
|
-
'estimated_completion_time' => $estimatedCompletionTime,
|
|
589
|
-
'estimated_completion_time_formatted' => $estimatedCompletionTime instanceof Carbon ? $estimatedCompletionTime->format('M jS, Y H:i') : null,
|
|
590
|
-
'start_time' => $this->getOrderStartTime(),
|
|
591
|
-
'completion_time' => $this->getOrderCompletionTime(),
|
|
592
|
-
'current_destination' => $this->getCurrentDestination(),
|
|
593
|
-
'next_destination' => $this->getNextDestination(),
|
|
594
|
-
'first_waypoint_completed' => $orderProgressPercentage > 10,
|
|
595
|
-
'last_waypoint_completed' => $orderProgressPercentage === 100 || $this->order->status === 'completed',
|
|
596
|
-
];
|
|
597
|
-
});
|
|
22
|
+
return app(TrackingIntelligenceService::class)->track($this->order, TrackingOptions::fromArray($options));
|
|
598
23
|
}
|
|
599
24
|
}
|
|
@@ -392,7 +392,7 @@ class Utils extends FleetbaseUtils
|
|
|
392
392
|
return null;
|
|
393
393
|
}
|
|
394
394
|
|
|
395
|
-
if (preg_match(
|
|
395
|
+
if (preg_match('/POINT\\(\\s*([-+]?\\d*\\.?\\d+)\\s+([-+]?\\d*\\.?\\d+)\\s*\\)/i', $expressionValue, $matches)) {
|
|
396
396
|
return sprintf('POINT(%s %s)', $matches[1], $matches[2]);
|
|
397
397
|
}
|
|
398
398
|
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
<?php
|
|
2
|
+
|
|
3
|
+
namespace Fleetbase\FleetOps\Tracking\Contracts;
|
|
4
|
+
|
|
5
|
+
use Fleetbase\FleetOps\Tracking\TrackingContext;
|
|
6
|
+
use Fleetbase\FleetOps\Tracking\TrackingOptions;
|
|
7
|
+
use Fleetbase\FleetOps\Tracking\TrackingProviderCapabilities;
|
|
8
|
+
use Fleetbase\FleetOps\Tracking\TrackingProviderResult;
|
|
9
|
+
|
|
10
|
+
interface TrackingProviderInterface
|
|
11
|
+
{
|
|
12
|
+
public function key(): string;
|
|
13
|
+
|
|
14
|
+
public function capabilities(): TrackingProviderCapabilities;
|
|
15
|
+
|
|
16
|
+
public function canTrack(TrackingContext $context): bool;
|
|
17
|
+
|
|
18
|
+
public function track(TrackingContext $context, TrackingOptions $options): TrackingProviderResult;
|
|
19
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
<?php
|
|
2
|
+
|
|
3
|
+
namespace Fleetbase\FleetOps\Tracking\Providers;
|
|
4
|
+
|
|
5
|
+
use Fleetbase\FleetOps\Support\Utils;
|
|
6
|
+
use Fleetbase\FleetOps\Tracking\Contracts\TrackingProviderInterface;
|
|
7
|
+
use Fleetbase\FleetOps\Tracking\TrackingContext;
|
|
8
|
+
use Fleetbase\FleetOps\Tracking\TrackingOptions;
|
|
9
|
+
use Fleetbase\FleetOps\Tracking\TrackingProviderCapabilities;
|
|
10
|
+
use Fleetbase\FleetOps\Tracking\TrackingProviderResult;
|
|
11
|
+
|
|
12
|
+
class CalculatedTrackingProvider implements TrackingProviderInterface
|
|
13
|
+
{
|
|
14
|
+
public function key(): string
|
|
15
|
+
{
|
|
16
|
+
return 'calculated';
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
public function capabilities(): TrackingProviderCapabilities
|
|
20
|
+
{
|
|
21
|
+
return new TrackingProviderCapabilities();
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
public function canTrack(TrackingContext $context): bool
|
|
25
|
+
{
|
|
26
|
+
return $context->canRoute();
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
public function track(TrackingContext $context, TrackingOptions $options): TrackingProviderResult
|
|
30
|
+
{
|
|
31
|
+
$points = $context->routePoints();
|
|
32
|
+
$distance = 0;
|
|
33
|
+
$legs = [];
|
|
34
|
+
|
|
35
|
+
for ($i = 0; $i < count($points) - 1; $i++) {
|
|
36
|
+
$legDistance = Utils::vincentyGreatCircleDistance($points[$i], $points[$i + 1]);
|
|
37
|
+
$legDuration = $this->durationFromDistance($legDistance, $options);
|
|
38
|
+
$distance += $legDistance;
|
|
39
|
+
$legs[] = [
|
|
40
|
+
'index' => $i,
|
|
41
|
+
'distance_m' => $legDistance,
|
|
42
|
+
'duration_s' => $legDuration,
|
|
43
|
+
'duration_in_traffic_s' => null,
|
|
44
|
+
'provider' => $this->key(),
|
|
45
|
+
];
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return new TrackingProviderResult(
|
|
49
|
+
provider: $this->key(),
|
|
50
|
+
distanceMeters: $distance,
|
|
51
|
+
durationSeconds: $this->durationFromDistance($distance, $options),
|
|
52
|
+
durationInTrafficSeconds: null,
|
|
53
|
+
legs: $legs,
|
|
54
|
+
warnings: ['calculated_route_used'],
|
|
55
|
+
confidence: 'low'
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
protected function durationFromDistance(float $distanceMeters, TrackingOptions $options): float
|
|
60
|
+
{
|
|
61
|
+
$metersPerSecond = max($options->defaultVehicleSpeedKph, 1) * 1000 / 3600;
|
|
62
|
+
|
|
63
|
+
return round($distanceMeters / $metersPerSecond);
|
|
64
|
+
}
|
|
65
|
+
}
|