@fleetbase/fleetops-engine 0.6.48 → 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.
Files changed (266) hide show
  1. package/addon/components/customer/create-order-form.hbs +2 -2
  2. package/addon/components/customer/form.hbs +2 -2
  3. package/addon/components/customer/order-form.hbs +2 -2
  4. package/addon/components/customer/orders.hbs +2 -2
  5. package/addon/components/display-place.hbs +2 -2
  6. package/addon/components/driver/details.hbs +5 -1
  7. package/addon/components/driver/form.hbs +11 -1
  8. package/addon/components/fleet-listing-panel.hbs +2 -2
  9. package/addon/components/fleet-listing-panel.js +10 -0
  10. package/addon/components/map/google-live-map.js +18 -75
  11. package/addon/components/map/leaflet-live-map.hbs +187 -37
  12. package/addon/components/map/leaflet-live-map.js +184 -20
  13. package/addon/components/modals/order-change-destination.hbs +46 -0
  14. package/addon/components/modals/order-change-destination.js +106 -0
  15. package/addon/components/modals/stop-activity.hbs +28 -0
  16. package/addon/components/modals/stop-activity.js +52 -0
  17. package/addon/components/modals/update-order-activity.hbs +45 -3
  18. package/addon/components/modals/update-order-activity.js +143 -5
  19. package/addon/components/orchestrator/phase-builder.hbs +21 -0
  20. package/addon/components/orchestrator/phase-builder.js +13 -0
  21. package/addon/components/orchestrator/resource-panel.hbs +0 -10
  22. package/addon/components/orchestrator/resource-panel.js +2 -4
  23. package/addon/components/orchestrator-workbench.js +1 -0
  24. package/addon/components/order/activity-list.hbs +2 -2
  25. package/addon/components/order/activity-timeline.hbs +31 -29
  26. package/addon/components/order/details/activity.hbs +3 -3
  27. package/addon/components/order/details/activity.js +11 -0
  28. package/addon/components/order/details/documents.js +2 -0
  29. package/addon/components/order/details/purchase-rate.hbs +2 -2
  30. package/addon/components/order/details/route.hbs +11 -3
  31. package/addon/components/order/details/route.js +91 -14
  32. package/addon/components/order/details/tracking.hbs +101 -72
  33. package/addon/components/order/details/tracking.js +149 -2
  34. package/addon/components/order/form/details.hbs +3 -3
  35. package/addon/components/order/form/documents.js +11 -4
  36. package/addon/components/order/form/route.hbs +108 -104
  37. package/addon/components/order/form/route.js +37 -1
  38. package/addon/components/order/form/service-rate.hbs +2 -2
  39. package/addon/components/order/form.hbs +2 -2
  40. package/addon/components/order-config-manager/activity-flow.hbs +4 -1
  41. package/addon/components/order-config-manager/activity-flow.js +178 -12
  42. package/addon/components/place/address.js +1 -1
  43. package/addon/components/route-list.hbs +17 -17
  44. package/addon/components/route-list.js +42 -10
  45. package/addon/components/service-rate/details.hbs +41 -0
  46. package/addon/components/service-rate/form.hbs +118 -0
  47. package/addon/components/service-rate/form.js +55 -0
  48. package/addon/components/vehicle/form.js +1 -1
  49. package/addon/components/vehicle/pill.hbs +1 -1
  50. package/addon/components/widget/fleet-ops-key-metrics.hbs +19 -17
  51. package/addon/components/widget/fleet-ops-key-metrics.js +26 -0
  52. package/addon/components/widget/fuel-efficiency.hbs +26 -0
  53. package/addon/components/widget/fuel-efficiency.js +43 -0
  54. package/addon/components/widget/geofence-violations.hbs +44 -0
  55. package/addon/components/widget/geofence-violations.js +55 -0
  56. package/addon/components/widget/issues-insights.hbs +43 -0
  57. package/addon/components/widget/issues-insights.js +62 -0
  58. package/addon/components/widget/kpi-active-orders.hbs +10 -0
  59. package/addon/components/widget/kpi-active-orders.js +5 -0
  60. package/addon/components/widget/kpi-aov.hbs +10 -0
  61. package/addon/components/widget/kpi-aov.js +5 -0
  62. package/addon/components/widget/kpi-distance.hbs +10 -0
  63. package/addon/components/widget/kpi-distance.js +5 -0
  64. package/addon/components/widget/kpi-drivers-online.hbs +10 -0
  65. package/addon/components/widget/kpi-drivers-online.js +5 -0
  66. package/addon/components/widget/kpi-earnings.hbs +10 -0
  67. package/addon/components/widget/kpi-earnings.js +5 -0
  68. package/addon/components/widget/kpi-open-issues.hbs +11 -0
  69. package/addon/components/widget/kpi-open-issues.js +5 -0
  70. package/addon/components/widget/kpi-tile.hbs +59 -0
  71. package/addon/components/widget/kpi-tile.js +168 -0
  72. package/addon/components/widget/live-fleet.hbs +122 -0
  73. package/addon/components/widget/live-fleet.js +365 -0
  74. package/addon/components/widget/maintenance-overview.hbs +45 -0
  75. package/addon/components/widget/maintenance-overview.js +40 -0
  76. package/addon/components/widget/on-time-delivery.hbs +27 -0
  77. package/addon/components/widget/on-time-delivery.js +73 -0
  78. package/addon/components/widget/operations-pulse.hbs +51 -0
  79. package/addon/components/widget/operations-pulse.js +173 -0
  80. package/addon/components/widget/orders-by-status.hbs +26 -0
  81. package/addon/components/widget/orders-by-status.js +54 -0
  82. package/addon/components/widget/revenue-trend.hbs +34 -0
  83. package/addon/components/widget/revenue-trend.js +67 -0
  84. package/addon/components/widget/top-drivers.hbs +44 -0
  85. package/addon/components/widget/top-drivers.js +54 -0
  86. package/addon/controllers/management/vehicles/index/new.js +1 -1
  87. package/addon/controllers/operations/orders/index/details.js +4 -4
  88. package/addon/controllers/operations/orders/index.js +14 -0
  89. package/addon/controllers/settings/routing.js +26 -0
  90. package/addon/extension.js +172 -3
  91. package/addon/helpers/place-address-html.js +2 -2
  92. package/addon/routes/operations/service-rates/index.js +1 -1
  93. package/addon/services/geofence.js +163 -19
  94. package/addon/services/leaflet-contextmenu-manager.js +27 -0
  95. package/addon/services/map-adapter/google.js +13 -2
  96. package/addon/services/vehicle-actions.js +1 -1
  97. package/addon/styles/fleetops-engine.css +205 -88
  98. package/addon/templates/maintenance/equipment/index.hbs +1 -1
  99. package/addon/templates/maintenance/parts/index.hbs +1 -1
  100. package/addon/templates/maintenance/schedules/index.hbs +1 -1
  101. package/addon/templates/management/vehicles/index.hbs +1 -1
  102. package/addon/templates/settings/avatars.hbs +1 -1
  103. package/addon/templates/settings/custom-fields.hbs +1 -1
  104. package/addon/templates/settings/map.hbs +1 -1
  105. package/addon/templates/settings/navigator-app.hbs +1 -1
  106. package/addon/templates/settings/notifications.hbs +1 -1
  107. package/addon/templates/settings/orchestrator.hbs +1 -1
  108. package/addon/templates/settings/payments/index.hbs +1 -1
  109. package/addon/templates/settings/payments/onboard.hbs +1 -1
  110. package/addon/templates/settings/routing.hbs +22 -2
  111. package/addon/templates/settings/scheduling.hbs +1 -1
  112. package/addon/utils/fleet-ops-options.js +2 -1
  113. package/addon/utils/live-map-card-content.js +171 -0
  114. package/addon/utils/normalize-order-config-flow.js +41 -0
  115. package/addon/utils/place-address-html.js +3 -2
  116. package/addon/utils/route-visualization.js +54 -7
  117. package/app/components/modals/order-change-destination.js +1 -0
  118. package/app/components/modals/stop-activity.js +1 -0
  119. package/app/components/widget/fuel-efficiency.js +1 -0
  120. package/app/components/widget/geofence-violations.js +1 -0
  121. package/app/components/widget/issues-insights.js +1 -0
  122. package/app/components/widget/kpi-active-orders.js +1 -0
  123. package/app/components/widget/kpi-aov.js +1 -0
  124. package/app/components/widget/kpi-distance.js +1 -0
  125. package/app/components/widget/kpi-drivers-online.js +1 -0
  126. package/app/components/widget/kpi-earnings.js +1 -0
  127. package/app/components/widget/kpi-open-issues.js +1 -0
  128. package/app/components/widget/kpi-tile.js +1 -0
  129. package/app/components/widget/live-fleet.js +1 -0
  130. package/app/components/widget/maintenance-overview.js +1 -0
  131. package/app/components/widget/on-time-delivery.js +1 -0
  132. package/app/components/widget/operations-pulse.js +1 -0
  133. package/app/components/widget/orders-by-status.js +1 -0
  134. package/app/components/widget/revenue-trend.js +1 -0
  135. package/app/components/widget/top-drivers.js +1 -0
  136. package/app/utils/live-map-card-content.js +1 -0
  137. package/composer.json +1 -1
  138. package/extension.json +1 -1
  139. package/package.json +3 -3
  140. package/pnpm-workspace.yaml +1 -0
  141. package/server/config/telematics.php +111 -1
  142. package/server/migrations/2026_05_18_000001_add_license_expiry_to_drivers_table.php +25 -0
  143. package/server/migrations/2026_05_21_000001_add_geographic_rules_to_service_rate_fees_table.php +37 -0
  144. package/server/migrations/2026_05_25_000001_add_endpoint_tracking_refs_to_payloads_table.php +50 -0
  145. package/server/migrations/2026_05_27_000001_default_driver_vehicle_statuses_to_available.php +48 -0
  146. package/server/resources/views/labels/default.php +57 -49
  147. package/server/resources/views/labels/waypoint-label.php +27 -20
  148. package/server/seeders/Testing/Concerns/SeedsTestingData.php +183 -0
  149. package/server/seeders/Testing/ConnectivitySeeder.php +213 -0
  150. package/server/seeders/Testing/FleetSeeder.php +341 -0
  151. package/server/seeders/Testing/MaintenanceSeeder.php +238 -0
  152. package/server/seeders/Testing/NetworkSeeder.php +250 -0
  153. package/server/seeders/Testing/OrdersSeeder.php +198 -0
  154. package/server/seeders/Testing/TestingSeeder.php +40 -0
  155. package/server/seeders/{VroomOrchestrationSeeder.php → Testing/VroomOrchestrationSeeder.php} +29 -22
  156. package/server/src/Console/Commands/AuditCustomerUserConflicts.php +75 -0
  157. package/server/src/Console/Commands/ProcessOperationalAlerts.php +279 -0
  158. package/server/src/Exceptions/CustomerUserConflictException.php +13 -0
  159. package/server/src/Http/Controllers/Api/v1/ContactController.php +17 -2
  160. package/server/src/Http/Controllers/Api/v1/CustomerController.php +876 -0
  161. package/server/src/Http/Controllers/Api/v1/DriverController.php +1 -1
  162. package/server/src/Http/Controllers/Api/v1/OrderConfigController.php +54 -0
  163. package/server/src/Http/Controllers/Api/v1/OrderController.php +98 -70
  164. package/server/src/Http/Controllers/Api/v1/ServiceAreaController.php +2 -2
  165. package/server/src/Http/Controllers/Api/v1/VehicleController.php +20 -3
  166. package/server/src/Http/Controllers/Internal/v1/AnalyticsController.php +119 -0
  167. package/server/src/Http/Controllers/Internal/v1/ContactController.php +19 -0
  168. package/server/src/Http/Controllers/Internal/v1/LiveController.php +101 -66
  169. package/server/src/Http/Controllers/Internal/v1/MetricsController.php +82 -3
  170. package/server/src/Http/Controllers/Internal/v1/OrderController.php +441 -32
  171. package/server/src/Http/Controllers/Internal/v1/ServiceQuoteController.php +1 -1
  172. package/server/src/Http/Controllers/Internal/v1/SettingController.php +46 -1
  173. package/server/src/Http/Controllers/Internal/v1/VehicleController.php +54 -1
  174. package/server/src/Http/Middleware/AuthenticateCustomerToken.php +34 -0
  175. package/server/src/Http/Requests/CreateCustomerOrderRequest.php +63 -0
  176. package/server/src/Http/Requests/CreateCustomerRequest.php +48 -0
  177. package/server/src/Http/Requests/CreateDriverRequest.php +1 -1
  178. package/server/src/Http/Requests/CreateVehicleRequest.php +25 -1
  179. package/server/src/Http/Requests/Internal/CreateDriverRequest.php +1 -1
  180. package/server/src/Http/Requests/VerifyCreateCustomerRequest.php +29 -0
  181. package/server/src/Http/Resources/Internal/v1/OrderConfig.php +41 -0
  182. package/server/src/Http/Resources/v1/Customer.php +79 -0
  183. package/server/src/Http/Resources/v1/Driver.php +2 -0
  184. package/server/src/Http/Resources/v1/Fleet.php +4 -0
  185. package/server/src/Http/Resources/v1/Index/Driver.php +48 -0
  186. package/server/src/Http/Resources/v1/Index/Vehicle.php +74 -1
  187. package/server/src/Http/Resources/v1/OrderConfig.php +74 -0
  188. package/server/src/Http/Resources/v1/Payload.php +23 -20
  189. package/server/src/Http/Resources/v1/ServiceArea.php +33 -19
  190. package/server/src/Http/Resources/v1/ServiceRateFee.php +7 -0
  191. package/server/src/Listeners/HandleOrderDispatched.php +2 -2
  192. package/server/src/Models/Contact.php +92 -15
  193. package/server/src/Models/Customer.php +64 -0
  194. package/server/src/Models/Driver.php +14 -3
  195. package/server/src/Models/Entity.php +2 -2
  196. package/server/src/Models/Order.php +29 -4
  197. package/server/src/Models/OrderConfig.php +14 -11
  198. package/server/src/Models/Payload.php +17 -1
  199. package/server/src/Models/ServiceRate.php +273 -1
  200. package/server/src/Models/ServiceRateFee.php +39 -9
  201. package/server/src/Models/TrackingNumber.php +8 -2
  202. package/server/src/Models/Vehicle.php +18 -2
  203. package/server/src/Models/Waypoint.php +2 -2
  204. package/server/src/Notifications/LateDeparture.php +48 -0
  205. package/server/src/Notifications/ProlongedStoppage.php +48 -0
  206. package/server/src/Notifications/RouteDeviation.php +48 -0
  207. package/server/src/Observers/ContactObserver.php +2 -0
  208. package/server/src/Observers/ServiceRateObserver.php +6 -6
  209. package/server/src/Orchestration/Engines/VroomOrchestrationEngine.php +29 -2
  210. package/server/src/Providers/FleetOpsServiceProvider.php +6 -0
  211. package/server/src/Providers/NotificationServiceProvider.php +3 -0
  212. package/server/src/Support/Analytics/AbstractAnalytics.php +77 -0
  213. package/server/src/Support/Analytics/FuelEfficiency.php +83 -0
  214. package/server/src/Support/Analytics/GeofenceViolations.php +63 -0
  215. package/server/src/Support/Analytics/IssuesInsights.php +67 -0
  216. package/server/src/Support/Analytics/LiveFleet.php +127 -0
  217. package/server/src/Support/Analytics/MaintenanceOverview.php +70 -0
  218. package/server/src/Support/Analytics/OnTimeDelivery.php +75 -0
  219. package/server/src/Support/Analytics/OperationsPulse.php +86 -0
  220. package/server/src/Support/Analytics/OrdersByStatus.php +64 -0
  221. package/server/src/Support/Analytics/RevenueTrend.php +78 -0
  222. package/server/src/Support/Analytics/TopDrivers.php +92 -0
  223. package/server/src/Support/CustomerAuth.php +98 -0
  224. package/server/src/Support/LabelPdf.php +52 -0
  225. package/server/src/Support/Metrics/AbstractMetric.php +170 -0
  226. package/server/src/Support/Metrics/ActiveLiveOrdersMetric.php +28 -0
  227. package/server/src/Support/Metrics/AvgOrderValueMetric.php +53 -0
  228. package/server/src/Support/Metrics/DriversOnlineMetric.php +30 -0
  229. package/server/src/Support/Metrics/EarningsMetric.php +30 -0
  230. package/server/src/Support/Metrics/FuelCostsMetric.php +30 -0
  231. package/server/src/Support/Metrics/MoneyMetric.php +30 -0
  232. package/server/src/Support/Metrics/OpenIssuesMetric.php +35 -0
  233. package/server/src/Support/Metrics/OrdersCanceledMetric.php +35 -0
  234. package/server/src/Support/Metrics/OrdersCompletedMetric.php +35 -0
  235. package/server/src/Support/Metrics/OrdersInProgressMetric.php +51 -0
  236. package/server/src/Support/Metrics/OrdersScheduledMetric.php +37 -0
  237. package/server/src/Support/Metrics/Registry.php +46 -0
  238. package/server/src/Support/Metrics/ResolvedIssuesMetric.php +35 -0
  239. package/server/src/Support/Metrics/TotalCustomersMetric.php +29 -0
  240. package/server/src/Support/Metrics/TotalDistanceTraveledMetric.php +35 -0
  241. package/server/src/Support/Metrics/TotalDriversMetric.php +28 -0
  242. package/server/src/Support/Metrics/TotalTimeTraveledMetric.php +35 -0
  243. package/server/src/Support/Metrics.php +46 -249
  244. package/server/src/Support/ResolvesOrderServiceStops.php +512 -0
  245. package/server/src/Support/Telematics/Providers/AfaqyProvider.php +290 -0
  246. package/server/src/Support/Telematics/Providers/SafeeProvider.php +333 -0
  247. package/server/src/Tracking/TrackingContextBuilder.php +41 -38
  248. package/server/src/Tracking/TrackingIntelligenceService.php +109 -9
  249. package/server/src/Tracking/TrackingStop.php +13 -11
  250. package/server/src/routes.php +50 -0
  251. package/server/tests/AnalyticsRoutesTest.php +41 -0
  252. package/server/tests/CapacityAllocationEngineTest.php +10 -10
  253. package/server/tests/CustomerContactIdentitySafetyTest.php +54 -0
  254. package/server/tests/CustomerEndpointTest.php +199 -0
  255. package/server/tests/DriverVehicleStatusDefaultTest.php +39 -0
  256. package/server/tests/LabelPdfRenderingTest.php +23 -0
  257. package/server/tests/LiveControllerViewportTest.php +60 -0
  258. package/server/tests/MetricsRegistryTest.php +84 -0
  259. package/server/tests/OrchestrationPayloadBuilderTest.php +2 -2
  260. package/server/tests/OrderConfigEndpointTest.php +86 -0
  261. package/server/tests/OrderFileAttachmentTest.php +20 -0
  262. package/server/tests/ServiceAreaZoneApiShapeTest.php +34 -0
  263. package/server/tests/VehicleDriverAssignmentTest.php +18 -0
  264. package/server/tests/VroomOrchestrationEngineTest.php +91 -0
  265. package/translations/en-us.yaml +38 -0
  266. 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');
@@ -112,13 +127,19 @@ class OrderController extends FleetOpsController
112
127
  $input['integrated_vendor_order'] = $integratedVendorOrder;
113
128
  }
114
129
 
115
- // Normalize order config + type onto the company transport
116
- // config when an explicit config was not provided.
117
- $resolvedOrderConfig = null;
118
- if (!empty($input['order_config_uuid']) || !empty($input['type'])) {
119
- $resolvedOrderConfig = OrderConfig::resolveFromIdentifier([$input['order_config_uuid'] ?? null, $input['type'] ?? null]);
130
+ // Normalize order config + type. Invalid explicit configs must
131
+ // not silently fall back to the default transport config.
132
+ $hasExplicitOrderConfig = !empty($input['order_config_uuid']);
133
+ $hasExplicitOrderType = !empty($input['type']);
134
+ $resolvedOrderConfig = OrderConfig::resolveFromIdentifier([$input['order_config_uuid'] ?? null, $input['type'] ?? null]);
135
+
136
+ if (!$resolvedOrderConfig && $hasExplicitOrderConfig) {
137
+ throw new FleetbaseRequestValidationException(['order_config_uuid' => 'The selected order config is invalid.']);
138
+ }
139
+
140
+ if (!$resolvedOrderConfig && !$hasExplicitOrderType) {
141
+ $resolvedOrderConfig = OrderConfig::defaultOrCreate();
120
142
  }
121
- $resolvedOrderConfig ??= OrderConfig::defaultOrCreate();
122
143
 
123
144
  if ($resolvedOrderConfig) {
124
145
  $input['order_config_uuid'] = $resolvedOrderConfig->uuid;
@@ -159,14 +180,7 @@ class OrderController extends FleetOpsController
159
180
  ->insertEntities($entities);
160
181
 
161
182
  // If order creation includes files assosciate each to this order
162
- if ($uploads) {
163
- $ids = collect($uploads)->pluck('uuid');
164
- $files = File::whereIn('uuid', $ids)->get();
165
-
166
- foreach ($files as $file) {
167
- $file->setKey($order);
168
- }
169
- }
183
+ $order->attachFiles($uploads);
170
184
 
171
185
  // save custom field values
172
186
  if (is_array($customFieldValues)) {
@@ -700,11 +714,22 @@ class OrderController extends FleetOpsController
700
714
  */
701
715
  public function updateActivity(string $id, Request $request)
702
716
  {
703
- $order = Order::findById($id, ['driverAssigned', 'payload.entities']);
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
+ ]);
704
728
  if (!$order) {
705
729
  return response()->error('No order found.');
706
730
  }
707
731
 
732
+ $proof = $request->input('proof');
708
733
  $activity = $request->array('activity');
709
734
  $activity = new Activity($activity, $order->getConfigFlow());
710
735
 
@@ -725,29 +750,77 @@ class OrderController extends FleetOpsController
725
750
  /**
726
751
  * @var \Fleetbase\LaravelMysqlSpatial\Types\Point
727
752
  */
728
- $location = $order->getLastLocation();
729
- $order->setStatus($activity->code);
730
- $order->insertActivity($activity, $location);
753
+ $location = $order->getLastLocation();
754
+ $isMultipleWaypointOrder = $this->payloadHasWaypoints($order->payload);
755
+
756
+ if (!$isMultipleWaypointOrder) {
757
+ $order->updateActivity($activity, $proof);
731
758
 
732
- // also update for each order entities if not multiple drop order
733
- // all entities will share the same activity status as is one drop order
734
- 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
735
761
  foreach ($order->payload->entities as $entity) {
736
- $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));
737
770
  }
771
+
772
+ return new OrderResource($order->refresh());
738
773
  }
739
774
 
740
- // Handle order completed
741
- if (Utils::isActivity($activity) && $activity->completesOrder() && $order->driverAssigned) {
742
- // unset from driver current job
743
- $order->driverAssigned->unassignCurrentOrder();
744
- $order->complete();
775
+ if ($activity->is('dispatched')) {
776
+ $order->updateActivity($activity, $proof);
777
+
778
+ return new OrderResource($order->refresh());
745
779
  }
746
780
 
747
- // Fire activity events
748
- $activity->fireEvents($order);
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);
794
+ }
749
795
 
750
- return response()->json(['status' => 'ok']);
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());
809
+ }
810
+
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
+ }
822
+
823
+ return new OrderResource($order->refresh());
751
824
  }
752
825
 
753
826
  /**
@@ -763,12 +836,24 @@ class OrderController extends FleetOpsController
763
836
  return response()->error('No order found.');
764
837
  }
765
838
 
766
- $waypoint = $request->filled('waypoint') ? Waypoint::findByPlace($request->input('waypoint'), $order) : null;
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
+ }
767
852
  $orderConfig = $order->ensureOrderConfig();
768
853
  if (!$orderConfig) {
769
854
  return response()->error('No order config found for order.');
770
855
  }
771
- $activities = $orderConfig->nextActivity($waypoint);
856
+ $activities = $stop ? $this->nextActivitiesForServiceStop($order, $order->payload, $stop) : $orderConfig->nextActivity();
772
857
 
773
858
  // If activity is to complete order add proof of delivery properties if required
774
859
  // This is a temporary fix until activity is updated to handle POD on it's own
@@ -788,6 +873,330 @@ class OrderController extends FleetOpsController
788
873
  return response()->json($activities);
789
874
  }
790
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
+
791
1200
  /**
792
1201
  * Finds and responds with the orders next activity update based on the orders configuration.
793
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->has('currency');
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']);
@@ -251,6 +251,7 @@ class SettingController extends Controller
251
251
  'route_cache_ttl_seconds' => (int) $request->input('route_cache_ttl_seconds', data_get($config, 'route_cache_ttl_seconds', 600)),
252
252
  'stale_location_threshold_seconds' => (int) $request->input('stale_location_threshold_seconds', data_get($config, 'stale_location_threshold_seconds', 300)),
253
253
  'default_vehicle_speed_kph' => (float) $request->input('default_vehicle_speed_kph', data_get($config, 'default_vehicle_speed_kph', 35)),
254
+ 'alerts' => $this->normalizeTrackingAlertSettings((array) $request->input('alerts', data_get($config, 'alerts', []))),
254
255
  ]);
255
256
 
256
257
  return response()->json([
@@ -275,7 +276,9 @@ class SettingController extends Controller
275
276
  'route_cache_ttl_seconds' => data_get($config, 'route_cache_ttl_seconds', 600),
276
277
  'stale_location_threshold_seconds' => data_get($config, 'stale_location_threshold_seconds', 300),
277
278
  'default_vehicle_speed_kph' => data_get($config, 'default_vehicle_speed_kph', 35),
279
+ 'alerts' => data_get($config, 'alerts', $this->trackingAlertDefaults()),
278
280
  ]);
281
+ $trackingSettings['alerts'] = $this->normalizeTrackingAlertSettings(data_get($trackingSettings, 'alerts', []));
279
282
  $trackingSettings['providers'] = $this->trackingProviderOptions();
280
283
 
281
284
  return response()->json($trackingSettings);
@@ -304,6 +307,7 @@ class SettingController extends Controller
304
307
  'route_cache_ttl_seconds' => (int) $request->input('route_cache_ttl_seconds', data_get($config, 'route_cache_ttl_seconds', 600)),
305
308
  'stale_location_threshold_seconds' => (int) $request->input('stale_location_threshold_seconds', data_get($config, 'stale_location_threshold_seconds', 300)),
306
309
  'default_vehicle_speed_kph' => (float) $request->input('default_vehicle_speed_kph', data_get($config, 'default_vehicle_speed_kph', 35)),
310
+ 'alerts' => $this->normalizeTrackingAlertSettings((array) $request->input('alerts', data_get($config, 'alerts', []))),
307
311
  ]);
308
312
 
309
313
  return response()->json($this->getAdminTrackingSettings()->getData(true));
@@ -519,7 +523,48 @@ class SettingController extends Controller
519
523
  $config = config('fleetops.tracking', []);
520
524
  $systemSettings = Setting::lookup('fleet-ops.tracking-settings', []);
521
525
 
522
- return array_merge($config, is_array($systemSettings) ? $systemSettings : []);
526
+ $defaults = array_merge($config, is_array($systemSettings) ? $systemSettings : []);
527
+ $defaults['alerts'] = $this->normalizeTrackingAlertSettings(data_get($defaults, 'alerts', []));
528
+
529
+ return $defaults;
530
+ }
531
+
532
+ protected function trackingAlertDefaults(): array
533
+ {
534
+ return [
535
+ 'late_departures' => [
536
+ 'enabled' => true,
537
+ 'grace_period_minutes' => 15,
538
+ ],
539
+ 'route_deviations' => [
540
+ 'enabled' => true,
541
+ 'distance_threshold_meters' => 500,
542
+ ],
543
+ 'prolonged_stoppages' => [
544
+ 'enabled' => true,
545
+ 'duration_threshold_minutes' => 30,
546
+ ],
547
+ ];
548
+ }
549
+
550
+ protected function normalizeTrackingAlertSettings(array $alerts): array
551
+ {
552
+ $defaults = $this->trackingAlertDefaults();
553
+
554
+ return [
555
+ 'late_departures' => [
556
+ 'enabled' => (bool) data_get($alerts, 'late_departures.enabled', data_get($defaults, 'late_departures.enabled')),
557
+ 'grace_period_minutes' => max(0, (int) data_get($alerts, 'late_departures.grace_period_minutes', data_get($defaults, 'late_departures.grace_period_minutes'))),
558
+ ],
559
+ 'route_deviations' => [
560
+ 'enabled' => (bool) data_get($alerts, 'route_deviations.enabled', data_get($defaults, 'route_deviations.enabled')),
561
+ 'distance_threshold_meters' => max(0, (int) data_get($alerts, 'route_deviations.distance_threshold_meters', data_get($defaults, 'route_deviations.distance_threshold_meters'))),
562
+ ],
563
+ 'prolonged_stoppages' => [
564
+ 'enabled' => (bool) data_get($alerts, 'prolonged_stoppages.enabled', data_get($defaults, 'prolonged_stoppages.enabled')),
565
+ 'duration_threshold_minutes' => max(0, (int) data_get($alerts, 'prolonged_stoppages.duration_threshold_minutes', data_get($defaults, 'prolonged_stoppages.duration_threshold_minutes'))),
566
+ ],
567
+ ];
523
568
  }
524
569
 
525
570
  protected function trackingProviderOptions(): array