@fleetbase/fleetops-engine 0.6.50 → 0.6.51
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/README.md +185 -67
- package/addon/components/contact/panel-header.hbs +11 -48
- package/addon/components/customer/details.hbs +1 -2
- package/addon/components/customer/form.hbs +63 -51
- package/addon/components/customer/form.js +27 -1
- package/addon/components/customer/orders.hbs +48 -0
- package/addon/components/customer/orders.js +67 -5
- package/addon/components/driver/form.hbs +1 -1
- package/addon/components/home/getting-started-guidance.hbs +113 -0
- package/addon/components/home/getting-started-guidance.js +107 -0
- package/addon/components/issue/details/comments.hbs +3 -0
- package/addon/components/issue/details/comments.js +3 -0
- package/addon/components/issue/details/correspondence.hbs +9 -0
- package/addon/components/issue/details/correspondence.js +83 -0
- package/addon/components/issue/details/documents.hbs +69 -0
- package/addon/components/issue/details/documents.js +85 -0
- package/addon/components/issue/details.hbs +220 -59
- package/addon/components/issue/details.js +245 -0
- package/addon/components/issue/form.hbs +19 -1
- package/addon/components/issue/panel-header.hbs +43 -0
- package/addon/components/issue/panel-header.js +22 -0
- package/addon/components/issue/timeline.hbs +40 -0
- package/addon/components/issue/timeline.js +45 -0
- package/addon/components/leaflet-tracking-marker.js +3 -0
- package/addon/components/map/drawer/position-listing.js +3 -0
- package/addon/components/map/leaflet-live-map.hbs +2 -2
- package/addon/components/map/leaflet-live-map.js +17 -0
- package/addon/components/map/order-list-overlay.hbs +4 -4
- package/addon/components/modals/convert-customer-to-vendor.hbs +30 -0
- package/addon/components/modals/convert-customer-to-vendor.js +55 -0
- package/addon/components/modals/issue-assign.hbs +20 -0
- package/addon/components/modals/issue-assign.js +49 -0
- package/addon/components/modals/issue-close.hbs +30 -0
- package/addon/components/modals/issue-close.js +73 -0
- package/addon/components/modals/issue-status.hbs +20 -0
- package/addon/components/modals/issue-status.js +43 -0
- package/addon/components/modals/reset-customer-credentials.js +12 -6
- package/addon/components/modals/update-order-activity.hbs +51 -17
- package/addon/components/modals/update-order-activity.js +59 -5
- package/addon/components/order/details/activity.js +2 -2
- package/addon/components/order/details/proof.hbs +40 -38
- package/addon/components/order/details/proof.js +31 -4
- package/addon/components/order/details.hbs +5 -5
- package/addon/components/order/details.js +19 -1
- package/addon/components/order/header.hbs +1 -1
- package/addon/components/route-list.hbs +0 -12
- package/addon/components/service-rate/details.hbs +1 -1
- package/addon/components/service-rate/form.hbs +1 -1
- package/addon/components/service-rate/form.js +4 -0
- package/addon/components/vendor/personnel-manager.hbs +166 -0
- package/addon/components/vendor/personnel-manager.js +143 -0
- package/addon/components/widget/live-fleet.hbs +1 -0
- package/addon/components/widget/live-fleet.js +2 -7
- package/addon/components/widget/revenue-trend.hbs +1 -1
- package/addon/components/widget/revenue-trend.js +75 -2
- package/addon/controllers/management/contacts/customers/details.js +31 -13
- package/addon/controllers/management/contacts/customers.js +16 -5
- package/addon/controllers/management/contacts/index/details.js +31 -13
- package/addon/controllers/management/contacts/index.js +7 -0
- package/addon/controllers/management/issues/index/details.js +52 -6
- package/addon/controllers/management/vendors/index/details.js +4 -0
- package/addon/controllers/operations/orders/index/details.js +11 -3
- package/addon/controllers/operations/orders/index.js +1 -1
- package/addon/extension.js +19 -12
- package/addon/helpers/format-duration.js +1 -38
- package/addon/initializers/load-leaflet-assets.js +5 -6
- package/addon/initializers/patch-ember-leaflet-tooltip-layer.js +51 -0
- package/addon/routes/management/issues/index/details.js +1 -1
- package/addon/routes.js +1 -0
- package/addon/services/contact-actions.js +231 -1
- package/addon/services/getting-started.js +44 -0
- package/addon/services/issue-actions.js +71 -0
- package/addon/services/leaflet-map-manager.js +2 -0
- package/addon/services/map-adapter/leaflet.js +19 -0
- package/addon/services/service-areas.js +3 -0
- package/addon/styles/fleetops-engine.css +1036 -0
- package/addon/templates/management/contacts/customers/details.hbs +1 -1
- package/addon/templates/management/contacts/index/details.hbs +1 -1
- package/addon/templates/management/contacts.hbs +2 -2
- package/addon/templates/management/issues/index/details/index.hbs +3 -1
- package/addon/templates/management/issues/index/details.hbs +3 -2
- package/addon/templates/management/vendors/index/details/personnel.hbs +1 -2
- package/addon/templates/operations/orders/index/details/index.hbs +2 -2
- package/addon/templates/operations/orders/index.hbs +1 -1
- package/addon/templates/operations/service-rates/index/edit.hbs +1 -0
- package/addon/templates/operations/service-rates/index/new.hbs +1 -0
- package/addon/templates/settings/payments/onboard.hbs +8 -8
- package/addon/utils/leaflet-draw-namespace-guard.js +22 -0
- package/addon/utils/leaflet-plugin-loader.js +219 -0
- package/addon/utils/setup-customer-portal.js +4 -64
- package/app/components/home/getting-started-guidance.js +1 -0
- package/app/components/issue/details/comments.js +1 -0
- package/app/components/issue/details/correspondence.js +1 -0
- package/app/components/issue/details/documents.js +1 -0
- package/app/components/issue/panel-header.js +1 -0
- package/app/components/modals/convert-customer-to-vendor.js +1 -0
- package/app/components/modals/issue-assign.js +1 -0
- package/app/components/modals/issue-close.js +1 -0
- package/app/components/modals/issue-status.js +1 -0
- package/app/components/vendor/personnel-manager.js +1 -0
- package/app/helpers/format-duration.js +1 -1
- package/app/services/getting-started.js +1 -0
- package/app/utils/leaflet-draw-namespace-guard.js +1 -0
- package/app/utils/leaflet-plugin-loader.js +1 -0
- package/composer.json +1 -1
- package/extension.json +1 -1
- package/package.json +5 -5
- package/server/migrations/2026_05_31_000001_add_portal_fields_to_vendor_personnels_table.php +30 -0
- package/server/migrations/2026_06_03_000000_ensure_tags_column_on_issues_table.php +31 -0
- package/server/migrations/2026_06_03_000001_add_order_uuid_to_issues_table.php +25 -0
- package/server/migrations/2026_06_04_000001_backfill_order_transaction_subjects.php +39 -0
- package/server/migrations/2026_06_04_000002_backfill_order_transaction_directions.php +24 -0
- package/server/resources/views/mail/customer-credentials.blade.php +16 -13
- package/server/src/Expansions/UserFilterExpansion.php +28 -0
- package/server/src/Http/Controllers/Api/v1/IssueController.php +2 -0
- package/server/src/Http/Controllers/Api/v1/OrderController.php +26 -25
- package/server/src/Http/Controllers/Api/v1/PlaceController.php +10 -58
- package/server/src/Http/Controllers/Internal/v1/ContactController.php +135 -0
- package/server/src/Http/Controllers/Internal/v1/CustomerController.php +116 -6
- package/server/src/Http/Controllers/Internal/v1/GettingStartedController.php +17 -0
- package/server/src/Http/Controllers/Internal/v1/IssueController.php +251 -0
- package/server/src/Http/Controllers/Internal/v1/OrderController.php +172 -97
- package/server/src/Http/Controllers/Internal/v1/PlaceController.php +10 -68
- package/server/src/Http/Controllers/Internal/v1/VendorController.php +89 -0
- package/server/src/Http/Requests/CreateIssueRequest.php +4 -0
- package/server/src/Http/Requests/UpdateIssueRequest.php +4 -0
- package/server/src/Http/Resources/v1/Index/Order.php +12 -9
- package/server/src/Http/Resources/v1/Issue.php +25 -0
- package/server/src/Http/Resources/v1/Order.php +20 -18
- package/server/src/Mail/CustomerCredentialsMail.php +5 -6
- package/server/src/Models/Contact.php +30 -51
- package/server/src/Models/Issue.php +47 -6
- package/server/src/Models/Order.php +33 -6
- package/server/src/Models/ServiceArea.php +1 -1
- package/server/src/Models/VendorPersonnel.php +10 -1
- package/server/src/Models/Zone.php +1 -1
- package/server/src/Observers/PurchaseRateObserver.php +20 -0
- package/server/src/Observers/ZoneObserver.php +56 -0
- package/server/src/Providers/FleetOpsServiceProvider.php +1 -0
- package/server/src/Support/Analytics/TopDrivers.php +9 -1
- package/server/src/Support/GettingStarted.php +144 -0
- package/server/src/Support/PlaceSearch.php +282 -0
- package/server/src/Support/ResolvesOrderServiceStops.php +22 -12
- package/server/src/routes.php +33 -1
- package/server/tests/AnalyticsRoutesTest.php +8 -0
- package/server/tests/CustomerContactIdentitySafetyTest.php +41 -0
- package/server/tests/DriverVehicleStatusDefaultTest.php +2 -2
- package/server/tests/GettingStartedStatusTest.php +21 -0
- package/server/tests/OrderActivityFlowRegressionTest.php +116 -0
- package/server/tests/ServiceAreaZoneApiShapeTest.php +44 -0
- package/translations/ar-ae.yml +9 -0
- package/translations/bg-bg.yaml +9 -0
- package/translations/en-us.yaml +11 -0
- package/translations/es-pa.yaml +9 -0
- package/translations/fr-fr.yaml +9 -0
- package/translations/mn-mn.yaml +9 -0
- package/translations/pt-br.yaml +9 -0
- package/translations/ru-ru.yaml +9 -0
- package/translations/vi-vn.yaml +9 -0
|
@@ -47,7 +47,6 @@ use Illuminate\Support\Facades\DB;
|
|
|
47
47
|
use Illuminate\Support\Facades\Storage;
|
|
48
48
|
use Illuminate\Support\Facades\Validator;
|
|
49
49
|
use Illuminate\Support\Str;
|
|
50
|
-
use Illuminate\Validation\ValidationException;
|
|
51
50
|
use Maatwebsite\Excel\Facades\Excel;
|
|
52
51
|
|
|
53
52
|
class OrderController extends FleetOpsController
|
|
@@ -676,28 +675,9 @@ class OrderController extends FleetOpsController
|
|
|
676
675
|
// get the next order activity
|
|
677
676
|
$flow = $activity = $order->config()->nextFirstActivity();
|
|
678
677
|
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
$location = $order->getLastLocation();
|
|
683
|
-
|
|
684
|
-
// if multi drop order set first destination
|
|
685
|
-
if ($payload->isMultipleDropOrder) {
|
|
686
|
-
$firstDestination = $payload->waypoints->first();
|
|
687
|
-
|
|
688
|
-
if ($firstDestination) {
|
|
689
|
-
$payload->current_waypoint_uuid = $firstDestination->uuid;
|
|
690
|
-
$payload->save();
|
|
691
|
-
}
|
|
692
|
-
|
|
693
|
-
// update activity for each waypoint and entity
|
|
694
|
-
foreach ($payload->waypointMarkers as $waypointMarker) {
|
|
695
|
-
$waypointMarker->insertActivity($activity, $location);
|
|
696
|
-
}
|
|
697
|
-
|
|
698
|
-
foreach ($payload->entities as $entity) {
|
|
699
|
-
$entity->insertActivity($activity, $location);
|
|
700
|
-
}
|
|
678
|
+
// if multi-stop route set first service stop destination
|
|
679
|
+
if ($this->payloadUsesServiceStopActivity($payload)) {
|
|
680
|
+
$this->ensurePayloadCurrentServiceStop($payload);
|
|
701
681
|
}
|
|
702
682
|
|
|
703
683
|
// update order activity
|
|
@@ -729,9 +709,16 @@ class OrderController extends FleetOpsController
|
|
|
729
709
|
return response()->error('No order found.');
|
|
730
710
|
}
|
|
731
711
|
|
|
732
|
-
$proof
|
|
733
|
-
$
|
|
734
|
-
$activity
|
|
712
|
+
$proof = $request->input('proof');
|
|
713
|
+
$bypassProof = $request->boolean('bypass_proof');
|
|
714
|
+
$activity = $request->array('activity');
|
|
715
|
+
$activity = new Activity($activity, $order->getConfigFlow());
|
|
716
|
+
|
|
717
|
+
$requiresProof = Utils::isActivity($activity)
|
|
718
|
+
&& ($activity->get('require_pod') || ($activity->completesOrder() && $order->pod_required));
|
|
719
|
+
if ($requiresProof && !$proof && !$bypassProof) {
|
|
720
|
+
return response()->error('Proof of delivery is required for this activity or must be explicitly bypassed.', 422);
|
|
721
|
+
}
|
|
735
722
|
|
|
736
723
|
// Handle pickup/dropoff order activity update as normal
|
|
737
724
|
if (Utils::isActivity($activity) && $activity->is('dispatched')) {
|
|
@@ -751,15 +738,20 @@ class OrderController extends FleetOpsController
|
|
|
751
738
|
* @var \Fleetbase\LaravelMysqlSpatial\Types\Point
|
|
752
739
|
*/
|
|
753
740
|
$location = $order->getLastLocation();
|
|
754
|
-
$
|
|
741
|
+
$usesServiceStopActivity = $this->payloadUsesServiceStopActivity($order->payload);
|
|
742
|
+
$isLifecycleActivity = Utils::isActivity($activity) && in_array($activity->code, ['created', 'dispatched', 'started'], true);
|
|
755
743
|
|
|
756
|
-
if (!$
|
|
744
|
+
if (!$usesServiceStopActivity || $isLifecycleActivity) {
|
|
757
745
|
$order->updateActivity($activity, $proof);
|
|
758
746
|
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
747
|
+
if (!$usesServiceStopActivity) {
|
|
748
|
+
foreach ($order->payload->entities as $entity) {
|
|
749
|
+
$entity->insertActivity($activity, $location, $proof);
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
if ($usesServiceStopActivity && $activity->is('started')) {
|
|
754
|
+
$this->ensurePayloadCurrentServiceStop($order->payload);
|
|
763
755
|
}
|
|
764
756
|
|
|
765
757
|
// Handle order completed
|
|
@@ -772,23 +764,6 @@ class OrderController extends FleetOpsController
|
|
|
772
764
|
return new OrderResource($order->refresh());
|
|
773
765
|
}
|
|
774
766
|
|
|
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
767
|
if (!$order->started && in_array($order->status, ['created', 'dispatched'])) {
|
|
793
768
|
return response()->error('Order must be started before waypoint activity can be updated.', 422);
|
|
794
769
|
}
|
|
@@ -836,18 +811,21 @@ class OrderController extends FleetOpsController
|
|
|
836
811
|
return response()->error('No order found.');
|
|
837
812
|
}
|
|
838
813
|
|
|
839
|
-
$
|
|
814
|
+
$order->loadMissing([
|
|
815
|
+
'payload.pickup',
|
|
816
|
+
'payload.dropoff',
|
|
817
|
+
'payload.waypoints',
|
|
818
|
+
'payload.waypointMarkers.place',
|
|
819
|
+
'payload.waypointMarkers.trackingNumber.status',
|
|
820
|
+
]);
|
|
821
|
+
|
|
822
|
+
$canUpdateWaypointActivity = $this->payloadUsesServiceStopActivity($order->payload)
|
|
823
|
+
&& ($order->started || $order->started_at || !in_array($order->status, ['created', 'dispatched']));
|
|
840
824
|
$stop = null;
|
|
841
|
-
if ($
|
|
842
|
-
$
|
|
843
|
-
|
|
844
|
-
|
|
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'));
|
|
825
|
+
if ($canUpdateWaypointActivity) {
|
|
826
|
+
$stop = $request->filled('waypoint')
|
|
827
|
+
? $this->resolveServiceStopFromKey($order->payload, $request->input('waypoint'))
|
|
828
|
+
: $this->payloadCurrentServiceStop($order->payload);
|
|
851
829
|
}
|
|
852
830
|
$orderConfig = $order->ensureOrderConfig();
|
|
853
831
|
if (!$orderConfig) {
|
|
@@ -896,7 +874,7 @@ class OrderController extends FleetOpsController
|
|
|
896
874
|
return response()->error('No order found.');
|
|
897
875
|
}
|
|
898
876
|
|
|
899
|
-
if (!$this->
|
|
877
|
+
if (!$this->payloadUsesServiceStopActivity($order->payload)) {
|
|
900
878
|
return response()->error('Destination can only be changed for multi-waypoint orders.', 422);
|
|
901
879
|
}
|
|
902
880
|
|
|
@@ -917,35 +895,43 @@ class OrderController extends FleetOpsController
|
|
|
917
895
|
*/
|
|
918
896
|
public function capturePhoto(Request $request, string $id, ?string $subjectId = null)
|
|
919
897
|
{
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
898
|
+
$incoming = $this->collectProofPhotoInputs($request);
|
|
899
|
+
$metadata = $request->input('data', []);
|
|
900
|
+
$metadata = is_array($metadata) ? $metadata : [];
|
|
901
|
+
$validator = Validator::make([
|
|
902
|
+
'photos' => $incoming,
|
|
903
|
+
'remarks' => $request->input('remarks'),
|
|
904
|
+
'data' => $metadata,
|
|
905
|
+
], [
|
|
906
|
+
'photos' => 'required|array|min:1',
|
|
907
|
+
'photos.*' => [
|
|
908
|
+
function ($attribute, $value, $fail) {
|
|
909
|
+
if ($value instanceof UploadedFile) {
|
|
910
|
+
$extension = strtolower($value->getClientOriginalExtension() ?: $value->extension());
|
|
911
|
+
if (
|
|
912
|
+
!$value->isValid()
|
|
913
|
+
|| !in_array($extension, ['jpg', 'jpeg', 'png', 'gif', 'webp'])
|
|
914
|
+
|| $value->getSize() > 10 * 1024 * 1024
|
|
915
|
+
) {
|
|
916
|
+
$fail("{$attribute} must be a valid image file <= 10 MB.");
|
|
935
917
|
}
|
|
936
918
|
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
}
|
|
919
|
+
return;
|
|
920
|
+
}
|
|
940
921
|
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
922
|
+
if ($this->isValidBase64ProofPhoto($value)) {
|
|
923
|
+
return;
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
$fail("{$attribute} must be an image file or a valid Base64 string.");
|
|
927
|
+
},
|
|
928
|
+
],
|
|
929
|
+
'remarks' => 'sometimes|string|max:255',
|
|
930
|
+
'data' => 'sometimes|array',
|
|
931
|
+
]);
|
|
932
|
+
|
|
933
|
+
if ($validator->fails()) {
|
|
934
|
+
$errorMessage = collect($validator->errors()->all())->first();
|
|
949
935
|
|
|
950
936
|
return response()->error($errorMessage, 422);
|
|
951
937
|
}
|
|
@@ -960,15 +946,10 @@ class OrderController extends FleetOpsController
|
|
|
960
946
|
return response()->error('Unable to capture photo as proof.', 422);
|
|
961
947
|
}
|
|
962
948
|
|
|
963
|
-
$disk
|
|
964
|
-
$bucket
|
|
965
|
-
$
|
|
966
|
-
$
|
|
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', []);
|
|
949
|
+
$disk = $request->input('disk', config('filesystems.default'));
|
|
950
|
+
$bucket = $request->input("filesystems.disks.{$disk}.bucket", config('filesystems.disks.s3.bucket'));
|
|
951
|
+
$remarks = $request->input('remarks', 'Verified by Photo');
|
|
952
|
+
$data = $metadata;
|
|
972
953
|
|
|
973
954
|
if (empty($incoming)) {
|
|
974
955
|
return response()->error('No photo data to capture.', 422);
|
|
@@ -992,13 +973,107 @@ class OrderController extends FleetOpsController
|
|
|
992
973
|
return new ProofResource($proof);
|
|
993
974
|
}
|
|
994
975
|
|
|
976
|
+
/**
|
|
977
|
+
* Normalize console proof upload payloads into one photo list before validation.
|
|
978
|
+
*/
|
|
979
|
+
protected function collectProofPhotoInputs(Request $request): array
|
|
980
|
+
{
|
|
981
|
+
$incoming = [];
|
|
982
|
+
foreach (['photos', 'photo', 'files', 'file'] as $key) {
|
|
983
|
+
$this->appendProofPhotoInputs($incoming, $request->file($key));
|
|
984
|
+
$this->appendProofPhotoInputs($incoming, $request->input($key));
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
$incoming = array_values(array_filter($incoming, function ($value) {
|
|
988
|
+
return $value instanceof UploadedFile || is_string($value);
|
|
989
|
+
}));
|
|
990
|
+
|
|
991
|
+
return $this->dedupeProofPhotoInputs($incoming);
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
/**
|
|
995
|
+
* Append a nested file/input value to the proof input list.
|
|
996
|
+
*/
|
|
997
|
+
protected function appendProofPhotoInputs(array &$incoming, mixed $value): void
|
|
998
|
+
{
|
|
999
|
+
if ($value === null) {
|
|
1000
|
+
return;
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
if (is_array($value)) {
|
|
1004
|
+
foreach ($value as $nested) {
|
|
1005
|
+
$this->appendProofPhotoInputs($incoming, $nested);
|
|
1006
|
+
}
|
|
1007
|
+
|
|
1008
|
+
return;
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
$incoming[] = $value;
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
/**
|
|
1015
|
+
* Remove duplicate photo inputs exposed under multiple multipart aliases.
|
|
1016
|
+
*/
|
|
1017
|
+
protected function dedupeProofPhotoInputs(array $incoming): array
|
|
1018
|
+
{
|
|
1019
|
+
$seen = [];
|
|
1020
|
+
|
|
1021
|
+
return array_values(array_filter($incoming, function ($value) use (&$seen) {
|
|
1022
|
+
$fingerprint = $this->proofPhotoInputFingerprint($value);
|
|
1023
|
+
if ($fingerprint === null || isset($seen[$fingerprint])) {
|
|
1024
|
+
return false;
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
$seen[$fingerprint] = true;
|
|
1028
|
+
|
|
1029
|
+
return true;
|
|
1030
|
+
}));
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
/**
|
|
1034
|
+
* Build a stable fingerprint for an uploaded file or Base64 proof payload.
|
|
1035
|
+
*/
|
|
1036
|
+
protected function proofPhotoInputFingerprint(mixed $value): ?string
|
|
1037
|
+
{
|
|
1038
|
+
if ($value instanceof UploadedFile) {
|
|
1039
|
+
return hash('sha256', implode('|', [
|
|
1040
|
+
$value->getRealPath(),
|
|
1041
|
+
$value->getSize(),
|
|
1042
|
+
$value->getClientOriginalName(),
|
|
1043
|
+
$value->getClientMimeType(),
|
|
1044
|
+
]));
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1047
|
+
if (is_string($value)) {
|
|
1048
|
+
$payload = Str::contains($value, 'base64,') ? Str::after($value, 'base64,') : $value;
|
|
1049
|
+
|
|
1050
|
+
return hash('sha256', $payload);
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
return null;
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
/**
|
|
1057
|
+
* Determine whether a proof input is a usable Base64 image payload.
|
|
1058
|
+
*/
|
|
1059
|
+
protected function isValidBase64ProofPhoto(mixed $value): bool
|
|
1060
|
+
{
|
|
1061
|
+
if (!is_string($value)) {
|
|
1062
|
+
return false;
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
$payload = Str::contains($value, 'base64,') ? Str::after($value, 'base64,') : $value;
|
|
1066
|
+
|
|
1067
|
+
return base64_decode($payload, true) !== false;
|
|
1068
|
+
}
|
|
1069
|
+
|
|
995
1070
|
/**
|
|
996
1071
|
* Store a proof photo and create its file record.
|
|
997
1072
|
*/
|
|
998
1073
|
protected function storeProofPhoto(Proof $proof, UploadedFile|string $photo, string $disk, string $bucket): File
|
|
999
1074
|
{
|
|
1000
1075
|
$isFile = $photo instanceof UploadedFile;
|
|
1001
|
-
$contents = $isFile ? file_get_contents($photo->getRealPath()) : base64_decode($photo);
|
|
1076
|
+
$contents = $isFile ? file_get_contents($photo->getRealPath()) : base64_decode(Str::contains($photo, 'base64,') ? Str::after($photo, 'base64,') : $photo);
|
|
1002
1077
|
$extension = $isFile ? $photo->getClientOriginalExtension() : 'png';
|
|
1003
1078
|
$contentType = $isFile ? $photo->getClientMimeType() : 'image/png';
|
|
1004
1079
|
$company = session('company');
|
|
@@ -7,10 +7,9 @@ use Fleetbase\FleetOps\Http\Controllers\FleetOpsController;
|
|
|
7
7
|
use Fleetbase\FleetOps\Http\Resources\v1\Place as PlaceResource;
|
|
8
8
|
use Fleetbase\FleetOps\Imports\PlaceImport;
|
|
9
9
|
use Fleetbase\FleetOps\Models\Place;
|
|
10
|
-
use Fleetbase\FleetOps\Support\
|
|
10
|
+
use Fleetbase\FleetOps\Support\PlaceSearch;
|
|
11
11
|
use Fleetbase\Http\Requests\ExportRequest;
|
|
12
12
|
use Fleetbase\Http\Requests\ImportRequest;
|
|
13
|
-
use Fleetbase\LaravelMysqlSpatial\Types\Point;
|
|
14
13
|
use Illuminate\Http\Request;
|
|
15
14
|
use Illuminate\Support\Str;
|
|
16
15
|
use Maatwebsite\Excel\Facades\Excel;
|
|
@@ -50,50 +49,15 @@ class PlaceController extends FleetOpsController
|
|
|
50
49
|
|
|
51
50
|
$query = Place::where('company_uuid', session('company'))
|
|
52
51
|
->whereNull('deleted_at')
|
|
53
|
-
->applyDirectivesForPermissions('fleet-ops list place')
|
|
54
|
-
->search($searchQuery);
|
|
55
|
-
|
|
56
|
-
if ($latitude && $longitude) {
|
|
57
|
-
$point = new Point($latitude, $longitude);
|
|
58
|
-
$query->whereNotNull('location')->whereRaw('
|
|
59
|
-
ST_Y(location) BETWEEN -90 AND 90
|
|
60
|
-
AND ST_X(location) BETWEEN -180 AND 180
|
|
61
|
-
AND NOT (ST_X(location) = 0 AND ST_Y(location) = 0)
|
|
62
|
-
');
|
|
63
|
-
$query->orderByDistanceSphere('location', $point, 'asc');
|
|
64
|
-
} else {
|
|
65
|
-
$query->orderBy('name', 'desc');
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
if ($limit) {
|
|
69
|
-
$query->limit($limit);
|
|
70
|
-
}
|
|
52
|
+
->applyDirectivesForPermissions('fleet-ops list place');
|
|
71
53
|
|
|
72
|
-
$results = $query
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
foreach ($geocodingResults as $result) {
|
|
80
|
-
$results->prepend($result);
|
|
81
|
-
}
|
|
82
|
-
} catch (\Throwable $e) {
|
|
83
|
-
return response()->error($e->getMessage());
|
|
84
|
-
}
|
|
85
|
-
} elseif ($latitude && $longitude) {
|
|
86
|
-
try {
|
|
87
|
-
$geocodingResults = Geocoding::reverseFromCoordinates($latitude, $longitude, $searchQuery);
|
|
88
|
-
|
|
89
|
-
foreach ($geocodingResults as $result) {
|
|
90
|
-
$results->prepend($result);
|
|
91
|
-
}
|
|
92
|
-
} catch (\Throwable $e) {
|
|
93
|
-
return response()->error($e->getMessage());
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
}
|
|
54
|
+
$results = PlaceSearch::search($query, $searchQuery, [
|
|
55
|
+
'limit' => $limit,
|
|
56
|
+
'geo' => $geo,
|
|
57
|
+
'latitude' => $latitude,
|
|
58
|
+
'longitude' => $longitude,
|
|
59
|
+
'no_query_order' => 'name_desc',
|
|
60
|
+
]);
|
|
97
61
|
|
|
98
62
|
return PlaceResource::collection($results);
|
|
99
63
|
}
|
|
@@ -108,29 +72,7 @@ class PlaceController extends FleetOpsController
|
|
|
108
72
|
$searchQuery = $request->searchQuery();
|
|
109
73
|
$latitude = $request->input('latitude', false);
|
|
110
74
|
$longitude = $request->input('longitude', false);
|
|
111
|
-
$results =
|
|
112
|
-
|
|
113
|
-
if ($searchQuery && Geocoding::canGoogleGeocode()) {
|
|
114
|
-
try {
|
|
115
|
-
$geocodingResults = Geocoding::query($searchQuery, $latitude, $longitude);
|
|
116
|
-
|
|
117
|
-
foreach ($geocodingResults as $result) {
|
|
118
|
-
$results->push($result);
|
|
119
|
-
}
|
|
120
|
-
} catch (\Throwable $e) {
|
|
121
|
-
return response()->error($e->getMessage());
|
|
122
|
-
}
|
|
123
|
-
} elseif ($latitude && $longitude) {
|
|
124
|
-
try {
|
|
125
|
-
$geocodingResults = Geocoding::reverseFromCoordinates($latitude, $longitude, $searchQuery);
|
|
126
|
-
|
|
127
|
-
foreach ($geocodingResults as $result) {
|
|
128
|
-
$results->push($result);
|
|
129
|
-
}
|
|
130
|
-
} catch (\Throwable $e) {
|
|
131
|
-
return response()->error($e->getMessage());
|
|
132
|
-
}
|
|
133
|
-
}
|
|
75
|
+
$results = PlaceSearch::geocode($searchQuery, $latitude, $longitude);
|
|
134
76
|
|
|
135
77
|
return response()->json($results)->withHeaders(['Cache-Control' => 'no-cache']);
|
|
136
78
|
}
|
|
@@ -4,9 +4,12 @@ namespace Fleetbase\FleetOps\Http\Controllers\Internal\v1;
|
|
|
4
4
|
|
|
5
5
|
use Fleetbase\FleetOps\Exports\VendorExport;
|
|
6
6
|
use Fleetbase\FleetOps\Http\Controllers\FleetOpsController;
|
|
7
|
+
use Fleetbase\FleetOps\Http\Resources\v1\Contact as ContactResource;
|
|
7
8
|
use Fleetbase\FleetOps\Imports\VendorImport;
|
|
9
|
+
use Fleetbase\FleetOps\Models\Contact;
|
|
8
10
|
use Fleetbase\FleetOps\Models\Driver;
|
|
9
11
|
use Fleetbase\FleetOps\Models\Vendor;
|
|
12
|
+
use Fleetbase\FleetOps\Models\VendorPersonnel;
|
|
10
13
|
use Fleetbase\Http\Requests\ExportRequest;
|
|
11
14
|
use Fleetbase\Http\Requests\ImportRequest;
|
|
12
15
|
use Illuminate\Http\Request;
|
|
@@ -190,4 +193,90 @@ class VendorController extends FleetOpsController
|
|
|
190
193
|
'status' => 'ok',
|
|
191
194
|
]);
|
|
192
195
|
}
|
|
196
|
+
|
|
197
|
+
public function vendorPersonnels(string $vendorId)
|
|
198
|
+
{
|
|
199
|
+
$vendor = Vendor::findByIdOrFail($vendorId);
|
|
200
|
+
|
|
201
|
+
$personnels = VendorPersonnel::where('vendor_uuid', $vendor->uuid)
|
|
202
|
+
->with('contact')
|
|
203
|
+
->latest()
|
|
204
|
+
->get()
|
|
205
|
+
->map(fn (VendorPersonnel $personnel) => $this->vendorPersonnelPayload($personnel));
|
|
206
|
+
|
|
207
|
+
return response()->json(['personnels' => $personnels->values()]);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
public function addVendorPersonnel(Request $request, string $vendorId)
|
|
211
|
+
{
|
|
212
|
+
$vendor = Vendor::findByIdOrFail($vendorId);
|
|
213
|
+
$contact = $this->resolveOrCreatePersonnelContact($request);
|
|
214
|
+
|
|
215
|
+
$personnel = VendorPersonnel::updateOrCreate(
|
|
216
|
+
['vendor_uuid' => $vendor->uuid, 'contact_uuid' => $contact->uuid],
|
|
217
|
+
[
|
|
218
|
+
'role' => $request->input('role', 'member'),
|
|
219
|
+
'status' => $request->input('status', 'active'),
|
|
220
|
+
'invited_by_uuid' => session('user'),
|
|
221
|
+
]
|
|
222
|
+
);
|
|
223
|
+
|
|
224
|
+
if ($request->boolean('create_login', true)) {
|
|
225
|
+
$contact->createUser();
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
return response()->json([
|
|
229
|
+
'personnel' => $this->vendorPersonnelPayload($personnel->load('contact')),
|
|
230
|
+
]);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
public function removeVendorPersonnel(string $vendorId, string $contactId)
|
|
234
|
+
{
|
|
235
|
+
$vendor = Vendor::findByIdOrFail($vendorId);
|
|
236
|
+
$contact = Contact::findByIdOrFail($contactId);
|
|
237
|
+
|
|
238
|
+
VendorPersonnel::where(['vendor_uuid' => $vendor->uuid, 'contact_uuid' => $contact->uuid])->delete();
|
|
239
|
+
|
|
240
|
+
return response()->json(['status' => 'ok']);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
protected function resolveOrCreatePersonnelContact(Request $request): Contact
|
|
244
|
+
{
|
|
245
|
+
$contactId = $request->input('contact');
|
|
246
|
+
if ($contactId) {
|
|
247
|
+
return Contact::where('company_uuid', session('company'))
|
|
248
|
+
->where(function ($query) use ($contactId) {
|
|
249
|
+
$query->where('uuid', $contactId)->orWhere('public_id', $contactId);
|
|
250
|
+
})
|
|
251
|
+
->firstOrFail();
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
return Contact::create([
|
|
255
|
+
'company_uuid' => session('company'),
|
|
256
|
+
'name' => $request->input('name'),
|
|
257
|
+
'email' => $request->input('email'),
|
|
258
|
+
'phone' => $request->input('phone'),
|
|
259
|
+
'type' => 'customer',
|
|
260
|
+
]);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
protected function vendorPersonnelPayload(VendorPersonnel $personnel): array
|
|
264
|
+
{
|
|
265
|
+
$contact = $personnel->contact;
|
|
266
|
+
|
|
267
|
+
return [
|
|
268
|
+
'id' => $contact?->public_id,
|
|
269
|
+
'uuid' => $contact?->uuid,
|
|
270
|
+
'contact_uuid' => $contact?->uuid,
|
|
271
|
+
'public_id' => $contact?->public_id,
|
|
272
|
+
'name' => $contact?->name,
|
|
273
|
+
'email' => $contact?->email,
|
|
274
|
+
'phone' => $contact?->phone,
|
|
275
|
+
'photo_url' => $contact?->photo_url,
|
|
276
|
+
'role' => $personnel->role ?? 'member',
|
|
277
|
+
'status' => $personnel->status ?? 'active',
|
|
278
|
+
'invited_by_uuid' => $personnel->invited_by_uuid,
|
|
279
|
+
'contact' => $contact ? (new ContactResource($contact))->resolve() : null,
|
|
280
|
+
];
|
|
281
|
+
}
|
|
193
282
|
}
|
|
@@ -26,10 +26,14 @@ class CreateIssueRequest extends FleetbaseRequest
|
|
|
26
26
|
return [
|
|
27
27
|
'driver' => ['required'],
|
|
28
28
|
'location' => ['required'],
|
|
29
|
+
'order' => ['nullable', 'exists:orders,public_id'],
|
|
30
|
+
'order_uuid' => ['nullable', 'exists:orders,uuid'],
|
|
29
31
|
'report' => ['required'],
|
|
30
32
|
'category' => ['nullable'],
|
|
31
33
|
'type' => ['nullable'],
|
|
32
34
|
'priority' => ['nullable'],
|
|
35
|
+
'tags' => ['nullable', 'array'],
|
|
36
|
+
'tags.*' => ['string'],
|
|
33
37
|
];
|
|
34
38
|
}
|
|
35
39
|
}
|
|
@@ -13,9 +13,13 @@ class UpdateIssueRequest extends CreateIssueRequest
|
|
|
13
13
|
{
|
|
14
14
|
return [
|
|
15
15
|
'report' => ['required'],
|
|
16
|
+
'order' => ['nullable', 'exists:orders,public_id'],
|
|
17
|
+
'order_uuid' => ['nullable', 'exists:orders,uuid'],
|
|
16
18
|
'category' => ['nullable'],
|
|
17
19
|
'type' => ['nullable'],
|
|
18
20
|
'priority' => ['nullable'],
|
|
21
|
+
'tags' => ['nullable', 'array'],
|
|
22
|
+
'tags.*' => ['string'],
|
|
19
23
|
];
|
|
20
24
|
}
|
|
21
25
|
}
|
|
@@ -44,9 +44,10 @@ class Order extends FleetbaseResource
|
|
|
44
44
|
$isInternal,
|
|
45
45
|
$this->whenLoaded('orderConfig', function () {
|
|
46
46
|
return [
|
|
47
|
-
'
|
|
48
|
-
'
|
|
49
|
-
'
|
|
47
|
+
'uuid' => $this->orderConfig->uuid,
|
|
48
|
+
'public_id' => $this->orderConfig->public_id,
|
|
49
|
+
'name' => $this->orderConfig->name,
|
|
50
|
+
'key' => $this->orderConfig->key,
|
|
50
51
|
];
|
|
51
52
|
})
|
|
52
53
|
),
|
|
@@ -104,12 +105,14 @@ class Order extends FleetbaseResource
|
|
|
104
105
|
}),
|
|
105
106
|
|
|
106
107
|
// Essential scalar fields
|
|
107
|
-
'type'
|
|
108
|
-
'status'
|
|
109
|
-
'adhoc'
|
|
110
|
-
'dispatched'
|
|
111
|
-
'has_driver_assigned'
|
|
112
|
-
'is_scheduled'
|
|
108
|
+
'type' => $this->type,
|
|
109
|
+
'status' => $this->status,
|
|
110
|
+
'adhoc' => (bool) data_get($this, 'adhoc', false),
|
|
111
|
+
'dispatched' => (bool) data_get($this, 'dispatched', false),
|
|
112
|
+
'has_driver_assigned' => $this->when($isInternal, $this->has_driver_assigned),
|
|
113
|
+
'is_scheduled' => $this->when($isInternal, $this->is_scheduled),
|
|
114
|
+
'transaction_amount' => $this->transaction_amount,
|
|
115
|
+
'currency' => $this->transaction_currency,
|
|
113
116
|
|
|
114
117
|
// Timestamps
|
|
115
118
|
'scheduled_at' => $this->scheduled_at,
|