@fleetbase/fleetops-engine 0.6.49 → 0.6.50
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/customer/create-order-form.hbs +2 -2
- package/addon/components/customer/form.hbs +2 -2
- package/addon/components/customer/order-form.hbs +2 -2
- package/addon/components/customer/orders.hbs +2 -2
- package/addon/components/display-place.hbs +2 -2
- package/addon/components/map/google-live-map.js +6 -2
- package/addon/components/map/leaflet-live-map.hbs +48 -14
- package/addon/components/map/leaflet-live-map.js +184 -20
- package/addon/components/modals/order-change-destination.hbs +46 -0
- package/addon/components/modals/order-change-destination.js +106 -0
- package/addon/components/modals/stop-activity.hbs +28 -0
- package/addon/components/modals/stop-activity.js +52 -0
- package/addon/components/modals/update-order-activity.hbs +45 -3
- package/addon/components/modals/update-order-activity.js +143 -5
- package/addon/components/orchestrator/resource-panel.hbs +0 -10
- package/addon/components/orchestrator/resource-panel.js +2 -4
- package/addon/components/order/activity-list.hbs +2 -2
- package/addon/components/order/activity-timeline.hbs +31 -29
- package/addon/components/order/details/activity.hbs +3 -3
- package/addon/components/order/details/activity.js +11 -0
- package/addon/components/order/details/documents.js +2 -0
- package/addon/components/order/details/purchase-rate.hbs +2 -2
- package/addon/components/order/details/route.hbs +11 -3
- package/addon/components/order/details/route.js +91 -14
- package/addon/components/order/details/tracking.hbs +101 -72
- package/addon/components/order/details/tracking.js +149 -2
- package/addon/components/order/form/details.hbs +3 -3
- package/addon/components/order/form/documents.js +11 -4
- package/addon/components/order/form/route.hbs +108 -104
- package/addon/components/order/form/route.js +37 -1
- package/addon/components/order/form/service-rate.hbs +2 -2
- package/addon/components/order-config-manager/activity-flow.hbs +4 -1
- package/addon/components/order-config-manager/activity-flow.js +178 -12
- package/addon/components/place/address.js +1 -1
- package/addon/components/route-list.hbs +17 -17
- package/addon/components/route-list.js +42 -10
- package/addon/components/service-rate/details.hbs +41 -0
- package/addon/components/service-rate/form.hbs +118 -0
- package/addon/components/service-rate/form.js +55 -0
- package/addon/components/vehicle/form.js +1 -1
- package/addon/components/vehicle/pill.hbs +1 -1
- package/addon/controllers/management/vehicles/index/new.js +1 -1
- package/addon/controllers/operations/orders/index/details.js +4 -4
- package/addon/controllers/operations/orders/index.js +14 -0
- package/addon/helpers/place-address-html.js +2 -2
- package/addon/routes/operations/service-rates/index.js +1 -1
- package/addon/services/geofence.js +163 -19
- package/addon/services/leaflet-contextmenu-manager.js +27 -0
- package/addon/services/vehicle-actions.js +1 -1
- package/addon/styles/fleetops-engine.css +91 -1
- package/addon/utils/fleet-ops-options.js +2 -1
- package/addon/utils/normalize-order-config-flow.js +41 -0
- package/addon/utils/place-address-html.js +3 -2
- package/addon/utils/route-visualization.js +54 -7
- package/app/components/modals/order-change-destination.js +1 -0
- package/app/components/modals/stop-activity.js +1 -0
- package/composer.json +1 -1
- package/extension.json +1 -1
- package/package.json +3 -3
- package/server/migrations/2026_05_21_000001_add_geographic_rules_to_service_rate_fees_table.php +37 -0
- package/server/migrations/2026_05_25_000001_add_endpoint_tracking_refs_to_payloads_table.php +50 -0
- package/server/migrations/2026_05_27_000001_default_driver_vehicle_statuses_to_available.php +48 -0
- package/server/resources/views/labels/default.php +57 -49
- package/server/resources/views/labels/waypoint-label.php +27 -20
- package/server/seeders/Testing/ConnectivitySeeder.php +1 -0
- package/server/seeders/Testing/FleetSeeder.php +22 -22
- package/server/seeders/Testing/OrdersSeeder.php +6 -22
- package/server/seeders/Testing/VroomOrchestrationSeeder.php +21 -21
- package/server/src/Console/Commands/AuditCustomerUserConflicts.php +75 -0
- package/server/src/Exceptions/CustomerUserConflictException.php +13 -0
- package/server/src/Http/Controllers/Api/v1/ContactController.php +17 -2
- package/server/src/Http/Controllers/Api/v1/CustomerController.php +876 -0
- package/server/src/Http/Controllers/Api/v1/DriverController.php +1 -1
- package/server/src/Http/Controllers/Api/v1/OrderConfigController.php +54 -0
- package/server/src/Http/Controllers/Api/v1/OrderController.php +98 -70
- package/server/src/Http/Controllers/Api/v1/ServiceAreaController.php +2 -2
- package/server/src/Http/Controllers/Api/v1/VehicleController.php +20 -3
- package/server/src/Http/Controllers/Internal/v1/ContactController.php +19 -0
- package/server/src/Http/Controllers/Internal/v1/LiveController.php +101 -66
- package/server/src/Http/Controllers/Internal/v1/OrderController.php +429 -26
- package/server/src/Http/Controllers/Internal/v1/ServiceQuoteController.php +1 -1
- package/server/src/Http/Controllers/Internal/v1/SettingController.php +2 -2
- package/server/src/Http/Controllers/Internal/v1/VehicleController.php +54 -1
- package/server/src/Http/Middleware/AuthenticateCustomerToken.php +34 -0
- package/server/src/Http/Requests/CreateCustomerOrderRequest.php +63 -0
- package/server/src/Http/Requests/CreateCustomerRequest.php +48 -0
- package/server/src/Http/Requests/CreateDriverRequest.php +1 -1
- package/server/src/Http/Requests/CreateVehicleRequest.php +25 -1
- package/server/src/Http/Requests/Internal/CreateDriverRequest.php +1 -1
- package/server/src/Http/Requests/VerifyCreateCustomerRequest.php +29 -0
- package/server/src/Http/Resources/Internal/v1/OrderConfig.php +41 -0
- package/server/src/Http/Resources/v1/Customer.php +79 -0
- package/server/src/Http/Resources/v1/OrderConfig.php +74 -0
- package/server/src/Http/Resources/v1/Payload.php +23 -20
- package/server/src/Http/Resources/v1/ServiceArea.php +33 -19
- package/server/src/Http/Resources/v1/ServiceRateFee.php +7 -0
- package/server/src/Listeners/HandleOrderDispatched.php +2 -2
- package/server/src/Models/Contact.php +92 -15
- package/server/src/Models/Customer.php +64 -0
- package/server/src/Models/Driver.php +12 -3
- package/server/src/Models/Entity.php +2 -2
- package/server/src/Models/Order.php +29 -4
- package/server/src/Models/Payload.php +17 -1
- package/server/src/Models/ServiceRate.php +273 -1
- package/server/src/Models/ServiceRateFee.php +39 -9
- package/server/src/Models/TrackingNumber.php +8 -2
- package/server/src/Models/Vehicle.php +18 -2
- package/server/src/Models/Waypoint.php +2 -2
- package/server/src/Notifications/LateDeparture.php +2 -2
- package/server/src/Notifications/ProlongedStoppage.php +2 -2
- package/server/src/Notifications/RouteDeviation.php +2 -2
- package/server/src/Observers/ContactObserver.php +2 -0
- package/server/src/Observers/ServiceRateObserver.php +6 -6
- package/server/src/Orchestration/Engines/VroomOrchestrationEngine.php +1 -1
- package/server/src/Providers/FleetOpsServiceProvider.php +1 -0
- package/server/src/Support/Analytics/AbstractAnalytics.php +6 -6
- package/server/src/Support/Analytics/FuelEfficiency.php +2 -2
- package/server/src/Support/Analytics/OrdersByStatus.php +1 -1
- package/server/src/Support/Analytics/RevenueTrend.php +0 -1
- package/server/src/Support/Analytics/TopDrivers.php +1 -1
- package/server/src/Support/CustomerAuth.php +98 -0
- package/server/src/Support/LabelPdf.php +52 -0
- package/server/src/Support/Metrics/AbstractMetric.php +4 -4
- package/server/src/Support/ResolvesOrderServiceStops.php +512 -0
- package/server/src/Tracking/TrackingContextBuilder.php +41 -38
- package/server/src/Tracking/TrackingIntelligenceService.php +109 -9
- package/server/src/Tracking/TrackingStop.php +13 -11
- package/server/src/routes.php +43 -9
- package/server/tests/CapacityAllocationEngineTest.php +10 -10
- package/server/tests/CustomerContactIdentitySafetyTest.php +54 -0
- package/server/tests/CustomerEndpointTest.php +199 -0
- package/server/tests/DriverVehicleStatusDefaultTest.php +39 -0
- package/server/tests/LabelPdfRenderingTest.php +23 -0
- package/server/tests/LiveControllerViewportTest.php +60 -0
- package/server/tests/MetricsRegistryTest.php +4 -8
- package/server/tests/OrchestrationPayloadBuilderTest.php +2 -2
- package/server/tests/OrderConfigEndpointTest.php +86 -0
- package/server/tests/OrderFileAttachmentTest.php +20 -0
- package/server/tests/ServiceAreaZoneApiShapeTest.php +34 -0
- package/server/tests/VehicleDriverAssignmentTest.php +18 -0
- package/translations/en-us.yaml +1 -0
- package/server/migrations/2026_05_13_000001_repair_customer_contact_user_types.php +0 -86
|
@@ -50,7 +50,7 @@ class DriverController extends Controller
|
|
|
50
50
|
$input = $request->except(['name', 'password', 'email', 'phone', 'location', 'altitude', 'heading', 'speed', 'meta']);
|
|
51
51
|
|
|
52
52
|
// Add default status
|
|
53
|
-
$input['status'] = '
|
|
53
|
+
$input['status'] = $request->input('status', 'available');
|
|
54
54
|
|
|
55
55
|
// get user details for driver
|
|
56
56
|
$userDetails = $request->only(['name', 'password', 'email', 'phone', 'timezone']);
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
<?php
|
|
2
|
+
|
|
3
|
+
namespace Fleetbase\FleetOps\Http\Controllers\Api\v1;
|
|
4
|
+
|
|
5
|
+
use Fleetbase\FleetOps\Http\Resources\v1\OrderConfig as OrderConfigResource;
|
|
6
|
+
use Fleetbase\FleetOps\Models\OrderConfig;
|
|
7
|
+
use Fleetbase\Http\Controllers\Controller;
|
|
8
|
+
use Illuminate\Database\Eloquent\ModelNotFoundException;
|
|
9
|
+
use Illuminate\Http\Request;
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Public, read-only API surface for OrderConfigs.
|
|
13
|
+
*
|
|
14
|
+
* The internal admin console manages the full OrderConfig lifecycle through
|
|
15
|
+
* `/int/v1/order-configs`. This public endpoint exposes the projected,
|
|
16
|
+
* non-sensitive subset (flow activities + metadata) that consumers like a
|
|
17
|
+
* customer portal need to render status filter chips, activity labels, and
|
|
18
|
+
* other UI driven by the active flow.
|
|
19
|
+
*/
|
|
20
|
+
class OrderConfigController extends Controller
|
|
21
|
+
{
|
|
22
|
+
/**
|
|
23
|
+
* List the OrderConfigs for the company resolved from the API credential.
|
|
24
|
+
*
|
|
25
|
+
* @return \Illuminate\Http\Resources\Json\AnonymousResourceCollection
|
|
26
|
+
*/
|
|
27
|
+
public function query(Request $request)
|
|
28
|
+
{
|
|
29
|
+
$results = OrderConfig::queryWithRequest($request);
|
|
30
|
+
|
|
31
|
+
return OrderConfigResource::collection($results);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Find a single OrderConfig. The `{id}` segment accepts any identifier
|
|
36
|
+
* supported by {@see OrderConfig::resolveFromIdentifier} — `uuid`,
|
|
37
|
+
* `public_id`, `namespace`, or short `key` (e.g. `transport`).
|
|
38
|
+
*
|
|
39
|
+
* @return OrderConfigResource|\Illuminate\Http\JsonResponse
|
|
40
|
+
*/
|
|
41
|
+
public function find(string $id)
|
|
42
|
+
{
|
|
43
|
+
$orderConfig = OrderConfig::resolveFromIdentifier($id);
|
|
44
|
+
if (!$orderConfig) {
|
|
45
|
+
try {
|
|
46
|
+
$orderConfig = OrderConfig::findRecordOrFail($id);
|
|
47
|
+
} catch (ModelNotFoundException $e) {
|
|
48
|
+
return response()->apiError('Order config not found.', 404);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return new OrderConfigResource($orderConfig);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
@@ -5,6 +5,7 @@ namespace Fleetbase\FleetOps\Http\Controllers\Api\v1;
|
|
|
5
5
|
use Fleetbase\FleetOps\Events\OrderDispatchFailed;
|
|
6
6
|
use Fleetbase\FleetOps\Events\OrderReady;
|
|
7
7
|
use Fleetbase\FleetOps\Events\OrderStarted;
|
|
8
|
+
use Fleetbase\FleetOps\Exceptions\CustomerUserConflictException;
|
|
8
9
|
use Fleetbase\FleetOps\Exceptions\UserAlreadyExistsException;
|
|
9
10
|
use Fleetbase\FleetOps\Flow\Activity;
|
|
10
11
|
use Fleetbase\FleetOps\Http\Requests\CreateOrderRequest;
|
|
@@ -23,6 +24,7 @@ use Fleetbase\FleetOps\Models\Place;
|
|
|
23
24
|
use Fleetbase\FleetOps\Models\Proof;
|
|
24
25
|
use Fleetbase\FleetOps\Models\ServiceQuote;
|
|
25
26
|
use Fleetbase\FleetOps\Models\Waypoint;
|
|
27
|
+
use Fleetbase\FleetOps\Support\ResolvesOrderServiceStops;
|
|
26
28
|
use Fleetbase\FleetOps\Support\Utils;
|
|
27
29
|
use Fleetbase\Http\Controllers\Controller;
|
|
28
30
|
use Fleetbase\Http\Resources\Comment as CommentResource;
|
|
@@ -43,6 +45,8 @@ use Illuminate\Validation\ValidationException;
|
|
|
43
45
|
|
|
44
46
|
class OrderController extends Controller
|
|
45
47
|
{
|
|
48
|
+
use ResolvesOrderServiceStops;
|
|
49
|
+
|
|
46
50
|
/**
|
|
47
51
|
* Creates a new Fleetbase Order resource.
|
|
48
52
|
*
|
|
@@ -251,6 +255,13 @@ class OrderController extends Controller
|
|
|
251
255
|
$customer = Arr::only($customer, ['internal_id', 'name', 'title', 'email', 'phone', 'meta']);
|
|
252
256
|
|
|
253
257
|
try {
|
|
258
|
+
$customerCandidate = new Contact([
|
|
259
|
+
...$customer,
|
|
260
|
+
'company_uuid' => session('company'),
|
|
261
|
+
'type' => 'customer',
|
|
262
|
+
]);
|
|
263
|
+
$customerCandidate->assertCustomerIdentityIsAvailable();
|
|
264
|
+
|
|
254
265
|
$customer = Contact::firstOrCreate(
|
|
255
266
|
[
|
|
256
267
|
'company_uuid' => session('company'),
|
|
@@ -263,19 +274,12 @@ class OrderController extends Controller
|
|
|
263
274
|
'type' => 'customer',
|
|
264
275
|
]
|
|
265
276
|
);
|
|
277
|
+
} catch (CustomerUserConflictException $e) {
|
|
278
|
+
return response()->apiError($e->getMessage());
|
|
279
|
+
} catch (UserAlreadyExistsException $e) {
|
|
280
|
+
return response()->apiError($e->getMessage());
|
|
266
281
|
} catch (\Exception $e) {
|
|
267
282
|
return response()->apiError('Failed to find or create customer for order.');
|
|
268
|
-
} catch (UserAlreadyExistsException $e) {
|
|
269
|
-
try {
|
|
270
|
-
// If user already exist then assign user to this customer and the company
|
|
271
|
-
$existingUser = $e->getUser();
|
|
272
|
-
// Assign user to customer
|
|
273
|
-
if ($existingUser && $customer) {
|
|
274
|
-
$customer->assignUser($existingUser);
|
|
275
|
-
}
|
|
276
|
-
} catch (\Exception $e) {
|
|
277
|
-
return response()->apiError('Failed to find or create customer for order.');
|
|
278
|
-
}
|
|
279
283
|
}
|
|
280
284
|
|
|
281
285
|
if (Str::isUuid($customer)) {
|
|
@@ -1043,7 +1047,17 @@ class OrderController extends Controller
|
|
|
1043
1047
|
// if string $id
|
|
1044
1048
|
if (!$order) {
|
|
1045
1049
|
try {
|
|
1046
|
-
$order = Order::findRecordOrFail($id, [
|
|
1050
|
+
$order = Order::findRecordOrFail($id, [
|
|
1051
|
+
'driverAssigned',
|
|
1052
|
+
'payload.entities',
|
|
1053
|
+
'payload.pickup',
|
|
1054
|
+
'payload.dropoff',
|
|
1055
|
+
'payload.return',
|
|
1056
|
+
'payload.currentWaypoint',
|
|
1057
|
+
'payload.waypoints',
|
|
1058
|
+
'payload.waypointMarkers.place',
|
|
1059
|
+
'payload.waypointMarkers.trackingNumber.status',
|
|
1060
|
+
]);
|
|
1047
1061
|
} catch (ModelNotFoundException $exception) {
|
|
1048
1062
|
return response()->apiError('Order resource not found.', 404);
|
|
1049
1063
|
}
|
|
@@ -1094,37 +1108,8 @@ class OrderController extends Controller
|
|
|
1094
1108
|
/** @var \Fleetbase\LaravelMysqlSpatial\Types\Point */
|
|
1095
1109
|
$location = $order->getLastLocation();
|
|
1096
1110
|
|
|
1097
|
-
// Check if multiple waypoint order to update activity for
|
|
1098
|
-
$isMultipleWaypointOrder = (
|
|
1099
|
-
|
|
1100
|
-
// if is multi drop order and no current destination set it
|
|
1101
|
-
if ($isMultipleWaypointOrder && !$order->payload->current_waypoint_uuid) {
|
|
1102
|
-
$order->payload->setFirstWaypoint($activity, $location);
|
|
1103
|
-
}
|
|
1104
|
-
|
|
1105
|
-
// Handle multiple dropoff waypoint activity
|
|
1106
|
-
if (Utils::isActivity($activity) && $activity->completesOrder() && $isMultipleWaypointOrder) {
|
|
1107
|
-
// Check if every waypoint is completed
|
|
1108
|
-
$isCompleted = $order->payload->waypointMarkers->every(function ($waypoint) {
|
|
1109
|
-
return $waypoint->complete;
|
|
1110
|
-
});
|
|
1111
|
-
|
|
1112
|
-
// only update activity for waypoint
|
|
1113
|
-
if (!$isCompleted) {
|
|
1114
|
-
$order->payload->updateWaypointActivity($activity, $location, $proof);
|
|
1115
|
-
$order->payload->setNextWaypointDestination();
|
|
1116
|
-
$order->payload->load('waypointMarkers');
|
|
1117
|
-
|
|
1118
|
-
// recheck if order is completed
|
|
1119
|
-
$isFullyCompleted = $order->payload->waypointMarkers->every(function ($waypoint) {
|
|
1120
|
-
return $waypoint->complete;
|
|
1121
|
-
});
|
|
1122
|
-
|
|
1123
|
-
if (!$isFullyCompleted) {
|
|
1124
|
-
return new OrderResource($order);
|
|
1125
|
-
}
|
|
1126
|
-
}
|
|
1127
|
-
}
|
|
1111
|
+
// Check if multiple waypoint order to update activity for.
|
|
1112
|
+
$isMultipleWaypointOrder = $this->payloadHasWaypoints($order->payload);
|
|
1128
1113
|
|
|
1129
1114
|
// also update for each order entities if not multiple drop order
|
|
1130
1115
|
// all entities will share the same activity status as is one drop order
|
|
@@ -1136,23 +1121,48 @@ class OrderController extends Controller
|
|
|
1136
1121
|
foreach ($order->payload->entities as $entity) {
|
|
1137
1122
|
$entity->insertActivity($activity, $location, $proof);
|
|
1138
1123
|
}
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
$order->
|
|
1124
|
+
// Handle order completion
|
|
1125
|
+
if (Utils::isActivity($activity) && $activity->completesOrder()) {
|
|
1126
|
+
$order->driverAssigned?->unassignCurrentJob();
|
|
1127
|
+
$order->complete($this->resolveProof($proof));
|
|
1143
1128
|
}
|
|
1144
1129
|
|
|
1145
|
-
$order
|
|
1130
|
+
return new OrderResource($order);
|
|
1131
|
+
}
|
|
1132
|
+
|
|
1133
|
+
if ($activity->is('started')) {
|
|
1134
|
+
$order->updateActivity($activity, $proof);
|
|
1135
|
+
|
|
1136
|
+
$this->ensurePayloadCurrentServiceStop($order->payload);
|
|
1137
|
+
if (!$this->payloadHasCurrentServiceStopActivity($order->payload, $activity)) {
|
|
1138
|
+
$this->updateCurrentServiceStopActivity($order, $activity, $location, $proof);
|
|
1139
|
+
}
|
|
1140
|
+
|
|
1141
|
+
return new OrderResource($order->refresh());
|
|
1142
|
+
}
|
|
1143
|
+
|
|
1144
|
+
$currentStop = $this->ensurePayloadCurrentServiceStop($order->payload);
|
|
1145
|
+
$isCompletingCurrentStop = Utils::isActivity($activity) && $activity->complete();
|
|
1146
|
+
|
|
1147
|
+
$this->updateCurrentServiceStopActivity($order, $activity, $location, $proof);
|
|
1148
|
+
|
|
1149
|
+
if (Utils::isActivity($activity) && $activity->is('canceled')) {
|
|
1150
|
+
$order->driverAssigned?->unassignCurrentJob();
|
|
1151
|
+
$order->cancel();
|
|
1152
|
+
|
|
1153
|
+
return new OrderResource($order->refresh());
|
|
1146
1154
|
}
|
|
1147
1155
|
|
|
1148
|
-
// Handle order completion
|
|
1149
1156
|
if (Utils::isActivity($activity) && $activity->completesOrder()) {
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1157
|
+
$nextStop = $isCompletingCurrentStop ? $this->advanceCurrentServiceStopDestination($order, $order->payload) : null;
|
|
1158
|
+
|
|
1159
|
+
if (!$nextStop || !$currentStop) {
|
|
1160
|
+
$order->driverAssigned?->unassignCurrentJob();
|
|
1161
|
+
$order->complete($this->resolveProof($proof));
|
|
1162
|
+
}
|
|
1153
1163
|
}
|
|
1154
1164
|
|
|
1155
|
-
return new OrderResource($order);
|
|
1165
|
+
return new OrderResource($order->refresh());
|
|
1156
1166
|
}
|
|
1157
1167
|
|
|
1158
1168
|
/**
|
|
@@ -1165,7 +1175,14 @@ class OrderController extends Controller
|
|
|
1165
1175
|
$waypointId = $request->input('waypoint');
|
|
1166
1176
|
|
|
1167
1177
|
try {
|
|
1168
|
-
$order = Order::findRecordOrFail($id, [
|
|
1178
|
+
$order = Order::findRecordOrFail($id, [
|
|
1179
|
+
'payload.pickup',
|
|
1180
|
+
'payload.dropoff',
|
|
1181
|
+
'payload.return',
|
|
1182
|
+
'payload.waypoints',
|
|
1183
|
+
'payload.waypointMarkers.place',
|
|
1184
|
+
'payload.waypointMarkers.trackingNumber.status',
|
|
1185
|
+
]);
|
|
1169
1186
|
} catch (ModelNotFoundException $exception) {
|
|
1170
1187
|
return response()->json(
|
|
1171
1188
|
[
|
|
@@ -1175,19 +1192,16 @@ class OrderController extends Controller
|
|
|
1175
1192
|
);
|
|
1176
1193
|
}
|
|
1177
1194
|
|
|
1178
|
-
|
|
1179
|
-
$waypoint = null;
|
|
1195
|
+
$stop = null;
|
|
1180
1196
|
if ($waypointId) {
|
|
1181
|
-
$
|
|
1182
|
-
$query->where('public_id', $waypointId);
|
|
1183
|
-
})->first();
|
|
1197
|
+
$stop = $this->resolveServiceStopFromKey($order->payload, $waypointId);
|
|
1184
1198
|
}
|
|
1185
1199
|
|
|
1186
1200
|
$orderConfig = $order->ensureOrderConfig();
|
|
1187
1201
|
if (!$orderConfig) {
|
|
1188
1202
|
return response()->apiError('No order config found for order.');
|
|
1189
1203
|
}
|
|
1190
|
-
$activities = $orderConfig->nextActivity(
|
|
1204
|
+
$activities = $stop ? $this->nextActivitiesForServiceStop($order, $order->payload, $stop) : $orderConfig->nextActivity();
|
|
1191
1205
|
|
|
1192
1206
|
// If activity is to complete order add proof of delivery properties if required
|
|
1193
1207
|
// This is a temporary fix until activity is updated to handle POD on it's own
|
|
@@ -1282,7 +1296,18 @@ class OrderController extends Controller
|
|
|
1282
1296
|
public function setDestination(string $id, string $placeId)
|
|
1283
1297
|
{
|
|
1284
1298
|
try {
|
|
1285
|
-
$order = Order::findRecordOrFail($id, [
|
|
1299
|
+
$order = Order::findRecordOrFail($id, [
|
|
1300
|
+
'payload.pickup',
|
|
1301
|
+
'payload.dropoff',
|
|
1302
|
+
'payload.return',
|
|
1303
|
+
'payload.waypoints',
|
|
1304
|
+
'payload.waypointMarkers.place',
|
|
1305
|
+
'payload.waypointMarkers.trackingNumber.status',
|
|
1306
|
+
'driverAssigned',
|
|
1307
|
+
'vehicleAssigned',
|
|
1308
|
+
'customer',
|
|
1309
|
+
'facilitator',
|
|
1310
|
+
]);
|
|
1286
1311
|
} catch (ModelNotFoundException $exception) {
|
|
1287
1312
|
return response()->apiError('Order resource not found.', 404);
|
|
1288
1313
|
}
|
|
@@ -1290,17 +1315,13 @@ class OrderController extends Controller
|
|
|
1290
1315
|
// Get the order payload
|
|
1291
1316
|
$payload = $order->payload;
|
|
1292
1317
|
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
?? (($payload->pickup && $payload->pickup->public_id === $placeId) ? $payload->pickup : null)
|
|
1296
|
-
?? (($payload->dropoff && $payload->dropoff->public_id === $placeId) ? $payload->dropoff : null);
|
|
1297
|
-
|
|
1298
|
-
if (!$place) {
|
|
1318
|
+
$stop = $this->resolveServiceStopFromKey($payload, $placeId);
|
|
1319
|
+
if (!$stop) {
|
|
1299
1320
|
return response()->apiError('Place resource is not a valid destination.', 422);
|
|
1300
1321
|
}
|
|
1301
1322
|
|
|
1302
1323
|
// Persist destination choice
|
|
1303
|
-
$
|
|
1324
|
+
$this->setPayloadCurrentServiceStop($payload, $stop);
|
|
1304
1325
|
|
|
1305
1326
|
return new OrderResource($order->refresh());
|
|
1306
1327
|
}
|
|
@@ -1385,7 +1406,7 @@ class OrderController extends Controller
|
|
|
1385
1406
|
|
|
1386
1407
|
// Load Order
|
|
1387
1408
|
try {
|
|
1388
|
-
$order = Order::findRecordOrFail($id);
|
|
1409
|
+
$order = Order::findRecordOrFail($id, ['payload.pickup', 'payload.dropoff', 'payload.return', 'payload.waypoints', 'payload.waypointMarkers.place']);
|
|
1389
1410
|
} catch (ModelNotFoundException $e) {
|
|
1390
1411
|
return response()->apiError('Order resource not found.', 404);
|
|
1391
1412
|
}
|
|
@@ -1580,7 +1601,7 @@ class OrderController extends Controller
|
|
|
1580
1601
|
|
|
1581
1602
|
// Load Order
|
|
1582
1603
|
try {
|
|
1583
|
-
$order = Order::findRecordOrFail($id);
|
|
1604
|
+
$order = Order::findRecordOrFail($id, ['payload.pickup', 'payload.dropoff', 'payload.return', 'payload.waypoints', 'payload.waypointMarkers.place']);
|
|
1584
1605
|
} catch (ModelNotFoundException $e) {
|
|
1585
1606
|
return response()->apiError('Order resource not found.', 404);
|
|
1586
1607
|
}
|
|
@@ -1678,6 +1699,13 @@ class OrderController extends Controller
|
|
|
1678
1699
|
return $order;
|
|
1679
1700
|
}
|
|
1680
1701
|
|
|
1702
|
+
if (in_array($type, ['place', 'waypoint'], true)) {
|
|
1703
|
+
$stop = $this->resolveServiceStopFromKey($order->payload, $subjectId);
|
|
1704
|
+
if ($stop) {
|
|
1705
|
+
return ($stop['waypoint'] ?? null) instanceof Waypoint ? $stop['waypoint'] : ($stop['place'] ?? $order);
|
|
1706
|
+
}
|
|
1707
|
+
}
|
|
1708
|
+
|
|
1681
1709
|
return match ($type) {
|
|
1682
1710
|
'place', 'waypoint' => Waypoint::withoutGlobalScopes()
|
|
1683
1711
|
->where('payload_uuid', $order->payload_uuid)
|
|
@@ -24,7 +24,7 @@ class ServiceAreaController extends Controller
|
|
|
24
24
|
public function create(CreateServiceAreaRequest $request)
|
|
25
25
|
{
|
|
26
26
|
// get request input
|
|
27
|
-
$input = $request->only(['name', 'type', 'status', 'country', 'color', 'stroke_color', 'trigger_on_entry', 'trigger_on_exit', 'dwell_threshold_minutes', 'speed_limit_kmh']);
|
|
27
|
+
$input = $request->only(['name', 'type', 'status', 'country', 'border', 'color', 'stroke_color', 'trigger_on_entry', 'trigger_on_exit', 'dwell_threshold_minutes', 'speed_limit_kmh']);
|
|
28
28
|
|
|
29
29
|
// get radius for creating service area border - default to 500 meters
|
|
30
30
|
$radius = (int) $request->input('radius', 500);
|
|
@@ -99,7 +99,7 @@ class ServiceAreaController extends Controller
|
|
|
99
99
|
}
|
|
100
100
|
|
|
101
101
|
// get request input
|
|
102
|
-
$input = $request->only(['name', 'type', 'status', 'country', 'color', 'stroke_color', 'trigger_on_entry', 'trigger_on_exit', 'dwell_threshold_minutes', 'speed_limit_kmh']);
|
|
102
|
+
$input = $request->only(['name', 'type', 'status', 'country', 'border', 'color', 'stroke_color', 'trigger_on_entry', 'trigger_on_exit', 'dwell_threshold_minutes', 'speed_limit_kmh']);
|
|
103
103
|
|
|
104
104
|
// get radius for creating service area border - default to 500 meters
|
|
105
105
|
$radius = $request->input('radius', 500);
|
|
@@ -67,7 +67,7 @@ class VehicleController extends Controller
|
|
|
67
67
|
$vehicle = Vehicle::create($input);
|
|
68
68
|
|
|
69
69
|
// driver assignment
|
|
70
|
-
if ($request->
|
|
70
|
+
if ($request->exists('driver') && !empty($request->input('driver'))) {
|
|
71
71
|
// set this vehicle to the driver
|
|
72
72
|
try {
|
|
73
73
|
$driver = Driver::findRecordOrFail($request->input('driver'));
|
|
@@ -80,8 +80,7 @@ class VehicleController extends Controller
|
|
|
80
80
|
);
|
|
81
81
|
}
|
|
82
82
|
|
|
83
|
-
$
|
|
84
|
-
$driver->save();
|
|
83
|
+
$vehicle->assignDriver($driver);
|
|
85
84
|
}
|
|
86
85
|
|
|
87
86
|
// response the driver resource
|
|
@@ -150,6 +149,24 @@ class VehicleController extends Controller
|
|
|
150
149
|
// save the update
|
|
151
150
|
$vehicle->save();
|
|
152
151
|
|
|
152
|
+
if ($request->exists('driver')) {
|
|
153
|
+
if (empty($request->input('driver'))) {
|
|
154
|
+
$vehicle->unassignDriver();
|
|
155
|
+
} else {
|
|
156
|
+
try {
|
|
157
|
+
$driver = Driver::findRecordOrFail($request->input('driver'));
|
|
158
|
+
$vehicle->assignDriver($driver);
|
|
159
|
+
} catch (\Illuminate\Database\Eloquent\ModelNotFoundException $exception) {
|
|
160
|
+
return response()->json(
|
|
161
|
+
[
|
|
162
|
+
'error' => 'The driver attempted to assign this vehicle was not found.',
|
|
163
|
+
],
|
|
164
|
+
404
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
153
170
|
// get udpated vehicle
|
|
154
171
|
$vehicle = $vehicle->refresh();
|
|
155
172
|
|
|
@@ -28,6 +28,7 @@ class ContactController extends FleetOpsController
|
|
|
28
28
|
public function onBeforeCreate(Request $request, array &$input)
|
|
29
29
|
{
|
|
30
30
|
$this->resolveUserInput($request, $input);
|
|
31
|
+
$this->assertCustomerIdentityIsAvailable($input);
|
|
31
32
|
}
|
|
32
33
|
|
|
33
34
|
/**
|
|
@@ -40,6 +41,7 @@ class ContactController extends FleetOpsController
|
|
|
40
41
|
}
|
|
41
42
|
|
|
42
43
|
$this->resolveUserInput($request, $input);
|
|
44
|
+
$this->assertCustomerIdentityIsAvailable($input, $contact);
|
|
43
45
|
}
|
|
44
46
|
|
|
45
47
|
/**
|
|
@@ -146,4 +148,21 @@ class ContactController extends FleetOpsController
|
|
|
146
148
|
$input['user_uuid'] = User::where('uuid', $user)->orWhere('public_id', $user)->value('uuid') ?? $user;
|
|
147
149
|
unset($input['user']);
|
|
148
150
|
}
|
|
151
|
+
|
|
152
|
+
private function assertCustomerIdentityIsAvailable(array $input, ?Contact $contact = null): void
|
|
153
|
+
{
|
|
154
|
+
$type = data_get($input, 'type', $contact?->type);
|
|
155
|
+
if ($type !== 'customer') {
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
$customer = $contact ? $contact->replicate() : new Contact();
|
|
160
|
+
if ($contact) {
|
|
161
|
+
$customer->forceFill($contact->getAttributes());
|
|
162
|
+
$customer->exists = $contact->exists;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
$customer->forceFill($input);
|
|
166
|
+
$customer->assertCustomerIdentityIsAvailable();
|
|
167
|
+
}
|
|
149
168
|
}
|
|
@@ -22,6 +22,10 @@ use Illuminate\Http\Request;
|
|
|
22
22
|
*/
|
|
23
23
|
class LiveController extends Controller
|
|
24
24
|
{
|
|
25
|
+
protected const DEFAULT_VIEWPORT_LIMIT = 500;
|
|
26
|
+
protected const MAX_VIEWPORT_LIMIT = 1000;
|
|
27
|
+
protected const VIEWPORT_BOUNDS_PRECISION = 4;
|
|
28
|
+
|
|
25
29
|
/**
|
|
26
30
|
* Get coordinates for active orders.
|
|
27
31
|
*
|
|
@@ -134,33 +138,19 @@ class LiveController extends Controller
|
|
|
134
138
|
*/
|
|
135
139
|
public function drivers(Request $request)
|
|
136
140
|
{
|
|
137
|
-
$bounds = $
|
|
138
|
-
$
|
|
141
|
+
$bounds = $this->normalizeLiveBounds($request);
|
|
142
|
+
$limit = $this->normalizeLiveLimit($request);
|
|
143
|
+
$cacheParams = ['bounds' => $bounds, 'limit' => $limit];
|
|
139
144
|
|
|
140
|
-
return LiveCacheService::remember('drivers', $cacheParams, function () use ($bounds) {
|
|
145
|
+
return LiveCacheService::remember('drivers', $cacheParams, function () use ($bounds, $limit) {
|
|
141
146
|
$query = Driver::where(['company_uuid' => session('company')])
|
|
142
147
|
->with(['user', 'vehicle'])
|
|
143
148
|
->applyDirectivesForPermissions('fleet-ops list driver');
|
|
144
149
|
|
|
145
|
-
|
|
146
|
-
$
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
AND ST_X(location) BETWEEN -180 AND 180
|
|
150
|
-
AND NOT (ST_X(location) = 0 AND ST_Y(location) = 0)
|
|
151
|
-
');
|
|
152
|
-
|
|
153
|
-
// Apply spatial filtering if bounds are provided
|
|
154
|
-
if ($bounds && is_array($bounds) && count($bounds) === 4) {
|
|
155
|
-
[$south, $west, $north, $east] = $bounds;
|
|
156
|
-
|
|
157
|
-
// Use MySQL spatial functions for POINT column
|
|
158
|
-
// ST_Within checks if location is within the bounding box
|
|
159
|
-
$query->whereRaw(
|
|
160
|
-
'ST_Within(location, ST_MakeEnvelope(POINT(?, ?), POINT(?, ?)))',
|
|
161
|
-
[$west, $south, $east, $north]
|
|
162
|
-
);
|
|
163
|
-
}
|
|
150
|
+
$this->applyLiveLocationGuards($query);
|
|
151
|
+
$this->applyLiveViewportBounds($query, $bounds);
|
|
152
|
+
|
|
153
|
+
$query->orderByDesc('updated_at')->orderByDesc('id')->limit($limit);
|
|
164
154
|
|
|
165
155
|
$drivers = $query->get();
|
|
166
156
|
|
|
@@ -175,34 +165,20 @@ class LiveController extends Controller
|
|
|
175
165
|
*/
|
|
176
166
|
public function vehicles(Request $request)
|
|
177
167
|
{
|
|
178
|
-
$bounds = $
|
|
179
|
-
$
|
|
168
|
+
$bounds = $this->normalizeLiveBounds($request);
|
|
169
|
+
$limit = $this->normalizeLiveLimit($request);
|
|
170
|
+
$cacheParams = ['bounds' => $bounds, 'limit' => $limit];
|
|
180
171
|
|
|
181
|
-
return LiveCacheService::remember('vehicles', $cacheParams, function () use ($bounds) {
|
|
172
|
+
return LiveCacheService::remember('vehicles', $cacheParams, function () use ($bounds, $limit) {
|
|
182
173
|
// Fetch vehicles that are online
|
|
183
174
|
$query = Vehicle::where(['company_uuid' => session('company')])
|
|
184
175
|
->with(['devices', 'driver'])
|
|
185
176
|
->applyDirectivesForPermissions('fleet-ops list vehicle');
|
|
186
177
|
|
|
187
|
-
|
|
188
|
-
$
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
AND ST_X(location) BETWEEN -180 AND 180
|
|
192
|
-
AND NOT (ST_X(location) = 0 AND ST_Y(location) = 0)
|
|
193
|
-
');
|
|
194
|
-
|
|
195
|
-
// Apply spatial filtering if bounds are provided
|
|
196
|
-
if ($bounds && is_array($bounds) && count($bounds) === 4) {
|
|
197
|
-
[$south, $west, $north, $east] = $bounds;
|
|
198
|
-
|
|
199
|
-
// Use MySQL spatial functions for POINT column
|
|
200
|
-
// ST_Within checks if location is within the bounding box
|
|
201
|
-
$query->whereRaw(
|
|
202
|
-
'ST_Within(location, ST_MakeEnvelope(POINT(?, ?), POINT(?, ?)))',
|
|
203
|
-
[$west, $south, $east, $north]
|
|
204
|
-
);
|
|
205
|
-
}
|
|
178
|
+
$this->applyLiveLocationGuards($query);
|
|
179
|
+
$this->applyLiveViewportBounds($query, $bounds);
|
|
180
|
+
|
|
181
|
+
$query->orderByDesc('updated_at')->orderByDesc('id')->limit($limit);
|
|
206
182
|
|
|
207
183
|
$vehicles = $query->get();
|
|
208
184
|
|
|
@@ -217,39 +193,98 @@ class LiveController extends Controller
|
|
|
217
193
|
*/
|
|
218
194
|
public function places(Request $request)
|
|
219
195
|
{
|
|
196
|
+
$bounds = $this->normalizeLiveBounds($request);
|
|
197
|
+
$limit = $this->normalizeLiveLimit($request);
|
|
198
|
+
|
|
220
199
|
// Cache key includes filter parameters
|
|
221
|
-
$cacheParams = $request->only(['query', 'type', 'country',
|
|
200
|
+
$cacheParams = array_merge($request->only(['query', 'type', 'country']), [
|
|
201
|
+
'bounds' => $bounds,
|
|
202
|
+
'limit' => $limit,
|
|
203
|
+
]);
|
|
222
204
|
|
|
223
|
-
return LiveCacheService::remember('places', $cacheParams, function () use ($request) {
|
|
205
|
+
return LiveCacheService::remember('places', $cacheParams, function () use ($request, $bounds, $limit) {
|
|
224
206
|
// Query places based on filters
|
|
225
207
|
$query = Place::where(['company_uuid' => session('company')])
|
|
226
208
|
->filter(new PlaceFilter($request))
|
|
227
209
|
->applyDirectivesForPermissions('fleet-ops list place');
|
|
228
210
|
|
|
229
|
-
|
|
230
|
-
$
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
AND ST_X(location) BETWEEN -180 AND 180
|
|
234
|
-
AND NOT (ST_X(location) = 0 AND ST_Y(location) = 0)
|
|
235
|
-
');
|
|
236
|
-
|
|
237
|
-
// Apply spatial filtering if bounds are provided
|
|
238
|
-
$bounds = $request->input('bounds');
|
|
239
|
-
if ($bounds && is_array($bounds) && count($bounds) === 4) {
|
|
240
|
-
[$south, $west, $north, $east] = $bounds;
|
|
241
|
-
|
|
242
|
-
// Use MySQL spatial functions for POINT column
|
|
243
|
-
// ST_Within checks if location is within the bounding box
|
|
244
|
-
$query->whereRaw(
|
|
245
|
-
'ST_Within(location, ST_MakeEnvelope(POINT(?, ?), POINT(?, ?)))',
|
|
246
|
-
[$west, $south, $east, $north]
|
|
247
|
-
);
|
|
248
|
-
}
|
|
211
|
+
$this->applyLiveLocationGuards($query);
|
|
212
|
+
$this->applyLiveViewportBounds($query, $bounds);
|
|
213
|
+
|
|
214
|
+
$query->orderByDesc('updated_at')->orderByDesc('id')->limit($limit);
|
|
249
215
|
|
|
250
216
|
$places = $query->get();
|
|
251
217
|
|
|
252
218
|
return PlaceIndexResource::collection($places);
|
|
253
219
|
});
|
|
254
220
|
}
|
|
221
|
+
|
|
222
|
+
protected function normalizeLiveBounds(Request $request): ?array
|
|
223
|
+
{
|
|
224
|
+
$bounds = $request->input('bounds');
|
|
225
|
+
|
|
226
|
+
if (!is_array($bounds) || count($bounds) !== 4) {
|
|
227
|
+
return null;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
$bounds = array_values($bounds);
|
|
231
|
+
|
|
232
|
+
if (array_filter($bounds, fn ($value) => !is_numeric($value))) {
|
|
233
|
+
return null;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
[$south, $west, $north, $east] = array_map('floatval', $bounds);
|
|
237
|
+
|
|
238
|
+
if ($south < -90 || $south > 90 || $north < -90 || $north > 90) {
|
|
239
|
+
return null;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
if ($west < -180 || $west > 180 || $east < -180 || $east > 180) {
|
|
243
|
+
return null;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
if ($south > $north || $west > $east) {
|
|
247
|
+
return null;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
return array_map(
|
|
251
|
+
fn ($value) => round($value, static::VIEWPORT_BOUNDS_PRECISION),
|
|
252
|
+
[$south, $west, $north, $east]
|
|
253
|
+
);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
protected function normalizeLiveLimit(Request $request): int
|
|
257
|
+
{
|
|
258
|
+
$limit = (int) $request->input('limit', static::DEFAULT_VIEWPORT_LIMIT);
|
|
259
|
+
|
|
260
|
+
if ($limit < 1) {
|
|
261
|
+
return static::DEFAULT_VIEWPORT_LIMIT;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
return min($limit, static::MAX_VIEWPORT_LIMIT);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
protected function applyLiveLocationGuards($query): void
|
|
268
|
+
{
|
|
269
|
+
$query->whereNotNull('location')
|
|
270
|
+
->whereRaw('
|
|
271
|
+
ST_Y(location) BETWEEN -90 AND 90
|
|
272
|
+
AND ST_X(location) BETWEEN -180 AND 180
|
|
273
|
+
AND NOT (ST_X(location) = 0 AND ST_Y(location) = 0)
|
|
274
|
+
');
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
protected function applyLiveViewportBounds($query, ?array $bounds): void
|
|
278
|
+
{
|
|
279
|
+
if (!$bounds) {
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
[$south, $west, $north, $east] = $bounds;
|
|
284
|
+
|
|
285
|
+
$query->whereRaw(
|
|
286
|
+
'ST_Y(location) BETWEEN ? AND ? AND ST_X(location) BETWEEN ? AND ?',
|
|
287
|
+
[$south, $north, $west, $east]
|
|
288
|
+
);
|
|
289
|
+
}
|
|
255
290
|
}
|