@fleetbase/fleetops-engine 0.6.47 → 0.6.49

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 (223) hide show
  1. package/addon/components/admin/routing-settings.hbs +71 -0
  2. package/addon/components/admin/routing-settings.js +124 -0
  3. package/addon/components/customer/orders.js +1 -1
  4. package/addon/components/display-place.hbs +1 -1
  5. package/addon/components/driver/details.hbs +5 -1
  6. package/addon/components/driver/form.hbs +11 -1
  7. package/addon/components/fleet-listing-panel.hbs +2 -2
  8. package/addon/components/fleet-listing-panel.js +10 -0
  9. package/addon/components/map/google-live-map.js +12 -73
  10. package/addon/components/map/leaflet-live-map.hbs +149 -33
  11. package/addon/components/map/order-list-overlay/order.hbs +18 -3
  12. package/addon/components/map/toolbar.hbs +2 -2
  13. package/addon/components/map/toolbar.js +10 -2
  14. package/addon/components/orchestrator/phase-builder.hbs +49 -5
  15. package/addon/components/orchestrator/phase-builder.js +34 -1
  16. package/addon/components/orchestrator/resource-panel.hbs +19 -1
  17. package/addon/components/orchestrator/resource-panel.js +4 -0
  18. package/addon/components/orchestrator-workbench.js +4 -1
  19. package/addon/components/order/details/purchase-rate.hbs +19 -22
  20. package/addon/components/order/details/route.hbs +26 -1
  21. package/addon/components/order/details/route.js +50 -0
  22. package/addon/components/order/details/tracking.hbs +171 -27
  23. package/addon/components/order/details/tracking.js +457 -0
  24. package/addon/components/order/form.hbs +2 -2
  25. package/addon/components/order/kanban-card.hbs +18 -3
  26. package/addon/components/order-list-overlay/order.hbs +18 -3
  27. package/addon/components/order-progress-card.hbs +21 -6
  28. package/addon/components/order-tracking-lookup.hbs +7 -7
  29. package/addon/components/order-tracking-lookup.js +1 -1
  30. package/addon/components/route-list.hbs +78 -8
  31. package/addon/components/route-list.js +48 -6
  32. package/addon/components/tracking-stop-progress.hbs +45 -0
  33. package/addon/components/tracking-stop-progress.js +79 -0
  34. package/addon/components/widget/fleet-ops-key-metrics.hbs +19 -17
  35. package/addon/components/widget/fleet-ops-key-metrics.js +26 -0
  36. package/addon/components/widget/fuel-efficiency.hbs +26 -0
  37. package/addon/components/widget/fuel-efficiency.js +43 -0
  38. package/addon/components/widget/geofence-violations.hbs +44 -0
  39. package/addon/components/widget/geofence-violations.js +55 -0
  40. package/addon/components/widget/issues-insights.hbs +43 -0
  41. package/addon/components/widget/issues-insights.js +62 -0
  42. package/addon/components/widget/kpi-active-orders.hbs +10 -0
  43. package/addon/components/widget/kpi-active-orders.js +5 -0
  44. package/addon/components/widget/kpi-aov.hbs +10 -0
  45. package/addon/components/widget/kpi-aov.js +5 -0
  46. package/addon/components/widget/kpi-distance.hbs +10 -0
  47. package/addon/components/widget/kpi-distance.js +5 -0
  48. package/addon/components/widget/kpi-drivers-online.hbs +10 -0
  49. package/addon/components/widget/kpi-drivers-online.js +5 -0
  50. package/addon/components/widget/kpi-earnings.hbs +10 -0
  51. package/addon/components/widget/kpi-earnings.js +5 -0
  52. package/addon/components/widget/kpi-open-issues.hbs +11 -0
  53. package/addon/components/widget/kpi-open-issues.js +5 -0
  54. package/addon/components/widget/kpi-tile.hbs +59 -0
  55. package/addon/components/widget/kpi-tile.js +168 -0
  56. package/addon/components/widget/live-fleet.hbs +122 -0
  57. package/addon/components/widget/live-fleet.js +365 -0
  58. package/addon/components/widget/maintenance-overview.hbs +45 -0
  59. package/addon/components/widget/maintenance-overview.js +40 -0
  60. package/addon/components/widget/on-time-delivery.hbs +27 -0
  61. package/addon/components/widget/on-time-delivery.js +73 -0
  62. package/addon/components/widget/operations-pulse.hbs +51 -0
  63. package/addon/components/widget/operations-pulse.js +173 -0
  64. package/addon/components/widget/orders-by-status.hbs +26 -0
  65. package/addon/components/widget/orders-by-status.js +54 -0
  66. package/addon/components/widget/revenue-trend.hbs +34 -0
  67. package/addon/components/widget/revenue-trend.js +67 -0
  68. package/addon/components/widget/top-drivers.hbs +44 -0
  69. package/addon/components/widget/top-drivers.js +54 -0
  70. package/addon/controllers/settings/routing.js +124 -1
  71. package/addon/extension.js +172 -3
  72. package/addon/helpers/format-duration.js +38 -0
  73. package/addon/routes/application.js +6 -0
  74. package/addon/services/map-adapter/google.js +13 -2
  75. package/addon/services/order-list-overlay.js +2 -2
  76. package/addon/styles/fleetops-engine.css +806 -46
  77. package/addon/templates/maintenance/equipment/index.hbs +1 -1
  78. package/addon/templates/maintenance/parts/index.hbs +1 -1
  79. package/addon/templates/maintenance/schedules/index.hbs +1 -1
  80. package/addon/templates/management/vehicles/index.hbs +1 -1
  81. package/addon/templates/settings/avatars.hbs +1 -1
  82. package/addon/templates/settings/custom-fields.hbs +1 -1
  83. package/addon/templates/settings/map.hbs +1 -1
  84. package/addon/templates/settings/navigator-app.hbs +1 -1
  85. package/addon/templates/settings/notifications.hbs +1 -1
  86. package/addon/templates/settings/orchestrator.hbs +1 -1
  87. package/addon/templates/settings/payments/index.hbs +1 -1
  88. package/addon/templates/settings/payments/onboard.hbs +1 -1
  89. package/addon/templates/settings/routing.hbs +82 -4
  90. package/addon/templates/settings/scheduling.hbs +1 -1
  91. package/addon/utils/live-map-card-content.js +171 -0
  92. package/app/components/tracking-stop-progress.js +1 -0
  93. package/app/components/widget/fuel-efficiency.js +1 -0
  94. package/app/components/widget/geofence-violations.js +1 -0
  95. package/app/components/widget/issues-insights.js +1 -0
  96. package/app/components/widget/kpi-active-orders.js +1 -0
  97. package/app/components/widget/kpi-aov.js +1 -0
  98. package/app/components/widget/kpi-distance.js +1 -0
  99. package/app/components/widget/kpi-drivers-online.js +1 -0
  100. package/app/components/widget/kpi-earnings.js +1 -0
  101. package/app/components/widget/kpi-open-issues.js +1 -0
  102. package/app/components/widget/kpi-tile.js +1 -0
  103. package/app/components/widget/live-fleet.js +1 -0
  104. package/app/components/widget/maintenance-overview.js +1 -0
  105. package/app/components/widget/on-time-delivery.js +1 -0
  106. package/app/components/widget/operations-pulse.js +1 -0
  107. package/app/components/widget/orders-by-status.js +1 -0
  108. package/app/components/widget/revenue-trend.js +1 -0
  109. package/app/components/widget/top-drivers.js +1 -0
  110. package/app/helpers/format-duration.js +1 -0
  111. package/app/utils/live-map-card-content.js +1 -0
  112. package/composer.json +1 -1
  113. package/extension.json +1 -1
  114. package/package.json +3 -3
  115. package/pnpm-workspace.yaml +1 -0
  116. package/server/config/fleetops.php +32 -11
  117. package/server/config/telematics.php +111 -1
  118. package/server/migrations/2026_05_13_000001_repair_customer_contact_user_types.php +86 -0
  119. package/server/migrations/2026_05_18_000001_add_license_expiry_to_drivers_table.php +25 -0
  120. package/server/seeders/Testing/Concerns/SeedsTestingData.php +183 -0
  121. package/server/seeders/Testing/ConnectivitySeeder.php +212 -0
  122. package/server/seeders/Testing/FleetSeeder.php +341 -0
  123. package/server/seeders/Testing/MaintenanceSeeder.php +238 -0
  124. package/server/seeders/Testing/NetworkSeeder.php +250 -0
  125. package/server/seeders/Testing/OrdersSeeder.php +214 -0
  126. package/server/seeders/Testing/TestingSeeder.php +40 -0
  127. package/server/seeders/Testing/VroomOrchestrationSeeder.php +409 -0
  128. package/server/src/Console/Commands/ProcessOperationalAlerts.php +279 -0
  129. package/server/src/Http/Controllers/Api/v1/OrchestrationController.php +64 -0
  130. package/server/src/Http/Controllers/Api/v1/OrderController.php +26 -26
  131. package/server/src/Http/Controllers/Api/v1/PayloadController.php +10 -10
  132. package/server/src/Http/Controllers/Api/v1/ServiceQuoteController.php +1 -1
  133. package/server/src/Http/Controllers/Internal/v1/AnalyticsController.php +119 -0
  134. package/server/src/Http/Controllers/Internal/v1/ContactController.php +41 -0
  135. package/server/src/Http/Controllers/Internal/v1/LiveController.php +6 -42
  136. package/server/src/Http/Controllers/Internal/v1/MetricsController.php +82 -3
  137. package/server/src/Http/Controllers/Internal/v1/OrchestrationController.php +1 -1
  138. package/server/src/Http/Controllers/Internal/v1/OrderController.php +52 -21
  139. package/server/src/Http/Controllers/Internal/v1/ServiceQuoteController.php +1 -1
  140. package/server/src/Http/Controllers/Internal/v1/SettingController.php +164 -12
  141. package/server/src/Http/Resources/v1/Driver.php +2 -0
  142. package/server/src/Http/Resources/v1/Fleet.php +4 -0
  143. package/server/src/Http/Resources/v1/Index/Driver.php +48 -0
  144. package/server/src/Http/Resources/v1/Index/Payload.php +2 -2
  145. package/server/src/Http/Resources/v1/Index/Vehicle.php +74 -1
  146. package/server/src/Models/Contact.php +122 -13
  147. package/server/src/Models/Driver.php +2 -0
  148. package/server/src/Models/Order.php +1 -1
  149. package/server/src/Models/OrderConfig.php +14 -11
  150. package/server/src/Models/Payload.php +8 -8
  151. package/server/src/Models/Place.php +0 -3
  152. package/server/src/Models/ServiceRate.php +1 -1
  153. package/server/src/Notifications/LateDeparture.php +48 -0
  154. package/server/src/Notifications/ProlongedStoppage.php +48 -0
  155. package/server/src/Notifications/RouteDeviation.php +48 -0
  156. package/server/src/Observers/ContactObserver.php +8 -0
  157. package/server/src/Orchestration/Engines/CapacityAllocationEngine.php +220 -0
  158. package/server/src/Orchestration/Engines/VroomOrchestrationEngine.php +357 -38
  159. package/server/src/Orchestration/Support/OrchestrationPayloadBuilder.php +259 -51
  160. package/server/src/Providers/FleetOpsServiceProvider.php +32 -0
  161. package/server/src/Providers/NotificationServiceProvider.php +3 -0
  162. package/server/src/Support/Analytics/AbstractAnalytics.php +77 -0
  163. package/server/src/Support/Analytics/FuelEfficiency.php +83 -0
  164. package/server/src/Support/Analytics/GeofenceViolations.php +63 -0
  165. package/server/src/Support/Analytics/IssuesInsights.php +67 -0
  166. package/server/src/Support/Analytics/LiveFleet.php +127 -0
  167. package/server/src/Support/Analytics/MaintenanceOverview.php +70 -0
  168. package/server/src/Support/Analytics/OnTimeDelivery.php +75 -0
  169. package/server/src/Support/Analytics/OperationsPulse.php +86 -0
  170. package/server/src/Support/Analytics/OrdersByStatus.php +64 -0
  171. package/server/src/Support/Analytics/RevenueTrend.php +79 -0
  172. package/server/src/Support/Analytics/TopDrivers.php +92 -0
  173. package/server/src/Support/LiveOrderQuery.php +75 -0
  174. package/server/src/Support/Metrics/AbstractMetric.php +170 -0
  175. package/server/src/Support/Metrics/ActiveLiveOrdersMetric.php +28 -0
  176. package/server/src/Support/Metrics/AvgOrderValueMetric.php +53 -0
  177. package/server/src/Support/Metrics/DriversOnlineMetric.php +30 -0
  178. package/server/src/Support/Metrics/EarningsMetric.php +30 -0
  179. package/server/src/Support/Metrics/FuelCostsMetric.php +30 -0
  180. package/server/src/Support/Metrics/MoneyMetric.php +30 -0
  181. package/server/src/Support/Metrics/OpenIssuesMetric.php +35 -0
  182. package/server/src/Support/Metrics/OrdersCanceledMetric.php +35 -0
  183. package/server/src/Support/Metrics/OrdersCompletedMetric.php +35 -0
  184. package/server/src/Support/Metrics/OrdersInProgressMetric.php +51 -0
  185. package/server/src/Support/Metrics/OrdersScheduledMetric.php +37 -0
  186. package/server/src/Support/Metrics/Registry.php +46 -0
  187. package/server/src/Support/Metrics/ResolvedIssuesMetric.php +35 -0
  188. package/server/src/Support/Metrics/TotalCustomersMetric.php +29 -0
  189. package/server/src/Support/Metrics/TotalDistanceTraveledMetric.php +35 -0
  190. package/server/src/Support/Metrics/TotalDriversMetric.php +28 -0
  191. package/server/src/Support/Metrics/TotalTimeTraveledMetric.php +35 -0
  192. package/server/src/Support/Metrics.php +46 -234
  193. package/server/src/Support/OSRM.php +11 -6
  194. package/server/src/Support/OrderTracker.php +7 -582
  195. package/server/src/Support/Telematics/Providers/AfaqyProvider.php +290 -0
  196. package/server/src/Support/Telematics/Providers/SafeeProvider.php +333 -0
  197. package/server/src/Support/Utils.php +1 -1
  198. package/server/src/Tracking/Contracts/TrackingProviderInterface.php +19 -0
  199. package/server/src/Tracking/Providers/CalculatedTrackingProvider.php +65 -0
  200. package/server/src/Tracking/Providers/GoogleRoutesTrackingProvider.php +118 -0
  201. package/server/src/Tracking/Providers/OsrmTrackingProvider.php +71 -0
  202. package/server/src/Tracking/README.md +25 -0
  203. package/server/src/Tracking/Support/FakeTrackingProvider.php +42 -0
  204. package/server/src/Tracking/TrackingContext.php +63 -0
  205. package/server/src/Tracking/TrackingContextBuilder.php +204 -0
  206. package/server/src/Tracking/TrackingIntelligenceService.php +180 -0
  207. package/server/src/Tracking/TrackingOptions.php +49 -0
  208. package/server/src/Tracking/TrackingProviderCapabilities.php +30 -0
  209. package/server/src/Tracking/TrackingProviderManager.php +89 -0
  210. package/server/src/Tracking/TrackingProviderRegistry.php +35 -0
  211. package/server/src/Tracking/TrackingProviderResult.php +20 -0
  212. package/server/src/Tracking/TrackingStop.php +59 -0
  213. package/server/src/routes.php +27 -0
  214. package/server/tests/AnalyticsRoutesTest.php +41 -0
  215. package/server/tests/CapacityAllocationEngineTest.php +108 -0
  216. package/server/tests/LiveOrderQueryTest.php +34 -0
  217. package/server/tests/MetricsRegistryTest.php +88 -0
  218. package/server/tests/OrchestrationPayloadBuilderTest.php +132 -0
  219. package/server/tests/OrchestratorConsumableApiTest.php +74 -0
  220. package/server/tests/PingDriverEndpointTest.php +19 -0
  221. package/server/tests/TrackingIntelligenceTest.php +176 -0
  222. package/server/tests/VroomOrchestrationEngineTest.php +250 -0
  223. package/translations/en-us.yaml +41 -0
@@ -26,16 +26,17 @@ use Fleetbase\FleetOps\Models\Proof;
26
26
  use Fleetbase\FleetOps\Models\ServiceQuote;
27
27
  use Fleetbase\FleetOps\Models\TrackingStatus;
28
28
  use Fleetbase\FleetOps\Models\Waypoint;
29
+ use Fleetbase\FleetOps\Notifications\OrderPing;
29
30
  use Fleetbase\FleetOps\Support\Utils;
30
31
  use Fleetbase\Http\Requests\ExportRequest;
31
32
  use Fleetbase\Http\Requests\Internal\BulkActionRequest;
32
33
  use Fleetbase\Models\File;
33
34
  use Fleetbase\Models\Type;
35
+ use Fleetbase\Support\Auth;
34
36
  use Fleetbase\Support\TemplateString;
35
37
  use Illuminate\Database\Eloquent\ModelNotFoundException;
36
38
  use Illuminate\Database\QueryException;
37
39
  use Illuminate\Http\Request;
38
- use Illuminate\Support\Facades\Cache;
39
40
  use Illuminate\Support\Facades\DB;
40
41
  use Illuminate\Support\Facades\Validator;
41
42
  use Illuminate\Support\Str;
@@ -111,17 +112,23 @@ class OrderController extends FleetOpsController
111
112
  $input['integrated_vendor_order'] = $integratedVendorOrder;
112
113
  }
113
114
 
114
- // Normalize order config + type onto the company transport
115
- // config when an explicit config was not provided.
116
- $resolvedOrderConfig = null;
117
- if (!empty($input['order_config_uuid']) || !empty($input['type'])) {
118
- $resolvedOrderConfig = OrderConfig::resolveFromIdentifier([$input['order_config_uuid'] ?? null, $input['type'] ?? null]);
115
+ // Normalize order config + type. Invalid explicit configs must
116
+ // not silently fall back to the default transport config.
117
+ $hasExplicitOrderConfig = !empty($input['order_config_uuid']);
118
+ $hasExplicitOrderType = !empty($input['type']);
119
+ $resolvedOrderConfig = OrderConfig::resolveFromIdentifier([$input['order_config_uuid'] ?? null, $input['type'] ?? null]);
120
+
121
+ if (!$resolvedOrderConfig && $hasExplicitOrderConfig) {
122
+ throw new FleetbaseRequestValidationException(['order_config_uuid' => 'The selected order config is invalid.']);
123
+ }
124
+
125
+ if (!$resolvedOrderConfig && !$hasExplicitOrderType) {
126
+ $resolvedOrderConfig = OrderConfig::defaultOrCreate();
119
127
  }
120
- $resolvedOrderConfig ??= OrderConfig::defaultOrCreate();
121
128
 
122
129
  if ($resolvedOrderConfig) {
123
130
  $input['order_config_uuid'] = $resolvedOrderConfig->uuid;
124
- $input['type'] = $resolvedOrderConfig->key;
131
+ $input['type'] = $resolvedOrderConfig->key;
125
132
  } elseif (!isset($input['type'])) {
126
133
  $input['type'] = 'transport';
127
134
  }
@@ -762,7 +769,7 @@ class OrderController extends FleetOpsController
762
769
  return response()->error('No order found.');
763
770
  }
764
771
 
765
- $waypoint = $request->filled('waypoint') ? Waypoint::findByPlace($request->input('waypoint'), $order) : null;
772
+ $waypoint = $request->filled('waypoint') ? Waypoint::findByPlace($request->input('waypoint'), $order) : null;
766
773
  $orderConfig = $order->ensureOrderConfig();
767
774
  if (!$orderConfig) {
768
775
  return response()->error('No order config found for order.');
@@ -792,33 +799,57 @@ class OrderController extends FleetOpsController
792
799
  *
793
800
  * @return \Illuminate\Http\Response
794
801
  */
795
- public function trackerInfo(string $id)
802
+ public function trackerInfo(Request $request, string $id)
796
803
  {
797
804
  $order = Order::findById($id);
798
805
  if (!$order) {
799
806
  return response()->error('No order found.');
800
807
  }
801
808
 
802
- // Cache tracker data for 30 seconds with order-specific key
803
- $cacheKey = "order:{$order->uuid}:tracker";
804
- $trackerInfo = Cache::remember($cacheKey, 30, function () use ($order) {
805
- return $order->tracker()->toArray();
806
- });
807
-
808
- return response()->json($trackerInfo);
809
+ return response()->json($order->tracker()->toArray($request->only(['provider', 'fallbacks', 'traffic_enabled'])));
809
810
  }
810
811
 
811
- public function waypointEtas(string $id)
812
+ public function waypointEtas(Request $request, string $id)
812
813
  {
813
814
  $order = Order::findById($id);
814
815
  if (!$order) {
815
816
  return response()->error('No order found.');
816
817
  }
817
818
 
818
- // Get order tracker
819
- $eta = $order->tracker()->eta();
819
+ return response()->json($order->tracker()->eta($request->only(['provider', 'fallbacks', 'traffic_enabled'])));
820
+ }
821
+
822
+ /**
823
+ * Ping the assigned driver to refresh/order attention in the driver app.
824
+ *
825
+ * @return \Illuminate\Http\JsonResponse
826
+ */
827
+ public function pingDriver(string $id)
828
+ {
829
+ if (!Auth::can('fleet-ops update order')) {
830
+ return response()->error('Unauthorized.', 403);
831
+ }
820
832
 
821
- return response()->json($eta);
833
+ try {
834
+ $order = Order::findByIdOrFail($id, ['driverAssigned']);
835
+ } catch (ModelNotFoundException $e) {
836
+ return response()->error('Order resource not found.', 404);
837
+ }
838
+
839
+ if (!$order->driverAssigned) {
840
+ return response()->error('Order does not have an assigned driver.', 422);
841
+ }
842
+
843
+ try {
844
+ $order->driverAssigned->notify(new OrderPing($order));
845
+
846
+ return response()->json([
847
+ 'status' => 'ok',
848
+ 'message' => 'Driver app ping sent.',
849
+ ]);
850
+ } catch (\Throwable $e) {
851
+ return response()->error('Unable to ping driver app.', 500);
852
+ }
822
853
  }
823
854
 
824
855
  /**
@@ -226,7 +226,7 @@ class ServiceQuoteController extends FleetOpsController
226
226
  $entities = collect($entities)->mapInto(Entity::class);
227
227
 
228
228
  // should all be Place like
229
- $waypoints = collect([$pickup, ...$waypoints, $dropoff])->filter();
229
+ $waypoints = collect([$pickup, ...$waypoints, $dropoff])->filter();
230
230
  $endpointCount = (int) ($pickup instanceof Place) + (int) ($dropoff instanceof Place);
231
231
 
232
232
  // if facilitator is an integrated partner resolve service quotes from bridge
@@ -2,6 +2,7 @@
2
2
 
3
3
  namespace Fleetbase\FleetOps\Http\Controllers\Internal\v1;
4
4
 
5
+ use Fleetbase\FleetOps\Tracking\TrackingProviderRegistry;
5
6
  use Fleetbase\Http\Controllers\Controller;
6
7
  use Fleetbase\Models\Setting;
7
8
  use Fleetbase\Support\Auth;
@@ -211,12 +212,12 @@ class SettingController extends Controller
211
212
  {
212
213
  $routingSettings = Setting::lookupCompany('routing', ['router' => 'osrm', 'unit' => 'km']);
213
214
 
214
- $displayEngine = data_get($routingSettings, 'display_engine', data_get($routingSettings, 'routing_display_engine', data_get($routingSettings, 'router', 'osrm')));
215
- $optimizationEngine = data_get($routingSettings, 'optimization_engine', data_get($routingSettings, 'routing_optimization_engine', $displayEngine));
216
- $routingSettings['router'] = $displayEngine;
217
- $routingSettings['display_engine'] = $displayEngine;
218
- $routingSettings['optimization_engine'] = $optimizationEngine;
219
- $routingSettings['routing_display_engine'] = $displayEngine;
215
+ $displayEngine = data_get($routingSettings, 'display_engine', data_get($routingSettings, 'routing_display_engine', data_get($routingSettings, 'router', 'osrm')));
216
+ $optimizationEngine = data_get($routingSettings, 'optimization_engine', data_get($routingSettings, 'routing_optimization_engine', $displayEngine));
217
+ $routingSettings['router'] = $displayEngine;
218
+ $routingSettings['display_engine'] = $displayEngine;
219
+ $routingSettings['optimization_engine'] = $optimizationEngine;
220
+ $routingSettings['routing_display_engine'] = $displayEngine;
220
221
  $routingSettings['routing_optimization_engine'] = $optimizationEngine;
221
222
 
222
223
  // always default to km if no unit is set
@@ -227,6 +228,91 @@ class SettingController extends Controller
227
228
  return response()->json($routingSettings);
228
229
  }
229
230
 
231
+ /**
232
+ * Save order tracking intelligence settings.
233
+ *
234
+ * @param Request $request the HTTP request object containing tracking settings
235
+ *
236
+ * @return \Illuminate\Http\JsonResponse
237
+ */
238
+ public function saveTrackingSettings(Request $request)
239
+ {
240
+ $config = $this->trackingDefaults();
241
+ $fallbacks = $request->input('fallbacks', data_get($config, 'fallbacks', ['osrm', 'calculated']));
242
+ if (is_string($fallbacks)) {
243
+ $fallbacks = array_values(array_filter(array_map('trim', explode(',', $fallbacks))));
244
+ }
245
+
246
+ Setting::configureCompany('tracking', [
247
+ 'provider' => $request->input('provider', data_get($config, 'provider', 'google_routes')),
248
+ 'fallbacks' => $fallbacks,
249
+ 'traffic_enabled' => $request->boolean('traffic_enabled', data_get($config, 'traffic_enabled', true)),
250
+ 'cache_ttl_seconds' => (int) $request->input('cache_ttl_seconds', data_get($config, 'cache_ttl_seconds', 60)),
251
+ 'route_cache_ttl_seconds' => (int) $request->input('route_cache_ttl_seconds', data_get($config, 'route_cache_ttl_seconds', 600)),
252
+ 'stale_location_threshold_seconds' => (int) $request->input('stale_location_threshold_seconds', data_get($config, 'stale_location_threshold_seconds', 300)),
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', []))),
255
+ ]);
256
+
257
+ return response()->json([
258
+ 'status' => 'ok',
259
+ 'message' => 'Tracking settings succesfully saved.',
260
+ ]);
261
+ }
262
+
263
+ /**
264
+ * Retrieve order tracking intelligence settings.
265
+ *
266
+ * @return \Illuminate\Http\JsonResponse
267
+ */
268
+ public function getTrackingSettings()
269
+ {
270
+ $config = $this->trackingDefaults();
271
+ $trackingSettings = Setting::lookupCompany('tracking', [
272
+ 'provider' => data_get($config, 'provider', 'google_routes'),
273
+ 'fallbacks' => data_get($config, 'fallbacks', ['osrm', 'calculated']),
274
+ 'traffic_enabled' => data_get($config, 'traffic_enabled', true),
275
+ 'cache_ttl_seconds' => data_get($config, 'cache_ttl_seconds', 60),
276
+ 'route_cache_ttl_seconds' => data_get($config, 'route_cache_ttl_seconds', 600),
277
+ 'stale_location_threshold_seconds' => data_get($config, 'stale_location_threshold_seconds', 300),
278
+ 'default_vehicle_speed_kph' => data_get($config, 'default_vehicle_speed_kph', 35),
279
+ 'alerts' => data_get($config, 'alerts', $this->trackingAlertDefaults()),
280
+ ]);
281
+ $trackingSettings['alerts'] = $this->normalizeTrackingAlertSettings(data_get($trackingSettings, 'alerts', []));
282
+ $trackingSettings['providers'] = $this->trackingProviderOptions();
283
+
284
+ return response()->json($trackingSettings);
285
+ }
286
+
287
+ public function getAdminTrackingSettings()
288
+ {
289
+ return response()->json(array_merge($this->trackingDefaults(), [
290
+ 'providers' => $this->trackingProviderOptions(),
291
+ ]));
292
+ }
293
+
294
+ public function saveAdminTrackingSettings(Request $request)
295
+ {
296
+ $config = config('fleetops.tracking', []);
297
+ $fallbacks = $request->input('fallbacks', data_get($config, 'fallbacks', ['osrm', 'calculated']));
298
+ if (is_string($fallbacks)) {
299
+ $fallbacks = array_values(array_filter(array_map('trim', explode(',', $fallbacks))));
300
+ }
301
+
302
+ Setting::configure('fleet-ops.tracking-settings', [
303
+ 'provider' => $request->input('provider', data_get($config, 'provider', 'google_routes')),
304
+ 'fallbacks' => $fallbacks,
305
+ 'traffic_enabled' => $request->boolean('traffic_enabled', data_get($config, 'traffic_enabled', true)),
306
+ 'cache_ttl_seconds' => (int) $request->input('cache_ttl_seconds', data_get($config, 'cache_ttl_seconds', 60)),
307
+ 'route_cache_ttl_seconds' => (int) $request->input('route_cache_ttl_seconds', data_get($config, 'route_cache_ttl_seconds', 600)),
308
+ 'stale_location_threshold_seconds' => (int) $request->input('stale_location_threshold_seconds', data_get($config, 'stale_location_threshold_seconds', 300)),
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', []))),
311
+ ]);
312
+
313
+ return response()->json($this->getAdminTrackingSettings()->getData(true));
314
+ }
315
+
230
316
  /**
231
317
  * Retrieve and return the map provider settings for the current company.
232
318
  *
@@ -244,15 +330,15 @@ class SettingController extends Controller
244
330
  'mapProvider' => 'leaflet',
245
331
  ];
246
332
 
247
- $systemMapSettings = Setting::lookup('fleet-ops.map-settings', []);
248
- $mapSettings = Setting::lookupFromCompany('fleet-ops.map-settings', $defaults);
333
+ $systemMapSettings = Setting::lookup('fleet-ops.map-settings', []);
334
+ $mapSettings = Setting::lookupFromCompany('fleet-ops.map-settings', $defaults);
249
335
  $mapSettings['mapProvider'] = data_get($mapSettings, 'mapProvider') ?: data_get($systemMapSettings, 'mapProvider', 'leaflet');
250
336
 
251
337
  // Source the Google Maps API key from the system-level services config
252
338
  // that is managed by the core-api admin settings panel. This ensures a
253
339
  // single source of truth and avoids duplicating key management.
254
340
  $mapSettings['googleMapsApiKey'] = config('services.google_maps.api_key', env('GOOGLE_MAPS_API_KEY', ''));
255
- $mapSettings['googleMapsMapId'] = data_get($systemMapSettings, 'googleMapsMapId', '');
341
+ $mapSettings['googleMapsMapId'] = data_get($systemMapSettings, 'googleMapsMapId', '');
256
342
 
257
343
  return response()->json($mapSettings);
258
344
  }
@@ -289,7 +375,7 @@ class SettingController extends Controller
289
375
  public function getAdminMapSettings()
290
376
  {
291
377
  $defaults = [
292
- 'mapProvider' => 'leaflet',
378
+ 'mapProvider' => 'leaflet',
293
379
  'googleMapsMapId' => '',
294
380
  ];
295
381
 
@@ -299,13 +385,13 @@ class SettingController extends Controller
299
385
  public function saveAdminMapSettings(Request $request)
300
386
  {
301
387
  $allowedProviders = ['leaflet', 'google'];
302
- $mapProvider = $request->input('mapProvider', 'leaflet');
388
+ $mapProvider = $request->input('mapProvider', 'leaflet');
303
389
  if (!in_array($mapProvider, $allowedProviders)) {
304
390
  $mapProvider = 'leaflet';
305
391
  }
306
392
 
307
393
  $settings = [
308
- 'mapProvider' => $mapProvider,
394
+ 'mapProvider' => $mapProvider,
309
395
  'googleMapsMapId' => (string) $request->input('googleMapsMapId', ''),
310
396
  ];
311
397
 
@@ -431,4 +517,70 @@ class SettingController extends Controller
431
517
  'settings' => $normalized,
432
518
  ]);
433
519
  }
520
+
521
+ protected function trackingDefaults(): array
522
+ {
523
+ $config = config('fleetops.tracking', []);
524
+ $systemSettings = Setting::lookup('fleet-ops.tracking-settings', []);
525
+
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
+ ];
568
+ }
569
+
570
+ protected function trackingProviderOptions(): array
571
+ {
572
+ $registry = app(TrackingProviderRegistry::class);
573
+
574
+ return collect($registry->all())->map(function ($provider, $key) {
575
+ $label = $key === 'osrm' ? 'OSRM' : str($key)->replace('_', ' ')->title()->toString();
576
+
577
+ return [
578
+ 'key' => $key,
579
+ 'name' => $label,
580
+ 'value' => $key,
581
+ 'label' => $label,
582
+ 'capabilities' => $provider->capabilities()->toArray(),
583
+ ];
584
+ })->values()->all();
585
+ }
434
586
  }
@@ -38,6 +38,7 @@ class Driver extends FleetbaseResource
38
38
  'email' => $this->email,
39
39
  'phone' => $this->phone,
40
40
  'drivers_license_number' => $this->drivers_license_number,
41
+ 'license_expiry' => $this->license_expiry,
41
42
  'photo_url' => $this->photo_url,
42
43
  'avatar_url' => $this->avatar_url,
43
44
  'avatar_value' => $this->when(Http::isInternalRequest(), $this->getOriginal('avatar_url')),
@@ -94,6 +95,7 @@ class Driver extends FleetbaseResource
94
95
  'email' => $this->email,
95
96
  'phone' => $this->phone,
96
97
  'photo_url' => $this->photo_url,
98
+ 'license_expiry' => $this->license_expiry,
97
99
  'vehicle' => data_get($this, 'vehicle.public_id'),
98
100
  'current_job' => data_get($this, 'currentJob.public_id'),
99
101
  'vendor' => data_get($this, 'vendor.public_id'),
@@ -23,6 +23,10 @@ class Fleet extends FleetbaseResource
23
23
  }, $request->array('with'));
24
24
 
25
25
  $this->load($with);
26
+
27
+ if (in_array('subfleets', $with, true) && in_array('vehicles', $with, true)) {
28
+ $this->loadMissing('subFleets.vehicles');
29
+ }
26
30
  }
27
31
 
28
32
  return $this->withCustomFields([
@@ -33,6 +33,7 @@ class Driver extends FleetbaseResource
33
33
  'current_job_uuid'=> $this->when($isInternal, $this->current_job_uuid),
34
34
  'name' => $this->name,
35
35
  'vehicle_name' => $this->when($isInternal, $this->vehicle_name),
36
+ 'email' => $this->email,
36
37
  'phone' => $this->phone,
37
38
  'photo_url' => $this->photo_url,
38
39
  'status' => $this->status,
@@ -41,6 +42,53 @@ class Driver extends FleetbaseResource
41
42
  'altitude' => (int) data_get($this, 'altitude', 0),
42
43
  'speed' => (int) data_get($this, 'speed', 0),
43
44
  'online' => data_get($this, 'online', false),
45
+ 'meta' => [
46
+ '_index_resource' => true,
47
+ 'location_coordinates' => $this->locationCoordinates(),
48
+ 'current_order_reference' => $this->currentOrderReference(),
49
+ 'speed_label' => $this->speedLabel(),
50
+ 'heading_label' => $this->headingLabel(),
51
+ 'status_label' => $this->statusLabel(),
52
+ ],
44
53
  ];
45
54
  }
55
+
56
+ protected function currentOrderReference(): ?string
57
+ {
58
+ $this->loadMissing('currentOrder');
59
+ $order = data_get($this, 'currentOrder');
60
+
61
+ return data_get($order, 'tracking') ?? data_get($order, 'public_id');
62
+ }
63
+
64
+ protected function locationCoordinates(): ?string
65
+ {
66
+ $location = Utils::castPoint($this->location);
67
+
68
+ return $location ? $this->formatCoordinate($location->getLat()) . ' ' . $this->formatCoordinate($location->getLng()) : null;
69
+ }
70
+
71
+ protected function speedLabel(): string
72
+ {
73
+ $speed = data_get($this, 'speed');
74
+
75
+ return is_numeric($speed) ? ((int) $speed) . ' km/h' : '-';
76
+ }
77
+
78
+ protected function headingLabel(): string
79
+ {
80
+ $heading = data_get($this, 'heading');
81
+
82
+ return is_numeric($heading) ? ((int) $heading) . ' deg' : '-';
83
+ }
84
+
85
+ protected function statusLabel(): ?string
86
+ {
87
+ return $this->status ? str($this->status)->replace(['_', '-'], ' ')->headline()->toString() : null;
88
+ }
89
+
90
+ protected function formatCoordinate(float $coordinate): string
91
+ {
92
+ return (string) round($coordinate, 4);
93
+ }
46
94
  }
@@ -19,8 +19,8 @@ class Payload extends FleetbaseResource
19
19
  public function toArray($request): array
20
20
  {
21
21
  $isInternal = Http::isInternalRequest();
22
- $pickup = $this->index_pickup_place;
23
- $dropoff = $this->index_dropoff_place;
22
+ $pickup = $this->index_pickup_place;
23
+ $dropoff = $this->index_dropoff_place;
24
24
 
25
25
  return [
26
26
  'id' => $this->when($isInternal, $this->id, $this->public_id),
@@ -2,6 +2,7 @@
2
2
 
3
3
  namespace Fleetbase\FleetOps\Http\Resources\v1\Index;
4
4
 
5
+ use Fleetbase\FleetOps\Models\Order;
5
6
  use Fleetbase\FleetOps\Support\Utils;
6
7
  use Fleetbase\Http\Resources\FleetbaseResource;
7
8
  use Fleetbase\Support\Http;
@@ -28,8 +29,12 @@ class Vehicle extends FleetbaseResource
28
29
  'company_uuid' => $this->when($isInternal, $this->company_uuid),
29
30
  'vendor_uuid' => $this->when($isInternal, $this->vendor_uuid),
30
31
  'photo_uuid' => $this->when($isInternal, $this->photo_uuid),
32
+ 'internal_id' => $this->internal_id,
31
33
  'display_name' => $this->display_name,
34
+ 'driver_name' => $this->driver_name,
32
35
  'plate_number' => $this->plate_number,
36
+ 'serial_number' => $this->serial_number,
37
+ 'vin' => $this->vin,
33
38
  'make' => $this->make,
34
39
  'model' => $this->model,
35
40
  'year' => $this->year,
@@ -43,8 +48,76 @@ class Vehicle extends FleetbaseResource
43
48
 
44
49
  // Meta flag to indicate this is an index resource
45
50
  'meta' => [
46
- '_index_resource' => true,
51
+ '_index_resource' => true,
52
+ 'current_order_reference' => $this->currentOrderReference(),
53
+ 'location_coordinates' => $this->locationCoordinates(),
54
+ 'speed_label' => $this->speedLabel(),
55
+ 'heading_label' => $this->headingLabel(),
56
+ 'status_label' => $this->statusLabel(),
47
57
  ],
48
58
  ];
49
59
  }
60
+
61
+ protected function currentOrderReference(): ?string
62
+ {
63
+ $this->loadMissing('driver.currentOrder');
64
+ $order = data_get($this, 'driver.currentOrder') ?? $this->currentPositionOrder() ?? $this->currentVehicleOrder();
65
+
66
+ return data_get($order, 'tracking') ?? data_get($order, 'public_id');
67
+ }
68
+
69
+ protected function locationCoordinates(): ?string
70
+ {
71
+ $location = Utils::castPoint($this->location);
72
+
73
+ return $location ? $this->formatCoordinate($location->getLat()) . ' ' . $this->formatCoordinate($location->getLng()) : null;
74
+ }
75
+
76
+ protected function speedLabel(): string
77
+ {
78
+ $speed = data_get($this->lastKnownPosition(), 'speed', data_get($this, 'speed'));
79
+
80
+ return is_numeric($speed) ? ((int) $speed) . ' km/h' : '-';
81
+ }
82
+
83
+ protected function headingLabel(): string
84
+ {
85
+ $heading = data_get($this->lastKnownPosition(), 'heading', data_get($this, 'heading'));
86
+
87
+ return is_numeric($heading) ? ((int) $heading) . ' deg' : '-';
88
+ }
89
+
90
+ protected function statusLabel(): ?string
91
+ {
92
+ return $this->status ? str($this->status)->replace(['_', '-'], ' ')->headline()->toString() : null;
93
+ }
94
+
95
+ protected function currentPositionOrder(): ?Order
96
+ {
97
+ $orderUuid = data_get($this->lastKnownPosition(), 'order_uuid');
98
+
99
+ return $orderUuid ? Order::where('uuid', $orderUuid)->first() : null;
100
+ }
101
+
102
+ protected function currentVehicleOrder(): ?Order
103
+ {
104
+ return Order::where('vehicle_assigned_uuid', $this->uuid)
105
+ ->whereNotIn('status', ['completed', 'canceled', 'cancelled'])
106
+ ->latest()
107
+ ->first();
108
+ }
109
+
110
+ protected function lastKnownPosition()
111
+ {
112
+ if (!$this->resource->relationLoaded('last_known_position')) {
113
+ $this->resource->setRelation('last_known_position', $this->getLastKnownPosition());
114
+ }
115
+
116
+ return $this->resource->getRelation('last_known_position');
117
+ }
118
+
119
+ protected function formatCoordinate(float $coordinate): string
120
+ {
121
+ return (string) round($coordinate, 4);
122
+ }
50
123
  }