@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
@@ -0,0 +1,64 @@
1
+ <?php
2
+
3
+ namespace Fleetbase\FleetOps\Http\Controllers\Api\v1;
4
+
5
+ use Fleetbase\FleetOps\Http\Controllers\Internal\v1\OrchestrationController as InternalOrchestrationController;
6
+ use Illuminate\Http\JsonResponse;
7
+ use Illuminate\Http\Request;
8
+
9
+ /**
10
+ * Public consumable API for FleetOps orchestration.
11
+ *
12
+ * This controller delegates orchestration behavior to the internal workbench
13
+ * controller and only changes the public API serialization boundary.
14
+ */
15
+ class OrchestrationController extends InternalOrchestrationController
16
+ {
17
+ public function run(Request $request): JsonResponse
18
+ {
19
+ return $this->publicResponse(parent::run($request));
20
+ }
21
+
22
+ public function commit(Request $request): JsonResponse
23
+ {
24
+ return $this->publicResponse(parent::commit($request));
25
+ }
26
+
27
+ protected function publicResponse(JsonResponse $response): JsonResponse
28
+ {
29
+ return response()->json(
30
+ $this->sanitizePublicPayload($response->getData(true)),
31
+ $response->getStatusCode()
32
+ );
33
+ }
34
+
35
+ /**
36
+ * Recursively remove internal identifiers from public orchestrator payloads.
37
+ */
38
+ protected function sanitizePublicPayload(mixed $payload): mixed
39
+ {
40
+ if (!is_array($payload)) {
41
+ return $payload;
42
+ }
43
+
44
+ $sanitized = [];
45
+
46
+ foreach ($payload as $key => $value) {
47
+ if (is_string($key) && $this->isInternalIdentifierKey($key)) {
48
+ continue;
49
+ }
50
+
51
+ $sanitized[$key] = $this->sanitizePublicPayload($value);
52
+ }
53
+
54
+ return $sanitized;
55
+ }
56
+
57
+ protected function isInternalIdentifierKey(string $key): bool
58
+ {
59
+ return $key === 'uuid'
60
+ || str_ends_with($key, '_uuid')
61
+ || $key === 'internal_id'
62
+ || $key === 'company_uuid';
63
+ }
64
+ }
@@ -93,17 +93,17 @@ class OrderController extends Controller
93
93
 
94
94
  // create payload
95
95
  if ($request->has('payload') && $request->isArray('payload')) {
96
- $payload = new Payload();
97
- $payloadInput = $request->input('payload');
98
- $entities = data_get($payloadInput, 'entities', []);
99
- $waypoints = data_get($payloadInput, 'waypoints', []);
100
- $pickup = data_get($payloadInput, 'pickup');
101
- $dropoff = data_get($payloadInput, 'dropoff');
102
- $return = data_get($payloadInput, 'return');
103
- $hasPickupField = array_key_exists('pickup', $payloadInput);
104
- $hasDropoffField = array_key_exists('dropoff', $payloadInput);
105
- $hasReturnField = array_key_exists('return', $payloadInput);
106
- $hasWaypointsField = array_key_exists('waypoints', $payloadInput);
96
+ $payload = new Payload();
97
+ $payloadInput = $request->input('payload');
98
+ $entities = data_get($payloadInput, 'entities', []);
99
+ $waypoints = data_get($payloadInput, 'waypoints', []);
100
+ $pickup = data_get($payloadInput, 'pickup');
101
+ $dropoff = data_get($payloadInput, 'dropoff');
102
+ $return = data_get($payloadInput, 'return');
103
+ $hasPickupField = array_key_exists('pickup', $payloadInput);
104
+ $hasDropoffField = array_key_exists('dropoff', $payloadInput);
105
+ $hasReturnField = array_key_exists('return', $payloadInput);
106
+ $hasWaypointsField = array_key_exists('waypoints', $payloadInput);
107
107
  $hasRouteEndpointFields = $hasPickupField || $hasDropoffField || $hasReturnField;
108
108
 
109
109
  if ($pickup) {
@@ -445,17 +445,17 @@ class OrderController extends Controller
445
445
 
446
446
  // create a payload if missing payload[] but has pickup/dropoff/etc
447
447
  if ($request->missing('payload')) {
448
- $payload = data_get($order, 'payload', new Payload());
449
- $payloadInput = $request->only(['pickup', 'dropoff', 'return', 'waypoints', 'entities']);
450
- $entities = data_get($payloadInput, 'entities', []);
451
- $waypoints = data_get($payloadInput, 'waypoints', []);
452
- $pickup = data_get($payloadInput, 'pickup');
453
- $dropoff = data_get($payloadInput, 'dropoff');
454
- $return = data_get($payloadInput, 'return');
455
- $hasPickupField = $request->exists('pickup');
456
- $hasDropoffField = $request->exists('dropoff');
457
- $hasReturnField = $request->exists('return');
458
- $hasWaypointsField = $request->exists('waypoints');
448
+ $payload = data_get($order, 'payload', new Payload());
449
+ $payloadInput = $request->only(['pickup', 'dropoff', 'return', 'waypoints', 'entities']);
450
+ $entities = data_get($payloadInput, 'entities', []);
451
+ $waypoints = data_get($payloadInput, 'waypoints', []);
452
+ $pickup = data_get($payloadInput, 'pickup');
453
+ $dropoff = data_get($payloadInput, 'dropoff');
454
+ $return = data_get($payloadInput, 'return');
455
+ $hasPickupField = $request->exists('pickup');
456
+ $hasDropoffField = $request->exists('dropoff');
457
+ $hasReturnField = $request->exists('return');
458
+ $hasWaypointsField = $request->exists('waypoints');
459
459
  $hasRouteEndpointFields = $hasPickupField || $hasDropoffField || $hasReturnField;
460
460
 
461
461
  // if no pickup and dropoff extract from waypoints
@@ -1328,13 +1328,13 @@ class OrderController extends Controller
1328
1328
  *
1329
1329
  * @return \Illuminate\Http\Response
1330
1330
  */
1331
- public function trackerData(string $id)
1331
+ public function trackerData(Request $request, string $id)
1332
1332
  {
1333
1333
  set_time_limit(280);
1334
1334
 
1335
1335
  try {
1336
1336
  $order = Order::findRecordOrFail($id);
1337
- $data = $order->tracker()->toArray();
1337
+ $data = $order->tracker()->toArray($request->only(['provider', 'fallbacks', 'traffic_enabled']));
1338
1338
 
1339
1339
  return response()->json($data);
1340
1340
  } catch (ModelNotFoundException $e) {
@@ -1351,11 +1351,11 @@ class OrderController extends Controller
1351
1351
  *
1352
1352
  * @return \Illuminate\Http\Response
1353
1353
  */
1354
- public function etaData(string $id)
1354
+ public function etaData(Request $request, string $id)
1355
1355
  {
1356
1356
  try {
1357
1357
  $order = Order::findRecordOrFail($id);
1358
- $data = $order->tracker()->eta();
1358
+ $data = $order->tracker()->eta($request->only(['provider', 'fallbacks', 'traffic_enabled']));
1359
1359
 
1360
1360
  return response()->json($data);
1361
1361
  } catch (ModelNotFoundException $e) {
@@ -25,16 +25,16 @@ class PayloadController extends Controller
25
25
  */
26
26
  public function create(CreatePayloadRequest $request)
27
27
  {
28
- $input = $request->all();
29
- $entities = data_get($input, 'entities', []);
30
- $waypoints = data_get($input, 'waypoints', []);
31
- $pickup = data_get($input, 'pickup');
32
- $dropoff = data_get($input, 'dropoff');
33
- $return = data_get($input, 'return');
34
- $hasPickupField = array_key_exists('pickup', $input);
35
- $hasDropoffField = array_key_exists('dropoff', $input);
36
- $hasReturnField = array_key_exists('return', $input);
37
- $hasWaypointsField = array_key_exists('waypoints', $input);
28
+ $input = $request->all();
29
+ $entities = data_get($input, 'entities', []);
30
+ $waypoints = data_get($input, 'waypoints', []);
31
+ $pickup = data_get($input, 'pickup');
32
+ $dropoff = data_get($input, 'dropoff');
33
+ $return = data_get($input, 'return');
34
+ $hasPickupField = array_key_exists('pickup', $input);
35
+ $hasDropoffField = array_key_exists('dropoff', $input);
36
+ $hasReturnField = array_key_exists('return', $input);
37
+ $hasWaypointsField = array_key_exists('waypoints', $input);
38
38
  $hasRouteEndpointFields = $hasPickupField || $hasDropoffField || $hasReturnField;
39
39
 
40
40
  // make sure company is set
@@ -231,7 +231,7 @@ class ServiceQuoteController extends Controller
231
231
  $entities = collect($entities)->mapInto(Entity::class);
232
232
 
233
233
  // should all be Place like
234
- $waypoints = collect([$pickup, ...$waypoints, $dropoff])->filter();
234
+ $waypoints = collect([$pickup, ...$waypoints, $dropoff])->filter();
235
235
  $endpointCount = (int) ($pickup instanceof Place) + (int) ($dropoff instanceof Place);
236
236
 
237
237
  // if facilitator is an integrated partner resolve service quotes from bridge
@@ -0,0 +1,119 @@
1
+ <?php
2
+
3
+ namespace Fleetbase\FleetOps\Http\Controllers\Internal\v1;
4
+
5
+ use Fleetbase\FleetOps\Support\Analytics\AbstractAnalytics;
6
+ use Fleetbase\FleetOps\Support\Analytics\FuelEfficiency;
7
+ use Fleetbase\FleetOps\Support\Analytics\GeofenceViolations;
8
+ use Fleetbase\FleetOps\Support\Analytics\IssuesInsights;
9
+ use Fleetbase\FleetOps\Support\Analytics\LiveFleet;
10
+ use Fleetbase\FleetOps\Support\Analytics\MaintenanceOverview;
11
+ use Fleetbase\FleetOps\Support\Analytics\OnTimeDelivery;
12
+ use Fleetbase\FleetOps\Support\Analytics\OperationsPulse;
13
+ use Fleetbase\FleetOps\Support\Analytics\OrdersByStatus;
14
+ use Fleetbase\FleetOps\Support\Analytics\RevenueTrend;
15
+ use Fleetbase\FleetOps\Support\Analytics\TopDrivers;
16
+ use Fleetbase\Http\Controllers\Controller;
17
+ use Illuminate\Http\Request;
18
+
19
+ /**
20
+ * One public method per dashboard widget. Each method:
21
+ * - Resolves a period from ?period=7d|30d|... or explicit start/end
22
+ * - Constructs the corresponding Support\Analytics\* helper
23
+ * - Returns the helper's JSON payload (Chart.js datasets, ranked rows,
24
+ * scalar tiles, etc. — shape is widget-specific)
25
+ *
26
+ * Cache headers are set per-widget by the route middleware in routes.php.
27
+ */
28
+ class AnalyticsController extends Controller
29
+ {
30
+ public function operationsPulse(Request $request)
31
+ {
32
+ return $this->run($request, OperationsPulse::class);
33
+ }
34
+
35
+ public function revenueTrend(Request $request)
36
+ {
37
+ return $this->run($request, RevenueTrend::class, function (RevenueTrend $a) use ($request) {
38
+ $a->groupBy($request->string('group_by', 'day')->toString());
39
+ });
40
+ }
41
+
42
+ public function ordersByStatus(Request $request)
43
+ {
44
+ return $this->run($request, OrdersByStatus::class, defaultDays: 14);
45
+ }
46
+
47
+ public function onTimeDelivery(Request $request)
48
+ {
49
+ return $this->run($request, OnTimeDelivery::class, function (OnTimeDelivery $a) use ($request) {
50
+ $a->slaMinutes((int) $request->input('sla_minutes', 30));
51
+ });
52
+ }
53
+
54
+ public function topDrivers(Request $request)
55
+ {
56
+ return $this->run($request, TopDrivers::class, function (TopDrivers $a) use ($request) {
57
+ $a->limit((int) $request->input('limit', 10));
58
+ $a->sortBy($request->string('sort_by', 'orders_completed')->toString());
59
+ });
60
+ }
61
+
62
+ public function fuelEfficiency(Request $request)
63
+ {
64
+ return $this->run($request, FuelEfficiency::class, defaultDays: 90);
65
+ }
66
+
67
+ public function issuesInsights(Request $request)
68
+ {
69
+ return $this->run($request, IssuesInsights::class);
70
+ }
71
+
72
+ public function maintenanceOverview(Request $request)
73
+ {
74
+ return $this->run($request, MaintenanceOverview::class);
75
+ }
76
+
77
+ public function geofenceViolations(Request $request)
78
+ {
79
+ return $this->run($request, GeofenceViolations::class, defaultDays: 7);
80
+ }
81
+
82
+ public function liveFleet(Request $request)
83
+ {
84
+ return $this->run($request, LiveFleet::class);
85
+ }
86
+
87
+ /**
88
+ * Shared boilerplate: period parsing + helper construction + JSON response.
89
+ *
90
+ * @param class-string<AbstractAnalytics> $class
91
+ */
92
+ private function run(Request $request, string $class, ?callable $configure = null, int $defaultDays = 30)
93
+ {
94
+ [$start, $end] = AbstractAnalytics::resolvePeriod(
95
+ $request->string('period')->toString() ?: null,
96
+ $request->date('start'),
97
+ $request->date('end'),
98
+ $defaultDays,
99
+ );
100
+
101
+ try {
102
+ /** @var AbstractAnalytics $analytics */
103
+ $analytics = $class::forCompany($request->user()->company)->between($start, $end);
104
+
105
+ if ($configure !== null) {
106
+ $configure($analytics);
107
+ }
108
+
109
+ return response()->json($analytics->get());
110
+ } catch (\Throwable $e) {
111
+ report($e);
112
+
113
+ return response()->json([
114
+ 'error' => $e->getMessage(),
115
+ 'widget' => $class,
116
+ ], 500);
117
+ }
118
+ }
119
+ }
@@ -8,6 +8,7 @@ use Fleetbase\FleetOps\Imports\ContactImport;
8
8
  use Fleetbase\FleetOps\Models\Contact;
9
9
  use Fleetbase\Http\Requests\ExportRequest;
10
10
  use Fleetbase\Http\Requests\ImportRequest;
11
+ use Fleetbase\Models\User;
11
12
  use Illuminate\Http\Request;
12
13
  use Illuminate\Support\Str;
13
14
  use Maatwebsite\Excel\Facades\Excel;
@@ -21,11 +22,35 @@ class ContactController extends FleetOpsController
21
22
  */
22
23
  public $resource = 'contact';
23
24
 
25
+ /**
26
+ * Handle pre-create transactions.
27
+ */
28
+ public function onBeforeCreate(Request $request, array &$input)
29
+ {
30
+ $this->resolveUserInput($request, $input);
31
+ }
32
+
33
+ /**
34
+ * Handle pre-update transactions.
35
+ */
36
+ public function onBeforeUpdate(Request $request, Contact $contact, array &$input)
37
+ {
38
+ if ($contact->type === 'customer' && isset($input['type']) && $input['type'] !== 'customer') {
39
+ throw new \Exception('Customer contact type cannot be changed.');
40
+ }
41
+
42
+ $this->resolveUserInput($request, $input);
43
+ }
44
+
24
45
  /**
25
46
  * Handle post save transactions.
26
47
  */
27
48
  public function afterSave(Request $request, Contact $contact)
28
49
  {
50
+ if ($contact->type === 'customer') {
51
+ $contact->normalizeCustomerUser();
52
+ }
53
+
29
54
  $customFieldValues = $request->array('contact.custom_field_values');
30
55
  if ($customFieldValues) {
31
56
  $contact->syncCustomFieldValues($customFieldValues);
@@ -105,4 +130,20 @@ class ContactController extends FleetOpsController
105
130
 
106
131
  return response()->json(['status' => 'ok', 'message' => 'Import completed', 'imported' => $importedCount]);
107
132
  }
133
+
134
+ private function resolveUserInput(Request $request, array &$input): void
135
+ {
136
+ $user = data_get($input, 'user_uuid') ?? data_get($input, 'user') ?? $request->input('contact.user_uuid') ?? $request->input('contact.user');
137
+
138
+ if (is_array($user)) {
139
+ $user = data_get($user, 'uuid') ?? data_get($user, 'id');
140
+ }
141
+
142
+ if (!$user) {
143
+ return;
144
+ }
145
+
146
+ $input['user_uuid'] = User::where('uuid', $user)->orWhere('public_id', $user)->value('uuid') ?? $user;
147
+ unset($input['user']);
148
+ }
108
149
  }
@@ -13,6 +13,7 @@ use Fleetbase\FleetOps\Models\Place;
13
13
  use Fleetbase\FleetOps\Models\Route;
14
14
  use Fleetbase\FleetOps\Models\Vehicle;
15
15
  use Fleetbase\FleetOps\Support\LiveCacheService;
16
+ use Fleetbase\FleetOps\Support\LiveOrderQuery;
16
17
  use Fleetbase\Http\Controllers\Controller;
17
18
  use Illuminate\Http\Request;
18
19
 
@@ -110,50 +111,13 @@ class LiveController extends Controller
110
111
  ];
111
112
 
112
113
  return LiveCacheService::remember('orders', $cacheParams, function () use ($exclude, $active, $unassigned) {
113
- $query = Order::where('company_uuid', session('company'))
114
- ->whereHas('payload', function ($query) {
115
- $query->where(
116
- function ($q) {
117
- $q->whereHas('waypoints');
118
- $q->orWhereHas('pickup');
119
- $q->orWhereHas('dropoff');
120
- }
121
- );
122
- })
123
- ->whereNotIn('status', ['canceled', 'completed', 'expired'])
124
- ->whereHas('trackingNumber')
125
- ->whereHas('trackingStatuses')
126
- ->whereNotIn('public_id', $exclude)
127
- ->whereNull('deleted_at')
128
- ->applyDirectivesForPermissions('fleet-ops list order')
129
- ->with([
130
- 'payload.entities',
131
- 'payload.dropoff',
132
- 'payload.pickup',
133
- 'payload.return',
134
- 'payload.firstWaypointMarker.place',
135
- 'payload.lastWaypointMarker.place',
136
- 'trackingNumber',
137
- 'trackingStatuses',
138
- 'driverAssigned' => function ($query) {
139
- $query->without(['jobs', 'currentJob']);
140
- },
141
- 'vehicleAssigned' => function ($query) {
142
- $query->without(['fleets', 'vendor']);
143
- },
144
- 'customer',
145
- 'facilitator',
114
+ $query = LiveOrderQuery::make(session('company'), [
115
+ 'exclude' => $exclude,
116
+ 'active' => $active,
117
+ 'unassigned' => $unassigned,
118
+ 'with_relations' => true,
146
119
  ]);
147
120
 
148
- if ($active) {
149
- $query->whereHas('driverAssigned');
150
- $query->whereNotIn('status', ['created', 'completed', 'expired', 'order_canceled', 'canceled', 'pending']);
151
- }
152
-
153
- if ($unassigned) {
154
- $query->whereNull('driver_assigned_uuid');
155
- }
156
-
157
121
  $query->limit(60); // max 60 latest
158
122
  $query->latest();
159
123
 
@@ -3,15 +3,17 @@
3
3
  namespace Fleetbase\FleetOps\Http\Controllers\Internal\v1;
4
4
 
5
5
  use Fleetbase\FleetOps\Support\Metrics;
6
+ use Fleetbase\FleetOps\Support\Metrics\Registry;
6
7
  use Fleetbase\Http\Controllers\Controller;
7
8
  use Illuminate\Http\Request;
9
+ use Illuminate\Support\Carbon;
8
10
 
9
11
  class MetricsController extends Controller
10
12
  {
11
13
  /**
12
- * Gets all key metrics for the current session.
13
- *
14
- * @return \Illuminate\Http\Response
14
+ * Legacy bulk endpoint. Returns a flat map of slug → scalar value for the
15
+ * requested period. Preserved for backward compat; the dashboard widgets
16
+ * now prefer per-slug GET /metrics/{slug}.
15
17
  */
16
18
  public function all(Request $request)
17
19
  {
@@ -27,4 +29,81 @@ class MetricsController extends Controller
27
29
 
28
30
  return response()->json($data);
29
31
  }
32
+
33
+ /**
34
+ * Per-metric endpoint backing <Widget::KpiTile>.
35
+ *
36
+ * Accepts:
37
+ * ?period=7d|30d|90d OR ?start=ISO&end=ISO
38
+ * ?sparkline=true to include a per-day mini-chart series
39
+ * ?compare=true to include period-over-period delta (default: true)
40
+ *
41
+ * Returns: { slug, value, format, currency?, delta_pct?, sparkline? }
42
+ */
43
+ public function show(Request $request, string $slug)
44
+ {
45
+ $class = Registry::resolve($slug);
46
+ if ($class === null) {
47
+ return response()->json(['error' => "Unknown metric: {$slug}"], 404);
48
+ }
49
+
50
+ [$start, $end] = $this->resolvePeriod($request);
51
+
52
+ $metric = $class::forCompany($request->user()->company)->between($start, $end);
53
+
54
+ if ($request->boolean('compare', true)) {
55
+ $duration = $end->getTimestamp() - $start->getTimestamp();
56
+ $compareEnd = (clone $start);
57
+ $compareStart = (clone $start)->modify("-{$duration} seconds");
58
+ $metric->compareTo($compareStart, $compareEnd);
59
+ }
60
+
61
+ if ($request->boolean('sparkline')) {
62
+ $buckets = (int) $request->input('sparkline_buckets', 14);
63
+ $metric->withSparkline($buckets, 'day');
64
+ }
65
+
66
+ return response()->json($metric->get());
67
+ }
68
+
69
+ /**
70
+ * Parse ?period=7d/30d/90d into a [start, end] pair, falling back to
71
+ * explicit ?start=&end= ISO dates, then to a 30-day default.
72
+ */
73
+ private function resolvePeriod(Request $request): array
74
+ {
75
+ $period = $request->string('period')->toString();
76
+
77
+ if ($period !== '') {
78
+ $days = match ($period) {
79
+ '7d' => 7,
80
+ '14d' => 14,
81
+ '30d' => 30,
82
+ '90d' => 90,
83
+ '180d' => 180,
84
+ '365d' => 365,
85
+ default => null,
86
+ };
87
+
88
+ if ($days !== null) {
89
+ $end = Carbon::now()->toDateTime();
90
+ $start = Carbon::now()->subDays($days)->toDateTime();
91
+
92
+ return [$start, $end];
93
+ }
94
+ }
95
+
96
+ $start = $request->date('start') ?? Carbon::now()->subDays(30)->toDateTime();
97
+ $end = $request->date('end') ?? Carbon::now()->toDateTime();
98
+
99
+ if (!$start instanceof \DateTime) {
100
+ $start = Carbon::parse($start)->toDateTime();
101
+ }
102
+
103
+ if (!$end instanceof \DateTime) {
104
+ $end = Carbon::parse($end)->toDateTime();
105
+ }
106
+
107
+ return [$start, $end];
108
+ }
30
109
  }
@@ -573,7 +573,7 @@ class OrchestrationController extends Controller
573
573
  $isMulti = $orderType === 'multi_waypoint';
574
574
 
575
575
  // ── Resolve OrderConfig ───────────────────────────────────────
576
- $orderConfigUuid = null;
576
+ $orderConfigUuid = null;
577
577
  $resolvedOrderConfig = null;
578
578
  if (!empty($firstRow['type'])) {
579
579
  $resolvedOrderConfig = OrderConfig::resolveFromIdentifier($firstRow['type']);