@fleetbase/fleetops-engine 0.6.49 → 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.
Files changed (271) hide show
  1. package/README.md +185 -67
  2. package/addon/components/contact/panel-header.hbs +11 -48
  3. package/addon/components/customer/create-order-form.hbs +2 -2
  4. package/addon/components/customer/details.hbs +1 -2
  5. package/addon/components/customer/form.hbs +62 -50
  6. package/addon/components/customer/form.js +27 -1
  7. package/addon/components/customer/order-form.hbs +2 -2
  8. package/addon/components/customer/orders.hbs +50 -2
  9. package/addon/components/customer/orders.js +67 -5
  10. package/addon/components/display-place.hbs +2 -2
  11. package/addon/components/driver/form.hbs +1 -1
  12. package/addon/components/home/getting-started-guidance.hbs +113 -0
  13. package/addon/components/home/getting-started-guidance.js +107 -0
  14. package/addon/components/issue/details/comments.hbs +3 -0
  15. package/addon/components/issue/details/comments.js +3 -0
  16. package/addon/components/issue/details/correspondence.hbs +9 -0
  17. package/addon/components/issue/details/correspondence.js +83 -0
  18. package/addon/components/issue/details/documents.hbs +69 -0
  19. package/addon/components/issue/details/documents.js +85 -0
  20. package/addon/components/issue/details.hbs +220 -59
  21. package/addon/components/issue/details.js +245 -0
  22. package/addon/components/issue/form.hbs +19 -1
  23. package/addon/components/issue/panel-header.hbs +43 -0
  24. package/addon/components/issue/panel-header.js +22 -0
  25. package/addon/components/issue/timeline.hbs +40 -0
  26. package/addon/components/issue/timeline.js +45 -0
  27. package/addon/components/leaflet-tracking-marker.js +3 -0
  28. package/addon/components/map/drawer/position-listing.js +3 -0
  29. package/addon/components/map/google-live-map.js +6 -2
  30. package/addon/components/map/leaflet-live-map.hbs +48 -14
  31. package/addon/components/map/leaflet-live-map.js +201 -20
  32. package/addon/components/map/order-list-overlay.hbs +4 -4
  33. package/addon/components/modals/convert-customer-to-vendor.hbs +30 -0
  34. package/addon/components/modals/convert-customer-to-vendor.js +55 -0
  35. package/addon/components/modals/issue-assign.hbs +20 -0
  36. package/addon/components/modals/issue-assign.js +49 -0
  37. package/addon/components/modals/issue-close.hbs +30 -0
  38. package/addon/components/modals/issue-close.js +73 -0
  39. package/addon/components/modals/issue-status.hbs +20 -0
  40. package/addon/components/modals/issue-status.js +43 -0
  41. package/addon/components/modals/order-change-destination.hbs +46 -0
  42. package/addon/components/modals/order-change-destination.js +106 -0
  43. package/addon/components/modals/reset-customer-credentials.js +12 -6
  44. package/addon/components/modals/stop-activity.hbs +28 -0
  45. package/addon/components/modals/stop-activity.js +52 -0
  46. package/addon/components/modals/update-order-activity.hbs +78 -2
  47. package/addon/components/modals/update-order-activity.js +197 -5
  48. package/addon/components/orchestrator/resource-panel.hbs +0 -10
  49. package/addon/components/orchestrator/resource-panel.js +2 -4
  50. package/addon/components/order/activity-list.hbs +2 -2
  51. package/addon/components/order/activity-timeline.hbs +31 -29
  52. package/addon/components/order/details/activity.hbs +3 -3
  53. package/addon/components/order/details/activity.js +13 -2
  54. package/addon/components/order/details/documents.js +2 -0
  55. package/addon/components/order/details/proof.hbs +40 -38
  56. package/addon/components/order/details/proof.js +31 -4
  57. package/addon/components/order/details/purchase-rate.hbs +2 -2
  58. package/addon/components/order/details/route.hbs +11 -3
  59. package/addon/components/order/details/route.js +91 -14
  60. package/addon/components/order/details/tracking.hbs +101 -72
  61. package/addon/components/order/details/tracking.js +149 -2
  62. package/addon/components/order/details.hbs +5 -5
  63. package/addon/components/order/details.js +19 -1
  64. package/addon/components/order/form/details.hbs +3 -3
  65. package/addon/components/order/form/documents.js +11 -4
  66. package/addon/components/order/form/route.hbs +108 -104
  67. package/addon/components/order/form/route.js +37 -1
  68. package/addon/components/order/form/service-rate.hbs +2 -2
  69. package/addon/components/order/header.hbs +1 -1
  70. package/addon/components/order-config-manager/activity-flow.hbs +4 -1
  71. package/addon/components/order-config-manager/activity-flow.js +178 -12
  72. package/addon/components/place/address.js +1 -1
  73. package/addon/components/route-list.hbs +17 -29
  74. package/addon/components/route-list.js +42 -10
  75. package/addon/components/service-rate/details.hbs +42 -1
  76. package/addon/components/service-rate/form.hbs +119 -1
  77. package/addon/components/service-rate/form.js +59 -0
  78. package/addon/components/vehicle/form.js +1 -1
  79. package/addon/components/vehicle/pill.hbs +1 -1
  80. package/addon/components/vendor/personnel-manager.hbs +166 -0
  81. package/addon/components/vendor/personnel-manager.js +143 -0
  82. package/addon/components/widget/live-fleet.hbs +1 -0
  83. package/addon/components/widget/live-fleet.js +2 -7
  84. package/addon/components/widget/revenue-trend.hbs +1 -1
  85. package/addon/components/widget/revenue-trend.js +75 -2
  86. package/addon/controllers/management/contacts/customers/details.js +31 -13
  87. package/addon/controllers/management/contacts/customers.js +16 -5
  88. package/addon/controllers/management/contacts/index/details.js +31 -13
  89. package/addon/controllers/management/contacts/index.js +7 -0
  90. package/addon/controllers/management/issues/index/details.js +52 -6
  91. package/addon/controllers/management/vehicles/index/new.js +1 -1
  92. package/addon/controllers/management/vendors/index/details.js +4 -0
  93. package/addon/controllers/operations/orders/index/details.js +15 -7
  94. package/addon/controllers/operations/orders/index.js +15 -1
  95. package/addon/extension.js +19 -12
  96. package/addon/helpers/format-duration.js +1 -38
  97. package/addon/helpers/place-address-html.js +2 -2
  98. package/addon/initializers/load-leaflet-assets.js +5 -6
  99. package/addon/initializers/patch-ember-leaflet-tooltip-layer.js +51 -0
  100. package/addon/routes/management/issues/index/details.js +1 -1
  101. package/addon/routes/operations/service-rates/index.js +1 -1
  102. package/addon/routes.js +1 -0
  103. package/addon/services/contact-actions.js +231 -1
  104. package/addon/services/geofence.js +163 -19
  105. package/addon/services/getting-started.js +44 -0
  106. package/addon/services/issue-actions.js +71 -0
  107. package/addon/services/leaflet-contextmenu-manager.js +27 -0
  108. package/addon/services/leaflet-map-manager.js +2 -0
  109. package/addon/services/map-adapter/leaflet.js +19 -0
  110. package/addon/services/service-areas.js +3 -0
  111. package/addon/services/vehicle-actions.js +1 -1
  112. package/addon/styles/fleetops-engine.css +1127 -1
  113. package/addon/templates/management/contacts/customers/details.hbs +1 -1
  114. package/addon/templates/management/contacts/index/details.hbs +1 -1
  115. package/addon/templates/management/contacts.hbs +2 -2
  116. package/addon/templates/management/issues/index/details/index.hbs +3 -1
  117. package/addon/templates/management/issues/index/details.hbs +3 -2
  118. package/addon/templates/management/vendors/index/details/personnel.hbs +1 -2
  119. package/addon/templates/operations/orders/index/details/index.hbs +2 -2
  120. package/addon/templates/operations/orders/index.hbs +1 -1
  121. package/addon/templates/operations/service-rates/index/edit.hbs +1 -0
  122. package/addon/templates/operations/service-rates/index/new.hbs +1 -0
  123. package/addon/templates/settings/payments/onboard.hbs +8 -8
  124. package/addon/utils/fleet-ops-options.js +2 -1
  125. package/addon/utils/leaflet-draw-namespace-guard.js +22 -0
  126. package/addon/utils/leaflet-plugin-loader.js +219 -0
  127. package/addon/utils/normalize-order-config-flow.js +41 -0
  128. package/addon/utils/place-address-html.js +3 -2
  129. package/addon/utils/route-visualization.js +54 -7
  130. package/addon/utils/setup-customer-portal.js +4 -64
  131. package/app/components/home/getting-started-guidance.js +1 -0
  132. package/app/components/issue/details/comments.js +1 -0
  133. package/app/components/issue/details/correspondence.js +1 -0
  134. package/app/components/issue/details/documents.js +1 -0
  135. package/app/components/issue/panel-header.js +1 -0
  136. package/app/components/modals/convert-customer-to-vendor.js +1 -0
  137. package/app/components/modals/issue-assign.js +1 -0
  138. package/app/components/modals/issue-close.js +1 -0
  139. package/app/components/modals/issue-status.js +1 -0
  140. package/app/components/modals/order-change-destination.js +1 -0
  141. package/app/components/modals/stop-activity.js +1 -0
  142. package/app/components/vendor/personnel-manager.js +1 -0
  143. package/app/helpers/format-duration.js +1 -1
  144. package/app/services/getting-started.js +1 -0
  145. package/app/utils/leaflet-draw-namespace-guard.js +1 -0
  146. package/app/utils/leaflet-plugin-loader.js +1 -0
  147. package/composer.json +1 -1
  148. package/extension.json +1 -1
  149. package/package.json +5 -5
  150. package/server/migrations/2026_05_21_000001_add_geographic_rules_to_service_rate_fees_table.php +37 -0
  151. package/server/migrations/2026_05_25_000001_add_endpoint_tracking_refs_to_payloads_table.php +50 -0
  152. package/server/migrations/2026_05_27_000001_default_driver_vehicle_statuses_to_available.php +48 -0
  153. package/server/migrations/2026_05_31_000001_add_portal_fields_to_vendor_personnels_table.php +30 -0
  154. package/server/migrations/2026_06_03_000000_ensure_tags_column_on_issues_table.php +31 -0
  155. package/server/migrations/2026_06_03_000001_add_order_uuid_to_issues_table.php +25 -0
  156. package/server/migrations/2026_06_04_000001_backfill_order_transaction_subjects.php +39 -0
  157. package/server/migrations/2026_06_04_000002_backfill_order_transaction_directions.php +24 -0
  158. package/server/resources/views/labels/default.php +57 -49
  159. package/server/resources/views/labels/waypoint-label.php +27 -20
  160. package/server/resources/views/mail/customer-credentials.blade.php +16 -13
  161. package/server/seeders/Testing/ConnectivitySeeder.php +1 -0
  162. package/server/seeders/Testing/FleetSeeder.php +22 -22
  163. package/server/seeders/Testing/OrdersSeeder.php +6 -22
  164. package/server/seeders/Testing/VroomOrchestrationSeeder.php +21 -21
  165. package/server/src/Console/Commands/AuditCustomerUserConflicts.php +75 -0
  166. package/server/src/Exceptions/CustomerUserConflictException.php +13 -0
  167. package/server/src/Expansions/UserFilterExpansion.php +28 -0
  168. package/server/src/Http/Controllers/Api/v1/ContactController.php +17 -2
  169. package/server/src/Http/Controllers/Api/v1/CustomerController.php +876 -0
  170. package/server/src/Http/Controllers/Api/v1/DriverController.php +1 -1
  171. package/server/src/Http/Controllers/Api/v1/IssueController.php +2 -0
  172. package/server/src/Http/Controllers/Api/v1/OrderConfigController.php +54 -0
  173. package/server/src/Http/Controllers/Api/v1/OrderController.php +109 -80
  174. package/server/src/Http/Controllers/Api/v1/PlaceController.php +10 -58
  175. package/server/src/Http/Controllers/Api/v1/ServiceAreaController.php +2 -2
  176. package/server/src/Http/Controllers/Api/v1/VehicleController.php +20 -3
  177. package/server/src/Http/Controllers/Internal/v1/ContactController.php +154 -0
  178. package/server/src/Http/Controllers/Internal/v1/CustomerController.php +116 -6
  179. package/server/src/Http/Controllers/Internal/v1/GettingStartedController.php +17 -0
  180. package/server/src/Http/Controllers/Internal/v1/IssueController.php +251 -0
  181. package/server/src/Http/Controllers/Internal/v1/LiveController.php +101 -66
  182. package/server/src/Http/Controllers/Internal/v1/OrderController.php +530 -52
  183. package/server/src/Http/Controllers/Internal/v1/PlaceController.php +10 -68
  184. package/server/src/Http/Controllers/Internal/v1/ServiceQuoteController.php +1 -1
  185. package/server/src/Http/Controllers/Internal/v1/SettingController.php +2 -2
  186. package/server/src/Http/Controllers/Internal/v1/VehicleController.php +54 -1
  187. package/server/src/Http/Controllers/Internal/v1/VendorController.php +89 -0
  188. package/server/src/Http/Middleware/AuthenticateCustomerToken.php +34 -0
  189. package/server/src/Http/Requests/CreateCustomerOrderRequest.php +63 -0
  190. package/server/src/Http/Requests/CreateCustomerRequest.php +48 -0
  191. package/server/src/Http/Requests/CreateDriverRequest.php +1 -1
  192. package/server/src/Http/Requests/CreateIssueRequest.php +4 -0
  193. package/server/src/Http/Requests/CreateVehicleRequest.php +25 -1
  194. package/server/src/Http/Requests/Internal/CreateDriverRequest.php +1 -1
  195. package/server/src/Http/Requests/UpdateIssueRequest.php +4 -0
  196. package/server/src/Http/Requests/VerifyCreateCustomerRequest.php +29 -0
  197. package/server/src/Http/Resources/Internal/v1/OrderConfig.php +41 -0
  198. package/server/src/Http/Resources/v1/Customer.php +79 -0
  199. package/server/src/Http/Resources/v1/Index/Order.php +12 -9
  200. package/server/src/Http/Resources/v1/Issue.php +25 -0
  201. package/server/src/Http/Resources/v1/Order.php +20 -18
  202. package/server/src/Http/Resources/v1/OrderConfig.php +74 -0
  203. package/server/src/Http/Resources/v1/Payload.php +23 -20
  204. package/server/src/Http/Resources/v1/ServiceArea.php +33 -19
  205. package/server/src/Http/Resources/v1/ServiceRateFee.php +7 -0
  206. package/server/src/Listeners/HandleOrderDispatched.php +2 -2
  207. package/server/src/Mail/CustomerCredentialsMail.php +5 -6
  208. package/server/src/Models/Contact.php +122 -66
  209. package/server/src/Models/Customer.php +64 -0
  210. package/server/src/Models/Driver.php +12 -3
  211. package/server/src/Models/Entity.php +2 -2
  212. package/server/src/Models/Issue.php +47 -6
  213. package/server/src/Models/Order.php +62 -10
  214. package/server/src/Models/Payload.php +17 -1
  215. package/server/src/Models/ServiceArea.php +1 -1
  216. package/server/src/Models/ServiceRate.php +273 -1
  217. package/server/src/Models/ServiceRateFee.php +39 -9
  218. package/server/src/Models/TrackingNumber.php +8 -2
  219. package/server/src/Models/Vehicle.php +18 -2
  220. package/server/src/Models/VendorPersonnel.php +10 -1
  221. package/server/src/Models/Waypoint.php +2 -2
  222. package/server/src/Models/Zone.php +1 -1
  223. package/server/src/Notifications/LateDeparture.php +2 -2
  224. package/server/src/Notifications/ProlongedStoppage.php +2 -2
  225. package/server/src/Notifications/RouteDeviation.php +2 -2
  226. package/server/src/Observers/ContactObserver.php +2 -0
  227. package/server/src/Observers/PurchaseRateObserver.php +20 -0
  228. package/server/src/Observers/ServiceRateObserver.php +6 -6
  229. package/server/src/Observers/ZoneObserver.php +56 -0
  230. package/server/src/Orchestration/Engines/VroomOrchestrationEngine.php +1 -1
  231. package/server/src/Providers/FleetOpsServiceProvider.php +2 -0
  232. package/server/src/Support/Analytics/AbstractAnalytics.php +6 -6
  233. package/server/src/Support/Analytics/FuelEfficiency.php +2 -2
  234. package/server/src/Support/Analytics/OrdersByStatus.php +1 -1
  235. package/server/src/Support/Analytics/RevenueTrend.php +0 -1
  236. package/server/src/Support/Analytics/TopDrivers.php +10 -2
  237. package/server/src/Support/CustomerAuth.php +98 -0
  238. package/server/src/Support/GettingStarted.php +144 -0
  239. package/server/src/Support/LabelPdf.php +52 -0
  240. package/server/src/Support/Metrics/AbstractMetric.php +4 -4
  241. package/server/src/Support/PlaceSearch.php +282 -0
  242. package/server/src/Support/ResolvesOrderServiceStops.php +522 -0
  243. package/server/src/Tracking/TrackingContextBuilder.php +41 -38
  244. package/server/src/Tracking/TrackingIntelligenceService.php +109 -9
  245. package/server/src/Tracking/TrackingStop.php +13 -11
  246. package/server/src/routes.php +76 -10
  247. package/server/tests/AnalyticsRoutesTest.php +8 -0
  248. package/server/tests/CapacityAllocationEngineTest.php +10 -10
  249. package/server/tests/CustomerContactIdentitySafetyTest.php +95 -0
  250. package/server/tests/CustomerEndpointTest.php +199 -0
  251. package/server/tests/DriverVehicleStatusDefaultTest.php +39 -0
  252. package/server/tests/GettingStartedStatusTest.php +21 -0
  253. package/server/tests/LabelPdfRenderingTest.php +23 -0
  254. package/server/tests/LiveControllerViewportTest.php +60 -0
  255. package/server/tests/MetricsRegistryTest.php +4 -8
  256. package/server/tests/OrchestrationPayloadBuilderTest.php +2 -2
  257. package/server/tests/OrderActivityFlowRegressionTest.php +116 -0
  258. package/server/tests/OrderConfigEndpointTest.php +86 -0
  259. package/server/tests/OrderFileAttachmentTest.php +20 -0
  260. package/server/tests/ServiceAreaZoneApiShapeTest.php +78 -0
  261. package/server/tests/VehicleDriverAssignmentTest.php +18 -0
  262. package/translations/ar-ae.yml +9 -0
  263. package/translations/bg-bg.yaml +9 -0
  264. package/translations/en-us.yaml +12 -0
  265. package/translations/es-pa.yaml +9 -0
  266. package/translations/fr-fr.yaml +9 -0
  267. package/translations/mn-mn.yaml +9 -0
  268. package/translations/pt-br.yaml +9 -0
  269. package/translations/ru-ru.yaml +9 -0
  270. package/translations/vi-vn.yaml +9 -0
  271. 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,17 @@ 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;
43
50
  use Maatwebsite\Excel\Facades\Excel;
44
51
 
45
52
  class OrderController extends FleetOpsController
46
53
  {
54
+ use ResolvesOrderServiceStops;
55
+
47
56
  /**
48
57
  * The resource to query.
49
58
  *
@@ -63,6 +72,11 @@ class OrderController extends FleetOpsController
63
72
  */
64
73
  public function onAfterUpdate($request, $order)
65
74
  {
75
+ $uploads = $request->array('order.files');
76
+ if ($uploads) {
77
+ $order->attachFiles($uploads);
78
+ }
79
+
66
80
  $waypoints = $request->array('order.payload.waypoints');
67
81
  if ($waypoints) {
68
82
  $order->loadMissing('payload');
@@ -165,14 +179,7 @@ class OrderController extends FleetOpsController
165
179
  ->insertEntities($entities);
166
180
 
167
181
  // If order creation includes files assosciate each to this order
168
- if ($uploads) {
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
- }
182
+ $order->attachFiles($uploads);
176
183
 
177
184
  // save custom field values
178
185
  if (is_array($customFieldValues)) {
@@ -668,28 +675,9 @@ class OrderController extends FleetOpsController
668
675
  // get the next order activity
669
676
  $flow = $activity = $order->config()->nextFirstActivity();
670
677
 
671
- /**
672
- * @var \Fleetbase\LaravelMysqlSpatial\Types\Point
673
- */
674
- $location = $order->getLastLocation();
675
-
676
- // if multi drop order set first destination
677
- if ($payload->isMultipleDropOrder) {
678
- $firstDestination = $payload->waypoints->first();
679
-
680
- if ($firstDestination) {
681
- $payload->current_waypoint_uuid = $firstDestination->uuid;
682
- $payload->save();
683
- }
684
-
685
- // update activity for each waypoint and entity
686
- foreach ($payload->waypointMarkers as $waypointMarker) {
687
- $waypointMarker->insertActivity($activity, $location);
688
- }
689
-
690
- foreach ($payload->entities as $entity) {
691
- $entity->insertActivity($activity, $location);
692
- }
678
+ // if multi-stop route set first service stop destination
679
+ if ($this->payloadUsesServiceStopActivity($payload)) {
680
+ $this->ensurePayloadCurrentServiceStop($payload);
693
681
  }
694
682
 
695
683
  // update order activity
@@ -706,13 +694,31 @@ class OrderController extends FleetOpsController
706
694
  */
707
695
  public function updateActivity(string $id, Request $request)
708
696
  {
709
- $order = Order::findById($id, ['driverAssigned', 'payload.entities']);
697
+ $order = Order::findById($id, [
698
+ 'driverAssigned',
699
+ 'payload.entities',
700
+ 'payload.pickup',
701
+ 'payload.dropoff',
702
+ 'payload.return',
703
+ 'payload.currentWaypoint',
704
+ 'payload.waypoints',
705
+ 'payload.waypointMarkers.place',
706
+ 'payload.waypointMarkers.trackingNumber.status',
707
+ ]);
710
708
  if (!$order) {
711
709
  return response()->error('No order found.');
712
710
  }
713
711
 
714
- $activity = $request->array('activity');
715
- $activity = new Activity($activity, $order->getConfigFlow());
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
+ }
716
722
 
717
723
  // Handle pickup/dropoff order activity update as normal
718
724
  if (Utils::isActivity($activity) && $activity->is('dispatched')) {
@@ -731,29 +737,65 @@ class OrderController extends FleetOpsController
731
737
  /**
732
738
  * @var \Fleetbase\LaravelMysqlSpatial\Types\Point
733
739
  */
734
- $location = $order->getLastLocation();
735
- $order->setStatus($activity->code);
736
- $order->insertActivity($activity, $location);
737
-
738
- // also update for each order entities if not multiple drop order
739
- // all entities will share the same activity status as is one drop order
740
- if (!$order->payload->isMultipleDropOrder) {
741
- foreach ($order->payload->entities as $entity) {
742
- $entity->insertActivity($activity, $location);
740
+ $location = $order->getLastLocation();
741
+ $usesServiceStopActivity = $this->payloadUsesServiceStopActivity($order->payload);
742
+ $isLifecycleActivity = Utils::isActivity($activity) && in_array($activity->code, ['created', 'dispatched', 'started'], true);
743
+
744
+ if (!$usesServiceStopActivity || $isLifecycleActivity) {
745
+ $order->updateActivity($activity, $proof);
746
+
747
+ if (!$usesServiceStopActivity) {
748
+ foreach ($order->payload->entities as $entity) {
749
+ $entity->insertActivity($activity, $location, $proof);
750
+ }
743
751
  }
752
+
753
+ if ($usesServiceStopActivity && $activity->is('started')) {
754
+ $this->ensurePayloadCurrentServiceStop($order->payload);
755
+ }
756
+
757
+ // Handle order completed
758
+ if (Utils::isActivity($activity) && $activity->completesOrder() && $order->driverAssigned) {
759
+ // unset from driver current job
760
+ $order->driverAssigned->unassignCurrentOrder();
761
+ $order->complete($this->resolveProof($proof));
762
+ }
763
+
764
+ return new OrderResource($order->refresh());
744
765
  }
745
766
 
746
- // Handle order completed
747
- if (Utils::isActivity($activity) && $activity->completesOrder() && $order->driverAssigned) {
748
- // unset from driver current job
749
- $order->driverAssigned->unassignCurrentOrder();
750
- $order->complete();
767
+ if (!$order->started && in_array($order->status, ['created', 'dispatched'])) {
768
+ return response()->error('Order must be started before waypoint activity can be updated.', 422);
751
769
  }
752
770
 
753
- // Fire activity events
754
- $activity->fireEvents($order);
771
+ $currentStop = $this->ensurePayloadCurrentServiceStop($order->payload);
772
+ $isCompletingCurrentStop = Utils::isActivity($activity) && $activity->complete();
773
+
774
+ $this->updateCurrentServiceStopActivity($order, $activity, $location, $proof);
775
+
776
+ if (Utils::isActivity($activity) && $activity->is('canceled')) {
777
+ if ($order->driverAssigned) {
778
+ $order->driverAssigned->unassignCurrentOrder();
779
+ }
755
780
 
756
- return response()->json(['status' => 'ok']);
781
+ $order->cancel();
782
+
783
+ return new OrderResource($order->refresh());
784
+ }
785
+
786
+ if (Utils::isActivity($activity) && $activity->completesOrder()) {
787
+ $nextStop = $isCompletingCurrentStop ? $this->advanceCurrentServiceStopDestination($order, $order->payload) : null;
788
+
789
+ if (!$nextStop || !$currentStop) {
790
+ if ($order->driverAssigned) {
791
+ $order->driverAssigned->unassignCurrentOrder();
792
+ }
793
+
794
+ $order->complete($this->resolveProof($proof));
795
+ }
796
+ }
797
+
798
+ return new OrderResource($order->refresh());
757
799
  }
758
800
 
759
801
  /**
@@ -769,12 +811,27 @@ class OrderController extends FleetOpsController
769
811
  return response()->error('No order found.');
770
812
  }
771
813
 
772
- $waypoint = $request->filled('waypoint') ? Waypoint::findByPlace($request->input('waypoint'), $order) : null;
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']));
824
+ $stop = null;
825
+ if ($canUpdateWaypointActivity) {
826
+ $stop = $request->filled('waypoint')
827
+ ? $this->resolveServiceStopFromKey($order->payload, $request->input('waypoint'))
828
+ : $this->payloadCurrentServiceStop($order->payload);
829
+ }
773
830
  $orderConfig = $order->ensureOrderConfig();
774
831
  if (!$orderConfig) {
775
832
  return response()->error('No order config found for order.');
776
833
  }
777
- $activities = $orderConfig->nextActivity($waypoint);
834
+ $activities = $stop ? $this->nextActivitiesForServiceStop($order, $order->payload, $stop) : $orderConfig->nextActivity();
778
835
 
779
836
  // If activity is to complete order add proof of delivery properties if required
780
837
  // This is a temporary fix until activity is updated to handle POD on it's own
@@ -794,6 +851,427 @@ class OrderController extends FleetOpsController
794
851
  return response()->json($activities);
795
852
  }
796
853
 
854
+ /**
855
+ * Mark a waypoint as the current destination for a multi-waypoint order.
856
+ *
857
+ * @return \Fleetbase\Http\Resources\v1\Order
858
+ */
859
+ public function setDestination(string $id, string $placeId)
860
+ {
861
+ $order = Order::findById($id, [
862
+ 'payload.pickup',
863
+ 'payload.dropoff',
864
+ 'payload.return',
865
+ 'payload.waypoints',
866
+ 'payload.waypointMarkers.place',
867
+ 'payload.waypointMarkers.trackingNumber.status',
868
+ 'driverAssigned',
869
+ 'vehicleAssigned',
870
+ 'customer',
871
+ 'facilitator',
872
+ ]);
873
+ if (!$order) {
874
+ return response()->error('No order found.');
875
+ }
876
+
877
+ if (!$this->payloadUsesServiceStopActivity($order->payload)) {
878
+ return response()->error('Destination can only be changed for multi-waypoint orders.', 422);
879
+ }
880
+
881
+ $stop = $this->resolveServiceStopFromKey($order->payload, $placeId);
882
+ if (!$stop) {
883
+ return response()->error('Place resource is not a valid route destination.', 422);
884
+ }
885
+
886
+ $this->setPayloadCurrentServiceStop($order->payload, $stop);
887
+
888
+ return new OrderResource($order->refresh());
889
+ }
890
+
891
+ /**
892
+ * Capture one or more photos for order/waypoint proof of delivery.
893
+ *
894
+ * @return ProofResource|\Illuminate\Http\Response
895
+ */
896
+ public function capturePhoto(Request $request, string $id, ?string $subjectId = null)
897
+ {
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.");
917
+ }
918
+
919
+ return;
920
+ }
921
+
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();
935
+
936
+ return response()->error($errorMessage, 422);
937
+ }
938
+
939
+ $order = Order::findById($id, ['payload.pickup', 'payload.dropoff', 'payload.return', 'payload.waypoints', 'payload.waypointMarkers.place']);
940
+ if (!$order) {
941
+ return response()->error('No order found.');
942
+ }
943
+
944
+ $subject = $this->resolveProofSubject($order, $subjectId);
945
+ if (!$subject) {
946
+ return response()->error('Unable to capture photo as proof.', 422);
947
+ }
948
+
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;
953
+
954
+ if (empty($incoming)) {
955
+ return response()->error('No photo data to capture.', 422);
956
+ }
957
+
958
+ foreach ($incoming as $item) {
959
+ $proof = Proof::create([
960
+ 'company_uuid' => session('company'),
961
+ 'order_uuid' => $order->uuid,
962
+ 'subject_uuid' => $subject->uuid,
963
+ 'subject_type' => Utils::getModelClassName($subject),
964
+ 'remarks' => $remarks,
965
+ 'raw_data' => $item instanceof UploadedFile ? null : $item,
966
+ 'data' => $data,
967
+ ]);
968
+
969
+ $file = $this->storeProofPhoto($proof, $item, $disk, $bucket);
970
+ $proof->update(['file_uuid' => $file->uuid]);
971
+ }
972
+
973
+ return new ProofResource($proof);
974
+ }
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
+
1070
+ /**
1071
+ * Store a proof photo and create its file record.
1072
+ */
1073
+ protected function storeProofPhoto(Proof $proof, UploadedFile|string $photo, string $disk, string $bucket): File
1074
+ {
1075
+ $isFile = $photo instanceof UploadedFile;
1076
+ $contents = $isFile ? file_get_contents($photo->getRealPath()) : base64_decode(Str::contains($photo, 'base64,') ? Str::after($photo, 'base64,') : $photo);
1077
+ $extension = $isFile ? $photo->getClientOriginalExtension() : 'png';
1078
+ $contentType = $isFile ? $photo->getClientMimeType() : 'image/png';
1079
+ $company = session('company');
1080
+ $path = "uploads/{$company}/photos/{$proof->public_id}.{$extension}";
1081
+
1082
+ Storage::disk($disk)->put($path, $contents);
1083
+
1084
+ return File::create([
1085
+ 'company_uuid' => $company,
1086
+ 'uploader_uuid' => session('user'),
1087
+ 'name' => basename($path),
1088
+ 'original_filename' => basename($path),
1089
+ 'extension' => $extension,
1090
+ 'content_type' => $contentType,
1091
+ 'path' => $path,
1092
+ 'bucket' => $bucket,
1093
+ 'type' => 'photo',
1094
+ 'size' => strlen($contents),
1095
+ ])->setKey($proof);
1096
+ }
1097
+
1098
+ /**
1099
+ * Resolve the subject that proof belongs to.
1100
+ */
1101
+ protected function resolveProofSubject(Order $order, ?string $subjectId = null): Order|Waypoint|Place|null
1102
+ {
1103
+ if (!$subjectId) {
1104
+ return $order;
1105
+ }
1106
+
1107
+ $stop = $this->resolveServiceStopFromKey($order->payload, $subjectId);
1108
+ if ($stop) {
1109
+ return ($stop['waypoint'] ?? null) instanceof Waypoint ? $stop['waypoint'] : ($stop['place'] ?? $order);
1110
+ }
1111
+
1112
+ return Waypoint::withoutGlobalScopes()
1113
+ ->where('payload_uuid', $order->payload_uuid)
1114
+ ->where(function ($query) use ($subjectId) {
1115
+ $query
1116
+ ->where('public_id', $subjectId)
1117
+ ->orWhere('uuid', $subjectId)
1118
+ ->orWhereHas('place', function ($query) use ($subjectId) {
1119
+ $query->where('public_id', $subjectId)->orWhere('uuid', $subjectId);
1120
+ });
1121
+ })
1122
+ ->first();
1123
+ }
1124
+
1125
+ /**
1126
+ * Resolve proof to the model expected by order completion.
1127
+ */
1128
+ protected function resolveProof($proof): ?Proof
1129
+ {
1130
+ if ($proof instanceof Proof) {
1131
+ return $proof;
1132
+ }
1133
+
1134
+ if (is_string($proof)) {
1135
+ return Proof::where('public_id', $proof)->orWhere('uuid', $proof)->first();
1136
+ }
1137
+
1138
+ return null;
1139
+ }
1140
+
1141
+ /**
1142
+ * Determine whether the payload should use waypoint-scoped activity.
1143
+ */
1144
+ protected function payloadHasWaypoints(?Payload $payload): bool
1145
+ {
1146
+ if (!$payload) {
1147
+ return false;
1148
+ }
1149
+
1150
+ $payload->loadMissing('waypoints');
1151
+
1152
+ return $payload->waypoints->isNotEmpty();
1153
+ }
1154
+
1155
+ /**
1156
+ * Insert an activity on the payload's current waypoint and its destination entities.
1157
+ */
1158
+ protected function updateCurrentWaypointActivity(?Payload $payload, Activity $activity, $location = null, $proof = null): ?Waypoint
1159
+ {
1160
+ if (!$payload || !Utils::isActivity($activity) || !$location) {
1161
+ return null;
1162
+ }
1163
+
1164
+ $payload->loadMissing(['order', 'waypointMarkers', 'entities']);
1165
+ $currentWaypoint = $payload->waypointMarkers->firstWhere('place_uuid', $payload->current_waypoint_uuid);
1166
+ if (!$currentWaypoint) {
1167
+ return null;
1168
+ }
1169
+
1170
+ $currentWaypoint->insertActivity($activity, $location, $proof);
1171
+ $activity->fireEvents($payload->order, $currentWaypoint);
1172
+
1173
+ $entities = $payload->entities->where('destination_uuid', $payload->current_waypoint_uuid);
1174
+ foreach ($entities as $entity) {
1175
+ $entity->insertActivity($activity, $location, $proof);
1176
+ if ($activity->complete()) {
1177
+ event(new EntityCompleted($entity, $activity));
1178
+ } else {
1179
+ event(new EntityActivityChanged($entity, $activity));
1180
+ }
1181
+ }
1182
+
1183
+ if ($activity->complete()) {
1184
+ event(new WaypointCompleted($currentWaypoint, $activity));
1185
+ } else {
1186
+ event(new WaypointActivityChanged($currentWaypoint, $activity));
1187
+ }
1188
+
1189
+ return $currentWaypoint;
1190
+ }
1191
+
1192
+ /**
1193
+ * Reload waypoint marker status and decide whether every stop is complete.
1194
+ */
1195
+ protected function allWaypointMarkersComplete(?Payload $payload): bool
1196
+ {
1197
+ if (!$payload) {
1198
+ return false;
1199
+ }
1200
+
1201
+ $waypointMarkers = $this->freshWaypointMarkers($payload);
1202
+
1203
+ return $waypointMarkers->isNotEmpty() && $waypointMarkers->every(fn (Waypoint $waypoint) => $this->waypointMarkerIsComplete($waypoint));
1204
+ }
1205
+
1206
+ /**
1207
+ * Move the current destination to the next incomplete waypoint.
1208
+ */
1209
+ protected function advanceCurrentWaypointDestination(?Payload $payload): ?Waypoint
1210
+ {
1211
+ if (!$payload) {
1212
+ return null;
1213
+ }
1214
+
1215
+ $nextWaypoint = $this->freshWaypointMarkers($payload)
1216
+ ->sortBy('order')
1217
+ ->first(function (Waypoint $waypoint) use ($payload) {
1218
+ return !$this->waypointMarkerIsComplete($waypoint) && $waypoint->place_uuid !== $payload->current_waypoint_uuid;
1219
+ });
1220
+
1221
+ if (!$nextWaypoint) {
1222
+ return null;
1223
+ }
1224
+
1225
+ $payload->setCurrentWaypoint($nextWaypoint);
1226
+ $payload->setRelation('currentWaypoint', $nextWaypoint->getPlace());
1227
+ $payload->setRelation('currentWaypointMarker', $nextWaypoint);
1228
+
1229
+ return $nextWaypoint;
1230
+ }
1231
+
1232
+ /**
1233
+ * Reload waypoint markers with current tracking status.
1234
+ */
1235
+ protected function freshWaypointMarkers(Payload $payload)
1236
+ {
1237
+ $payload->unsetRelation('waypointMarkers');
1238
+ $payload->load(['waypointMarkers.trackingNumber.status']);
1239
+
1240
+ return $payload->waypointMarkers;
1241
+ }
1242
+
1243
+ /**
1244
+ * Determine whether a waypoint marker's latest status is complete.
1245
+ */
1246
+ protected function waypointMarkerIsComplete(Waypoint $waypoint): bool
1247
+ {
1248
+ if (!$waypoint->tracking_number_uuid) {
1249
+ return false;
1250
+ }
1251
+
1252
+ return (bool) data_get($this->trackingNumberStatus($waypoint->tracking_number_uuid), 'complete', false);
1253
+ }
1254
+
1255
+ /**
1256
+ * Determine whether the payload's current waypoint already has an activity row.
1257
+ */
1258
+ protected function payloadHasCurrentWaypointActivity(?Payload $payload, Activity $activity): bool
1259
+ {
1260
+ if (!$payload || !$payload->current_waypoint_uuid) {
1261
+ return false;
1262
+ }
1263
+
1264
+ $payload->loadMissing('waypointMarkers');
1265
+ $currentWaypoint = $payload->waypointMarkers->firstWhere('place_uuid', $payload->current_waypoint_uuid);
1266
+ if (!$currentWaypoint || !$currentWaypoint->tracking_number_uuid) {
1267
+ return false;
1268
+ }
1269
+
1270
+ return TrackingStatus::where('tracking_number_uuid', $currentWaypoint->tracking_number_uuid)
1271
+ ->where('code', TrackingStatus::prepareCode($activity->code))
1272
+ ->exists();
1273
+ }
1274
+
797
1275
  /**
798
1276
  * Finds and responds with the orders next activity update based on the orders configuration.
799
1277
  *