@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
|
@@ -3,9 +3,13 @@
|
|
|
3
3
|
namespace Fleetbase\FleetOps\Http\Controllers\Internal\v1;
|
|
4
4
|
|
|
5
5
|
use Fleetbase\Exceptions\FleetbaseRequestValidationException;
|
|
6
|
+
use Fleetbase\FleetOps\Events\EntityActivityChanged;
|
|
7
|
+
use Fleetbase\FleetOps\Events\EntityCompleted;
|
|
6
8
|
use Fleetbase\FleetOps\Events\OrderDispatchFailed;
|
|
7
9
|
use Fleetbase\FleetOps\Events\OrderReady;
|
|
8
10
|
use Fleetbase\FleetOps\Events\OrderStarted;
|
|
11
|
+
use Fleetbase\FleetOps\Events\WaypointActivityChanged;
|
|
12
|
+
use Fleetbase\FleetOps\Events\WaypointCompleted;
|
|
9
13
|
use Fleetbase\FleetOps\Exports\OrderExport;
|
|
10
14
|
use Fleetbase\FleetOps\Flow\Activity;
|
|
11
15
|
use Fleetbase\FleetOps\Http\Controllers\FleetOpsController;
|
|
@@ -27,6 +31,7 @@ use Fleetbase\FleetOps\Models\ServiceQuote;
|
|
|
27
31
|
use Fleetbase\FleetOps\Models\TrackingStatus;
|
|
28
32
|
use Fleetbase\FleetOps\Models\Waypoint;
|
|
29
33
|
use Fleetbase\FleetOps\Notifications\OrderPing;
|
|
34
|
+
use Fleetbase\FleetOps\Support\ResolvesOrderServiceStops;
|
|
30
35
|
use Fleetbase\FleetOps\Support\Utils;
|
|
31
36
|
use Fleetbase\Http\Requests\ExportRequest;
|
|
32
37
|
use Fleetbase\Http\Requests\Internal\BulkActionRequest;
|
|
@@ -37,13 +42,18 @@ use Fleetbase\Support\TemplateString;
|
|
|
37
42
|
use Illuminate\Database\Eloquent\ModelNotFoundException;
|
|
38
43
|
use Illuminate\Database\QueryException;
|
|
39
44
|
use Illuminate\Http\Request;
|
|
45
|
+
use Illuminate\Http\UploadedFile;
|
|
40
46
|
use Illuminate\Support\Facades\DB;
|
|
47
|
+
use Illuminate\Support\Facades\Storage;
|
|
41
48
|
use Illuminate\Support\Facades\Validator;
|
|
42
49
|
use Illuminate\Support\Str;
|
|
50
|
+
use Illuminate\Validation\ValidationException;
|
|
43
51
|
use Maatwebsite\Excel\Facades\Excel;
|
|
44
52
|
|
|
45
53
|
class OrderController extends FleetOpsController
|
|
46
54
|
{
|
|
55
|
+
use ResolvesOrderServiceStops;
|
|
56
|
+
|
|
47
57
|
/**
|
|
48
58
|
* The resource to query.
|
|
49
59
|
*
|
|
@@ -63,6 +73,11 @@ class OrderController extends FleetOpsController
|
|
|
63
73
|
*/
|
|
64
74
|
public function onAfterUpdate($request, $order)
|
|
65
75
|
{
|
|
76
|
+
$uploads = $request->array('order.files');
|
|
77
|
+
if ($uploads) {
|
|
78
|
+
$order->attachFiles($uploads);
|
|
79
|
+
}
|
|
80
|
+
|
|
66
81
|
$waypoints = $request->array('order.payload.waypoints');
|
|
67
82
|
if ($waypoints) {
|
|
68
83
|
$order->loadMissing('payload');
|
|
@@ -165,14 +180,7 @@ class OrderController extends FleetOpsController
|
|
|
165
180
|
->insertEntities($entities);
|
|
166
181
|
|
|
167
182
|
// If order creation includes files assosciate each to this order
|
|
168
|
-
|
|
169
|
-
$ids = collect($uploads)->pluck('uuid');
|
|
170
|
-
$files = File::whereIn('uuid', $ids)->get();
|
|
171
|
-
|
|
172
|
-
foreach ($files as $file) {
|
|
173
|
-
$file->setKey($order);
|
|
174
|
-
}
|
|
175
|
-
}
|
|
183
|
+
$order->attachFiles($uploads);
|
|
176
184
|
|
|
177
185
|
// save custom field values
|
|
178
186
|
if (is_array($customFieldValues)) {
|
|
@@ -706,11 +714,22 @@ class OrderController extends FleetOpsController
|
|
|
706
714
|
*/
|
|
707
715
|
public function updateActivity(string $id, Request $request)
|
|
708
716
|
{
|
|
709
|
-
$order = Order::findById($id, [
|
|
717
|
+
$order = Order::findById($id, [
|
|
718
|
+
'driverAssigned',
|
|
719
|
+
'payload.entities',
|
|
720
|
+
'payload.pickup',
|
|
721
|
+
'payload.dropoff',
|
|
722
|
+
'payload.return',
|
|
723
|
+
'payload.currentWaypoint',
|
|
724
|
+
'payload.waypoints',
|
|
725
|
+
'payload.waypointMarkers.place',
|
|
726
|
+
'payload.waypointMarkers.trackingNumber.status',
|
|
727
|
+
]);
|
|
710
728
|
if (!$order) {
|
|
711
729
|
return response()->error('No order found.');
|
|
712
730
|
}
|
|
713
731
|
|
|
732
|
+
$proof = $request->input('proof');
|
|
714
733
|
$activity = $request->array('activity');
|
|
715
734
|
$activity = new Activity($activity, $order->getConfigFlow());
|
|
716
735
|
|
|
@@ -731,29 +750,77 @@ class OrderController extends FleetOpsController
|
|
|
731
750
|
/**
|
|
732
751
|
* @var \Fleetbase\LaravelMysqlSpatial\Types\Point
|
|
733
752
|
*/
|
|
734
|
-
$location
|
|
735
|
-
$
|
|
736
|
-
|
|
753
|
+
$location = $order->getLastLocation();
|
|
754
|
+
$isMultipleWaypointOrder = $this->payloadHasWaypoints($order->payload);
|
|
755
|
+
|
|
756
|
+
if (!$isMultipleWaypointOrder) {
|
|
757
|
+
$order->updateActivity($activity, $proof);
|
|
737
758
|
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
if (!$order->payload->isMultipleDropOrder) {
|
|
759
|
+
// also update for each order entities if not multiple drop order
|
|
760
|
+
// all entities will share the same activity status as is one drop order
|
|
741
761
|
foreach ($order->payload->entities as $entity) {
|
|
742
|
-
$entity->insertActivity($activity, $location);
|
|
762
|
+
$entity->insertActivity($activity, $location, $proof);
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
// Handle order completed
|
|
766
|
+
if (Utils::isActivity($activity) && $activity->completesOrder() && $order->driverAssigned) {
|
|
767
|
+
// unset from driver current job
|
|
768
|
+
$order->driverAssigned->unassignCurrentOrder();
|
|
769
|
+
$order->complete($this->resolveProof($proof));
|
|
743
770
|
}
|
|
771
|
+
|
|
772
|
+
return new OrderResource($order->refresh());
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
if ($activity->is('dispatched')) {
|
|
776
|
+
$order->updateActivity($activity, $proof);
|
|
777
|
+
|
|
778
|
+
return new OrderResource($order->refresh());
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
if ($activity->is('started')) {
|
|
782
|
+
$order->updateActivity($activity, $proof);
|
|
783
|
+
|
|
784
|
+
$this->ensurePayloadCurrentServiceStop($order->payload);
|
|
785
|
+
if (!$this->payloadHasCurrentServiceStopActivity($order->payload, $activity)) {
|
|
786
|
+
$this->updateCurrentServiceStopActivity($order, $activity, $location, $proof);
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
return new OrderResource($order->refresh());
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
if (!$order->started && in_array($order->status, ['created', 'dispatched'])) {
|
|
793
|
+
return response()->error('Order must be started before waypoint activity can be updated.', 422);
|
|
744
794
|
}
|
|
745
795
|
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
796
|
+
$currentStop = $this->ensurePayloadCurrentServiceStop($order->payload);
|
|
797
|
+
$isCompletingCurrentStop = Utils::isActivity($activity) && $activity->complete();
|
|
798
|
+
|
|
799
|
+
$this->updateCurrentServiceStopActivity($order, $activity, $location, $proof);
|
|
800
|
+
|
|
801
|
+
if (Utils::isActivity($activity) && $activity->is('canceled')) {
|
|
802
|
+
if ($order->driverAssigned) {
|
|
803
|
+
$order->driverAssigned->unassignCurrentOrder();
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
$order->cancel();
|
|
807
|
+
|
|
808
|
+
return new OrderResource($order->refresh());
|
|
751
809
|
}
|
|
752
810
|
|
|
753
|
-
|
|
754
|
-
|
|
811
|
+
if (Utils::isActivity($activity) && $activity->completesOrder()) {
|
|
812
|
+
$nextStop = $isCompletingCurrentStop ? $this->advanceCurrentServiceStopDestination($order, $order->payload) : null;
|
|
813
|
+
|
|
814
|
+
if (!$nextStop || !$currentStop) {
|
|
815
|
+
if ($order->driverAssigned) {
|
|
816
|
+
$order->driverAssigned->unassignCurrentOrder();
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
$order->complete($this->resolveProof($proof));
|
|
820
|
+
}
|
|
821
|
+
}
|
|
755
822
|
|
|
756
|
-
return
|
|
823
|
+
return new OrderResource($order->refresh());
|
|
757
824
|
}
|
|
758
825
|
|
|
759
826
|
/**
|
|
@@ -769,12 +836,24 @@ class OrderController extends FleetOpsController
|
|
|
769
836
|
return response()->error('No order found.');
|
|
770
837
|
}
|
|
771
838
|
|
|
772
|
-
$
|
|
839
|
+
$canUpdateWaypointActivity = $order->started || $order->started_at || !in_array($order->status, ['created', 'dispatched']);
|
|
840
|
+
$stop = null;
|
|
841
|
+
if ($request->filled('waypoint') && $canUpdateWaypointActivity) {
|
|
842
|
+
$order->loadMissing([
|
|
843
|
+
'payload.pickup',
|
|
844
|
+
'payload.dropoff',
|
|
845
|
+
'payload.return',
|
|
846
|
+
'payload.waypoints',
|
|
847
|
+
'payload.waypointMarkers.place',
|
|
848
|
+
'payload.waypointMarkers.trackingNumber.status',
|
|
849
|
+
]);
|
|
850
|
+
$stop = $this->resolveServiceStopFromKey($order->payload, $request->input('waypoint'));
|
|
851
|
+
}
|
|
773
852
|
$orderConfig = $order->ensureOrderConfig();
|
|
774
853
|
if (!$orderConfig) {
|
|
775
854
|
return response()->error('No order config found for order.');
|
|
776
855
|
}
|
|
777
|
-
$activities = $orderConfig->nextActivity(
|
|
856
|
+
$activities = $stop ? $this->nextActivitiesForServiceStop($order, $order->payload, $stop) : $orderConfig->nextActivity();
|
|
778
857
|
|
|
779
858
|
// If activity is to complete order add proof of delivery properties if required
|
|
780
859
|
// This is a temporary fix until activity is updated to handle POD on it's own
|
|
@@ -794,6 +873,330 @@ class OrderController extends FleetOpsController
|
|
|
794
873
|
return response()->json($activities);
|
|
795
874
|
}
|
|
796
875
|
|
|
876
|
+
/**
|
|
877
|
+
* Mark a waypoint as the current destination for a multi-waypoint order.
|
|
878
|
+
*
|
|
879
|
+
* @return \Fleetbase\Http\Resources\v1\Order
|
|
880
|
+
*/
|
|
881
|
+
public function setDestination(string $id, string $placeId)
|
|
882
|
+
{
|
|
883
|
+
$order = Order::findById($id, [
|
|
884
|
+
'payload.pickup',
|
|
885
|
+
'payload.dropoff',
|
|
886
|
+
'payload.return',
|
|
887
|
+
'payload.waypoints',
|
|
888
|
+
'payload.waypointMarkers.place',
|
|
889
|
+
'payload.waypointMarkers.trackingNumber.status',
|
|
890
|
+
'driverAssigned',
|
|
891
|
+
'vehicleAssigned',
|
|
892
|
+
'customer',
|
|
893
|
+
'facilitator',
|
|
894
|
+
]);
|
|
895
|
+
if (!$order) {
|
|
896
|
+
return response()->error('No order found.');
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
if (!$this->payloadHasWaypoints($order->payload)) {
|
|
900
|
+
return response()->error('Destination can only be changed for multi-waypoint orders.', 422);
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
$stop = $this->resolveServiceStopFromKey($order->payload, $placeId);
|
|
904
|
+
if (!$stop) {
|
|
905
|
+
return response()->error('Place resource is not a valid route destination.', 422);
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
$this->setPayloadCurrentServiceStop($order->payload, $stop);
|
|
909
|
+
|
|
910
|
+
return new OrderResource($order->refresh());
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
/**
|
|
914
|
+
* Capture one or more photos for order/waypoint proof of delivery.
|
|
915
|
+
*
|
|
916
|
+
* @return ProofResource|\Illuminate\Http\Response
|
|
917
|
+
*/
|
|
918
|
+
public function capturePhoto(Request $request, string $id, ?string $subjectId = null)
|
|
919
|
+
{
|
|
920
|
+
try {
|
|
921
|
+
$request->validate([
|
|
922
|
+
'photos' => 'required|array|min:1',
|
|
923
|
+
'photos.*' => [
|
|
924
|
+
function ($attribute, $value, $fail) {
|
|
925
|
+
if ($value instanceof UploadedFile) {
|
|
926
|
+
if (
|
|
927
|
+
!$value->isValid()
|
|
928
|
+
|| !in_array($value->extension(), ['jpg', 'jpeg', 'png', 'gif', 'webp'])
|
|
929
|
+
|| $value->getSize() > 10 * 1024 * 1024
|
|
930
|
+
) {
|
|
931
|
+
$fail("{$attribute} must be a valid image file <= 10 MB.");
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
return;
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
if (is_string($value) && base64_decode($value, true) !== false) {
|
|
938
|
+
return;
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
$fail("{$attribute} must be an image file or a valid Base64 string.");
|
|
942
|
+
},
|
|
943
|
+
],
|
|
944
|
+
'remarks' => 'sometimes|string|max:255',
|
|
945
|
+
'data' => 'sometimes|array',
|
|
946
|
+
]);
|
|
947
|
+
} catch (ValidationException $e) {
|
|
948
|
+
$errorMessage = collect($e->errors())->flatten()->first();
|
|
949
|
+
|
|
950
|
+
return response()->error($errorMessage, 422);
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
$order = Order::findById($id, ['payload.pickup', 'payload.dropoff', 'payload.return', 'payload.waypoints', 'payload.waypointMarkers.place']);
|
|
954
|
+
if (!$order) {
|
|
955
|
+
return response()->error('No order found.');
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
$subject = $this->resolveProofSubject($order, $subjectId);
|
|
959
|
+
if (!$subject) {
|
|
960
|
+
return response()->error('Unable to capture photo as proof.', 422);
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
$disk = $request->input('disk', config('filesystems.default'));
|
|
964
|
+
$bucket = $request->input("filesystems.disks.{$disk}.bucket", config('filesystems.disks.s3.bucket'));
|
|
965
|
+
$rawInputs = $request->file('photos', []);
|
|
966
|
+
$base64Inputs = array_filter($request->input('photos', []), function ($value) {
|
|
967
|
+
return is_string($value) && base64_decode($value, true) !== false;
|
|
968
|
+
});
|
|
969
|
+
$incoming = array_merge($rawInputs, $base64Inputs);
|
|
970
|
+
$remarks = $request->input('remarks', 'Verified by Photo');
|
|
971
|
+
$data = $request->input('data', []);
|
|
972
|
+
|
|
973
|
+
if (empty($incoming)) {
|
|
974
|
+
return response()->error('No photo data to capture.', 422);
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
foreach ($incoming as $item) {
|
|
978
|
+
$proof = Proof::create([
|
|
979
|
+
'company_uuid' => session('company'),
|
|
980
|
+
'order_uuid' => $order->uuid,
|
|
981
|
+
'subject_uuid' => $subject->uuid,
|
|
982
|
+
'subject_type' => Utils::getModelClassName($subject),
|
|
983
|
+
'remarks' => $remarks,
|
|
984
|
+
'raw_data' => $item instanceof UploadedFile ? null : $item,
|
|
985
|
+
'data' => $data,
|
|
986
|
+
]);
|
|
987
|
+
|
|
988
|
+
$file = $this->storeProofPhoto($proof, $item, $disk, $bucket);
|
|
989
|
+
$proof->update(['file_uuid' => $file->uuid]);
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
return new ProofResource($proof);
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
/**
|
|
996
|
+
* Store a proof photo and create its file record.
|
|
997
|
+
*/
|
|
998
|
+
protected function storeProofPhoto(Proof $proof, UploadedFile|string $photo, string $disk, string $bucket): File
|
|
999
|
+
{
|
|
1000
|
+
$isFile = $photo instanceof UploadedFile;
|
|
1001
|
+
$contents = $isFile ? file_get_contents($photo->getRealPath()) : base64_decode($photo);
|
|
1002
|
+
$extension = $isFile ? $photo->getClientOriginalExtension() : 'png';
|
|
1003
|
+
$contentType = $isFile ? $photo->getClientMimeType() : 'image/png';
|
|
1004
|
+
$company = session('company');
|
|
1005
|
+
$path = "uploads/{$company}/photos/{$proof->public_id}.{$extension}";
|
|
1006
|
+
|
|
1007
|
+
Storage::disk($disk)->put($path, $contents);
|
|
1008
|
+
|
|
1009
|
+
return File::create([
|
|
1010
|
+
'company_uuid' => $company,
|
|
1011
|
+
'uploader_uuid' => session('user'),
|
|
1012
|
+
'name' => basename($path),
|
|
1013
|
+
'original_filename' => basename($path),
|
|
1014
|
+
'extension' => $extension,
|
|
1015
|
+
'content_type' => $contentType,
|
|
1016
|
+
'path' => $path,
|
|
1017
|
+
'bucket' => $bucket,
|
|
1018
|
+
'type' => 'photo',
|
|
1019
|
+
'size' => strlen($contents),
|
|
1020
|
+
])->setKey($proof);
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
/**
|
|
1024
|
+
* Resolve the subject that proof belongs to.
|
|
1025
|
+
*/
|
|
1026
|
+
protected function resolveProofSubject(Order $order, ?string $subjectId = null): Order|Waypoint|Place|null
|
|
1027
|
+
{
|
|
1028
|
+
if (!$subjectId) {
|
|
1029
|
+
return $order;
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
$stop = $this->resolveServiceStopFromKey($order->payload, $subjectId);
|
|
1033
|
+
if ($stop) {
|
|
1034
|
+
return ($stop['waypoint'] ?? null) instanceof Waypoint ? $stop['waypoint'] : ($stop['place'] ?? $order);
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
return Waypoint::withoutGlobalScopes()
|
|
1038
|
+
->where('payload_uuid', $order->payload_uuid)
|
|
1039
|
+
->where(function ($query) use ($subjectId) {
|
|
1040
|
+
$query
|
|
1041
|
+
->where('public_id', $subjectId)
|
|
1042
|
+
->orWhere('uuid', $subjectId)
|
|
1043
|
+
->orWhereHas('place', function ($query) use ($subjectId) {
|
|
1044
|
+
$query->where('public_id', $subjectId)->orWhere('uuid', $subjectId);
|
|
1045
|
+
});
|
|
1046
|
+
})
|
|
1047
|
+
->first();
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1050
|
+
/**
|
|
1051
|
+
* Resolve proof to the model expected by order completion.
|
|
1052
|
+
*/
|
|
1053
|
+
protected function resolveProof($proof): ?Proof
|
|
1054
|
+
{
|
|
1055
|
+
if ($proof instanceof Proof) {
|
|
1056
|
+
return $proof;
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1059
|
+
if (is_string($proof)) {
|
|
1060
|
+
return Proof::where('public_id', $proof)->orWhere('uuid', $proof)->first();
|
|
1061
|
+
}
|
|
1062
|
+
|
|
1063
|
+
return null;
|
|
1064
|
+
}
|
|
1065
|
+
|
|
1066
|
+
/**
|
|
1067
|
+
* Determine whether the payload should use waypoint-scoped activity.
|
|
1068
|
+
*/
|
|
1069
|
+
protected function payloadHasWaypoints(?Payload $payload): bool
|
|
1070
|
+
{
|
|
1071
|
+
if (!$payload) {
|
|
1072
|
+
return false;
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
$payload->loadMissing('waypoints');
|
|
1076
|
+
|
|
1077
|
+
return $payload->waypoints->isNotEmpty();
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1080
|
+
/**
|
|
1081
|
+
* Insert an activity on the payload's current waypoint and its destination entities.
|
|
1082
|
+
*/
|
|
1083
|
+
protected function updateCurrentWaypointActivity(?Payload $payload, Activity $activity, $location = null, $proof = null): ?Waypoint
|
|
1084
|
+
{
|
|
1085
|
+
if (!$payload || !Utils::isActivity($activity) || !$location) {
|
|
1086
|
+
return null;
|
|
1087
|
+
}
|
|
1088
|
+
|
|
1089
|
+
$payload->loadMissing(['order', 'waypointMarkers', 'entities']);
|
|
1090
|
+
$currentWaypoint = $payload->waypointMarkers->firstWhere('place_uuid', $payload->current_waypoint_uuid);
|
|
1091
|
+
if (!$currentWaypoint) {
|
|
1092
|
+
return null;
|
|
1093
|
+
}
|
|
1094
|
+
|
|
1095
|
+
$currentWaypoint->insertActivity($activity, $location, $proof);
|
|
1096
|
+
$activity->fireEvents($payload->order, $currentWaypoint);
|
|
1097
|
+
|
|
1098
|
+
$entities = $payload->entities->where('destination_uuid', $payload->current_waypoint_uuid);
|
|
1099
|
+
foreach ($entities as $entity) {
|
|
1100
|
+
$entity->insertActivity($activity, $location, $proof);
|
|
1101
|
+
if ($activity->complete()) {
|
|
1102
|
+
event(new EntityCompleted($entity, $activity));
|
|
1103
|
+
} else {
|
|
1104
|
+
event(new EntityActivityChanged($entity, $activity));
|
|
1105
|
+
}
|
|
1106
|
+
}
|
|
1107
|
+
|
|
1108
|
+
if ($activity->complete()) {
|
|
1109
|
+
event(new WaypointCompleted($currentWaypoint, $activity));
|
|
1110
|
+
} else {
|
|
1111
|
+
event(new WaypointActivityChanged($currentWaypoint, $activity));
|
|
1112
|
+
}
|
|
1113
|
+
|
|
1114
|
+
return $currentWaypoint;
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1117
|
+
/**
|
|
1118
|
+
* Reload waypoint marker status and decide whether every stop is complete.
|
|
1119
|
+
*/
|
|
1120
|
+
protected function allWaypointMarkersComplete(?Payload $payload): bool
|
|
1121
|
+
{
|
|
1122
|
+
if (!$payload) {
|
|
1123
|
+
return false;
|
|
1124
|
+
}
|
|
1125
|
+
|
|
1126
|
+
$waypointMarkers = $this->freshWaypointMarkers($payload);
|
|
1127
|
+
|
|
1128
|
+
return $waypointMarkers->isNotEmpty() && $waypointMarkers->every(fn (Waypoint $waypoint) => $this->waypointMarkerIsComplete($waypoint));
|
|
1129
|
+
}
|
|
1130
|
+
|
|
1131
|
+
/**
|
|
1132
|
+
* Move the current destination to the next incomplete waypoint.
|
|
1133
|
+
*/
|
|
1134
|
+
protected function advanceCurrentWaypointDestination(?Payload $payload): ?Waypoint
|
|
1135
|
+
{
|
|
1136
|
+
if (!$payload) {
|
|
1137
|
+
return null;
|
|
1138
|
+
}
|
|
1139
|
+
|
|
1140
|
+
$nextWaypoint = $this->freshWaypointMarkers($payload)
|
|
1141
|
+
->sortBy('order')
|
|
1142
|
+
->first(function (Waypoint $waypoint) use ($payload) {
|
|
1143
|
+
return !$this->waypointMarkerIsComplete($waypoint) && $waypoint->place_uuid !== $payload->current_waypoint_uuid;
|
|
1144
|
+
});
|
|
1145
|
+
|
|
1146
|
+
if (!$nextWaypoint) {
|
|
1147
|
+
return null;
|
|
1148
|
+
}
|
|
1149
|
+
|
|
1150
|
+
$payload->setCurrentWaypoint($nextWaypoint);
|
|
1151
|
+
$payload->setRelation('currentWaypoint', $nextWaypoint->getPlace());
|
|
1152
|
+
$payload->setRelation('currentWaypointMarker', $nextWaypoint);
|
|
1153
|
+
|
|
1154
|
+
return $nextWaypoint;
|
|
1155
|
+
}
|
|
1156
|
+
|
|
1157
|
+
/**
|
|
1158
|
+
* Reload waypoint markers with current tracking status.
|
|
1159
|
+
*/
|
|
1160
|
+
protected function freshWaypointMarkers(Payload $payload)
|
|
1161
|
+
{
|
|
1162
|
+
$payload->unsetRelation('waypointMarkers');
|
|
1163
|
+
$payload->load(['waypointMarkers.trackingNumber.status']);
|
|
1164
|
+
|
|
1165
|
+
return $payload->waypointMarkers;
|
|
1166
|
+
}
|
|
1167
|
+
|
|
1168
|
+
/**
|
|
1169
|
+
* Determine whether a waypoint marker's latest status is complete.
|
|
1170
|
+
*/
|
|
1171
|
+
protected function waypointMarkerIsComplete(Waypoint $waypoint): bool
|
|
1172
|
+
{
|
|
1173
|
+
if (!$waypoint->tracking_number_uuid) {
|
|
1174
|
+
return false;
|
|
1175
|
+
}
|
|
1176
|
+
|
|
1177
|
+
return (bool) data_get($this->trackingNumberStatus($waypoint->tracking_number_uuid), 'complete', false);
|
|
1178
|
+
}
|
|
1179
|
+
|
|
1180
|
+
/**
|
|
1181
|
+
* Determine whether the payload's current waypoint already has an activity row.
|
|
1182
|
+
*/
|
|
1183
|
+
protected function payloadHasCurrentWaypointActivity(?Payload $payload, Activity $activity): bool
|
|
1184
|
+
{
|
|
1185
|
+
if (!$payload || !$payload->current_waypoint_uuid) {
|
|
1186
|
+
return false;
|
|
1187
|
+
}
|
|
1188
|
+
|
|
1189
|
+
$payload->loadMissing('waypointMarkers');
|
|
1190
|
+
$currentWaypoint = $payload->waypointMarkers->firstWhere('place_uuid', $payload->current_waypoint_uuid);
|
|
1191
|
+
if (!$currentWaypoint || !$currentWaypoint->tracking_number_uuid) {
|
|
1192
|
+
return false;
|
|
1193
|
+
}
|
|
1194
|
+
|
|
1195
|
+
return TrackingStatus::where('tracking_number_uuid', $currentWaypoint->tracking_number_uuid)
|
|
1196
|
+
->where('code', TrackingStatus::prepareCode($activity->code))
|
|
1197
|
+
->exists();
|
|
1198
|
+
}
|
|
1199
|
+
|
|
797
1200
|
/**
|
|
798
1201
|
* Finds and responds with the orders next activity update based on the orders configuration.
|
|
799
1202
|
*
|
|
@@ -188,7 +188,7 @@ class ServiceQuoteController extends FleetOpsController
|
|
|
188
188
|
$service = $request->input('service', 'all'); // the specific service rate to query - defaults to `all`
|
|
189
189
|
$serviceType = $request->input('service_type'); // the specific type of service rate to query
|
|
190
190
|
$isCashOnDelivery = $request->has('cod');
|
|
191
|
-
$currency = $request->
|
|
191
|
+
$currency = $request->input('currency');
|
|
192
192
|
$totalDistance = $request->input('distance');
|
|
193
193
|
$totalTime = $request->input('time');
|
|
194
194
|
$pickup = $request->or(['payload.pickup', 'payload.pickup_uuid', 'pickup']);
|
|
@@ -523,7 +523,7 @@ class SettingController extends Controller
|
|
|
523
523
|
$config = config('fleetops.tracking', []);
|
|
524
524
|
$systemSettings = Setting::lookup('fleet-ops.tracking-settings', []);
|
|
525
525
|
|
|
526
|
-
$defaults
|
|
526
|
+
$defaults = array_merge($config, is_array($systemSettings) ? $systemSettings : []);
|
|
527
527
|
$defaults['alerts'] = $this->normalizeTrackingAlertSettings(data_get($defaults, 'alerts', []));
|
|
528
528
|
|
|
529
529
|
return $defaults;
|
|
@@ -541,7 +541,7 @@ class SettingController extends Controller
|
|
|
541
541
|
'distance_threshold_meters' => 500,
|
|
542
542
|
],
|
|
543
543
|
'prolonged_stoppages' => [
|
|
544
|
-
'enabled'
|
|
544
|
+
'enabled' => true,
|
|
545
545
|
'duration_threshold_minutes' => 30,
|
|
546
546
|
],
|
|
547
547
|
];
|
|
@@ -5,10 +5,12 @@ namespace Fleetbase\FleetOps\Http\Controllers\Internal\v1;
|
|
|
5
5
|
use Fleetbase\FleetOps\Exports\VehicleExport;
|
|
6
6
|
use Fleetbase\FleetOps\Http\Controllers\FleetOpsController;
|
|
7
7
|
use Fleetbase\FleetOps\Imports\VehicleImport;
|
|
8
|
+
use Fleetbase\FleetOps\Models\Driver;
|
|
8
9
|
use Fleetbase\FleetOps\Models\Vehicle;
|
|
9
10
|
use Fleetbase\Http\Requests\ExportRequest;
|
|
10
11
|
use Fleetbase\Http\Requests\ImportRequest;
|
|
11
12
|
use Illuminate\Http\Request;
|
|
13
|
+
use Illuminate\Support\Arr;
|
|
12
14
|
use Illuminate\Support\Facades\DB;
|
|
13
15
|
use Illuminate\Support\Str;
|
|
14
16
|
use Maatwebsite\Excel\Facades\Excel;
|
|
@@ -25,14 +27,65 @@ class VehicleController extends FleetOpsController
|
|
|
25
27
|
/**
|
|
26
28
|
* Handle post save transactions.
|
|
27
29
|
*/
|
|
28
|
-
public function afterSave(Request $request, Vehicle $vehicle)
|
|
30
|
+
public function afterSave(Request $request, Vehicle $vehicle, array $input = [])
|
|
29
31
|
{
|
|
32
|
+
if ($this->hasDriverInput($request)) {
|
|
33
|
+
$this->syncDriverAssignment($vehicle, $this->driverIdentifierFromRequest($request));
|
|
34
|
+
}
|
|
35
|
+
|
|
30
36
|
$customFieldValues = $request->array('vehicle.custom_field_values');
|
|
31
37
|
if ($customFieldValues) {
|
|
32
38
|
$vehicle->syncCustomFieldValues($customFieldValues);
|
|
33
39
|
}
|
|
34
40
|
}
|
|
35
41
|
|
|
42
|
+
protected function hasDriverInput(Request $request): bool
|
|
43
|
+
{
|
|
44
|
+
$payload = $request->all();
|
|
45
|
+
|
|
46
|
+
return Arr::has($payload, 'vehicle.driver_uuid')
|
|
47
|
+
|| Arr::has($payload, 'vehicle.driver.uuid')
|
|
48
|
+
|| Arr::has($payload, 'vehicle.driver.id')
|
|
49
|
+
|| Arr::has($payload, 'driver_uuid')
|
|
50
|
+
|| Arr::has($payload, 'driver');
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
protected function driverIdentifierFromRequest(Request $request): ?string
|
|
54
|
+
{
|
|
55
|
+
$payload = $request->all();
|
|
56
|
+
$driver = data_get($payload, 'vehicle.driver') ?? data_get($payload, 'driver');
|
|
57
|
+
|
|
58
|
+
return data_get($payload, 'vehicle.driver_uuid')
|
|
59
|
+
?? data_get($payload, 'vehicle.driver.uuid')
|
|
60
|
+
?? data_get($payload, 'vehicle.driver.id')
|
|
61
|
+
?? (is_string($driver) ? $driver : null)
|
|
62
|
+
?? data_get($payload, 'driver_uuid')
|
|
63
|
+
?? data_get($payload, 'driver.uuid')
|
|
64
|
+
?? data_get($payload, 'driver.id');
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
protected function syncDriverAssignment(Vehicle $vehicle, ?string $identifier): void
|
|
68
|
+
{
|
|
69
|
+
if (empty($identifier)) {
|
|
70
|
+
$vehicle->unassignDriver();
|
|
71
|
+
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
$driver = Driver::withoutGlobalScopes()
|
|
76
|
+
->whereNull('deleted_at')
|
|
77
|
+
->where(function ($query) use ($identifier) {
|
|
78
|
+
$query->where('uuid', $identifier)->orWhere('public_id', $identifier);
|
|
79
|
+
})
|
|
80
|
+
->when(session('company'), fn ($query, $company) => $query->where('company_uuid', $company))
|
|
81
|
+
->first();
|
|
82
|
+
|
|
83
|
+
if ($driver) {
|
|
84
|
+
$vehicle->assignDriver($driver);
|
|
85
|
+
$vehicle->setRelation('driver', $driver);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
36
89
|
/**
|
|
37
90
|
* Get all status options for an vehicle.
|
|
38
91
|
*
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
<?php
|
|
2
|
+
|
|
3
|
+
namespace Fleetbase\FleetOps\Http\Middleware;
|
|
4
|
+
|
|
5
|
+
use Fleetbase\FleetOps\Support\CustomerAuth;
|
|
6
|
+
use Illuminate\Http\Request;
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Enforce that a Customer-Token header is present and resolves to a
|
|
10
|
+
* customer-type Contact that belongs to the same company as the request's
|
|
11
|
+
* API credential.
|
|
12
|
+
*
|
|
13
|
+
* Apply this middleware to authenticated `/v1/customers/...` routes. The
|
|
14
|
+
* public sign-up / login routes do NOT use it.
|
|
15
|
+
*/
|
|
16
|
+
class AuthenticateCustomerToken
|
|
17
|
+
{
|
|
18
|
+
public function handle(Request $request, \Closure $next)
|
|
19
|
+
{
|
|
20
|
+
$customer = CustomerAuth::resolveFromHeader($request);
|
|
21
|
+
if (!$customer) {
|
|
22
|
+
return response()->apiError('Customer token is missing or invalid.', 401);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
$sessionCompany = session('company');
|
|
26
|
+
if ($sessionCompany && $customer->company_uuid !== $sessionCompany) {
|
|
27
|
+
return response()->apiError('Customer does not belong to this company.', 403);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
CustomerAuth::setCurrent($customer);
|
|
31
|
+
|
|
32
|
+
return $next($request);
|
|
33
|
+
}
|
|
34
|
+
}
|