@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,180 @@
1
+ <?php
2
+
3
+ namespace Fleetbase\FleetOps\Tracking;
4
+
5
+ use Fleetbase\FleetOps\Models\Order;
6
+ use Illuminate\Support\Carbon;
7
+ use Illuminate\Support\Facades\Cache;
8
+
9
+ class TrackingIntelligenceService
10
+ {
11
+ public function __construct(
12
+ protected TrackingContextBuilder $contextBuilder,
13
+ protected TrackingProviderManager $providerManager,
14
+ ) {
15
+ }
16
+
17
+ public function track(Order $order, array|TrackingOptions $options = []): array
18
+ {
19
+ $options = $options instanceof TrackingOptions ? $options : TrackingOptions::fromArray($options);
20
+ $context = $this->contextBuilder->build($order, $options);
21
+ $cacheKey = $this->cacheKey($context, $options);
22
+
23
+ return Cache::remember($cacheKey, $options->cacheTtlSeconds, function () use ($context, $options) {
24
+ return $this->buildResult($context, $this->providerManager->track($context, $options), $options);
25
+ });
26
+ }
27
+
28
+ public function eta(Order $order, array|TrackingOptions $options = []): array
29
+ {
30
+ $tracker = $this->track($order, $options);
31
+
32
+ return [
33
+ 'active_stop_seconds' => data_get($tracker, 'eta.active_stop_seconds'),
34
+ 'completion_seconds' => data_get($tracker, 'eta.completion_seconds'),
35
+ 'active_stop_at' => data_get($tracker, 'eta.active_stop_at'),
36
+ 'completion_at' => data_get($tracker, 'eta.completion_at'),
37
+ 'provider' => data_get($tracker, 'provider'),
38
+ 'confidence' => data_get($tracker, 'confidence'),
39
+ 'warnings' => data_get($tracker, 'warnings', []),
40
+ ];
41
+ }
42
+
43
+ protected function buildResult(TrackingContext $context, TrackingProviderResult $providerResult, TrackingOptions $options): array
44
+ {
45
+ $now = now();
46
+ $completionSeconds = $providerResult->durationInTrafficSeconds ?? $providerResult->durationSeconds;
47
+ $activeStopSeconds = data_get($providerResult->legs, '0.duration_in_traffic_s', data_get($providerResult->legs, '0.duration_s', $completionSeconds));
48
+ $progress = $this->progress($context, $providerResult);
49
+ $warnings = array_values(array_unique([...$context->warnings, ...$providerResult->warnings]));
50
+
51
+ if ($providerResult->confidence === 'low') {
52
+ $warnings[] = 'low_confidence_eta';
53
+ }
54
+
55
+ return [
56
+ 'provider' => $providerResult->provider,
57
+ 'fallback_provider' => in_array('fallback_used', $warnings) ? $providerResult->provider : null,
58
+ 'generated_at' => $now->toISOString(),
59
+ 'confidence' => $providerResult->confidence,
60
+ 'warnings' => array_values(array_unique($warnings)),
61
+ 'driver' => [
62
+ 'location' => $this->pointToGeoJson($context->driverLocation),
63
+ 'location_age_seconds' => $context->driverLocationAgeSeconds,
64
+ 'online' => (bool) data_get($context->driver, 'online', false),
65
+ ],
66
+ 'progress' => $progress,
67
+ 'stops' => $context->stops->map(fn (TrackingStop $stop) => $stop->toArray())->values()->all(),
68
+ 'active_stop' => $context->activeStop?->toArray(),
69
+ 'next_stop' => $context->nextStop?->toArray(),
70
+ 'route' => [
71
+ 'distance_m' => $providerResult->distanceMeters,
72
+ 'duration_s' => $providerResult->durationSeconds,
73
+ 'duration_in_traffic_s' => $providerResult->durationInTrafficSeconds,
74
+ 'polyline' => $providerResult->polyline,
75
+ 'coordinates' => $providerResult->coordinates,
76
+ 'legs' => $this->legs($context, $providerResult, $now),
77
+ ],
78
+ 'eta' => [
79
+ 'active_stop_seconds' => $activeStopSeconds,
80
+ 'completion_seconds' => $completionSeconds,
81
+ 'active_stop_at' => $this->addSeconds($now, $activeStopSeconds),
82
+ 'completion_at' => $this->addSeconds($now, $completionSeconds),
83
+ ],
84
+ 'insights' => [
85
+ 'is_delayed' => false,
86
+ 'delay_seconds' => 0,
87
+ 'is_location_stale' => in_array('stale_driver_location', $warnings),
88
+ 'is_off_route' => in_array('off_route', $warnings),
89
+ ],
90
+ 'capabilities' => $this->capabilitiesFor($providerResult->provider),
91
+ ];
92
+ }
93
+
94
+ protected function progress(TrackingContext $context, TrackingProviderResult $providerResult): array
95
+ {
96
+ $totalStops = max($context->stops->count(), 1);
97
+ $completedStops = $context->completedStops->count();
98
+ $percentage = $context->order->status === 'completed' ? 100 : round(($completedStops / $totalStops) * 100, 2);
99
+
100
+ return [
101
+ 'percentage' => $percentage,
102
+ 'completed_stops' => $completedStops,
103
+ 'remaining_stops' => $context->remainingStops->count(),
104
+ 'total_stops' => $context->stops->count(),
105
+ 'completed_distance_m' => null,
106
+ 'remaining_distance_m' => $providerResult->distanceMeters,
107
+ ];
108
+ }
109
+
110
+ protected function legs(TrackingContext $context, TrackingProviderResult $providerResult, Carbon $now): array
111
+ {
112
+ $elapsedSeconds = 0;
113
+
114
+ return collect($providerResult->legs)->map(function ($leg, $index) use ($context, $now, &$elapsedSeconds) {
115
+ $stop = $context->remainingStops->values()->get($index);
116
+ $legSeconds = data_get($leg, 'duration_in_traffic_s', data_get($leg, 'duration_s'));
117
+ $etaSeconds = null;
118
+ $etaAt = null;
119
+
120
+ if ($legSeconds !== null) {
121
+ $elapsedSeconds += (float) $legSeconds;
122
+ $etaSeconds = $elapsedSeconds;
123
+ $etaAt = $this->addSeconds($now, $etaSeconds);
124
+ }
125
+
126
+ return array_merge($leg, [
127
+ 'stop' => $stop instanceof TrackingStop ? $stop->toArray() : null,
128
+ 'eta_seconds' => $etaSeconds,
129
+ 'eta_at' => $etaAt,
130
+ ]);
131
+ })->all();
132
+ }
133
+
134
+ protected function capabilitiesFor(string $provider): array
135
+ {
136
+ $registry = app(TrackingProviderRegistry::class);
137
+ $registered = $registry->get($provider);
138
+
139
+ return $registered ? $registered->capabilities()->toArray() : (new TrackingProviderCapabilities())->toArray();
140
+ }
141
+
142
+ protected function cacheKey(TrackingContext $context, TrackingOptions $options): string
143
+ {
144
+ return 'order_tracking_intelligence:' . md5(json_encode([
145
+ 'order' => $context->order->uuid,
146
+ 'order_ts' => optional($context->order->updated_at)->timestamp,
147
+ 'payload' => optional($context->payload)->uuid,
148
+ 'payload_ts' => optional($context->payload?->updated_at)->timestamp,
149
+ 'driver' => optional($context->driver)->uuid,
150
+ 'driver_ts' => optional($context->driver?->updated_at)->timestamp,
151
+ 'stops' => $context->stateSignature(),
152
+ 'provider' => $options->provider,
153
+ 'fallbacks' => $options->fallbacks,
154
+ 'traffic' => $options->trafficEnabled,
155
+ 'settings' => [
156
+ 'summary_ttl' => $options->cacheTtlSeconds,
157
+ 'route_ttl' => $options->routeCacheTtlSeconds,
158
+ 'stale_threshold' => $options->staleLocationThresholdSeconds,
159
+ 'fallback_speed' => $options->defaultVehicleSpeedKph,
160
+ ],
161
+ ]));
162
+ }
163
+
164
+ protected function pointToGeoJson($point): ?array
165
+ {
166
+ if (!$point) {
167
+ return null;
168
+ }
169
+
170
+ return [
171
+ 'type' => 'Point',
172
+ 'coordinates' => [$point->getLng(), $point->getLat()],
173
+ ];
174
+ }
175
+
176
+ protected function addSeconds(Carbon $now, ?float $seconds): ?string
177
+ {
178
+ return $seconds === null ? null : $now->copy()->addSeconds((int) round($seconds))->toISOString();
179
+ }
180
+ }
@@ -0,0 +1,49 @@
1
+ <?php
2
+
3
+ namespace Fleetbase\FleetOps\Tracking;
4
+
5
+ use Fleetbase\Models\Setting;
6
+ use Illuminate\Support\Arr;
7
+
8
+ class TrackingOptions
9
+ {
10
+ public function __construct(
11
+ public ?string $provider = null,
12
+ public array $fallbacks = [],
13
+ public bool $trafficEnabled = true,
14
+ public int $cacheTtlSeconds = 60,
15
+ public int $routeCacheTtlSeconds = 600,
16
+ public int $staleLocationThresholdSeconds = 300,
17
+ public float $defaultVehicleSpeedKph = 35,
18
+ public array $raw = [],
19
+ ) {
20
+ }
21
+
22
+ public static function fromArray(array $options = []): self
23
+ {
24
+ $systemSettings = Setting::lookup('fleet-ops.tracking-settings', []);
25
+ $config = array_merge(config('fleetops.tracking', []), is_array($systemSettings) ? $systemSettings : []);
26
+ $companySettings = [];
27
+ try {
28
+ $companySettings = Setting::lookupCompany('tracking', []);
29
+ } catch (\Throwable) {
30
+ $companySettings = [];
31
+ }
32
+
33
+ $fallbacks = Arr::get($options, 'fallbacks', Arr::get($companySettings, 'fallbacks', Arr::get($config, 'fallbacks', [])));
34
+ if (is_string($fallbacks)) {
35
+ $fallbacks = array_values(array_filter(array_map('trim', explode(',', $fallbacks))));
36
+ }
37
+
38
+ return new self(
39
+ provider: Arr::get($options, 'provider', Arr::get($companySettings, 'provider', Arr::get($config, 'provider'))),
40
+ fallbacks: Arr::wrap($fallbacks),
41
+ trafficEnabled: (bool) Arr::get($options, 'traffic_enabled', Arr::get($companySettings, 'traffic_enabled', Arr::get($config, 'traffic_enabled', true))),
42
+ cacheTtlSeconds: (int) Arr::get($options, 'cache_ttl_seconds', Arr::get($companySettings, 'cache_ttl_seconds', Arr::get($config, 'cache_ttl_seconds', 60))),
43
+ routeCacheTtlSeconds: (int) Arr::get($options, 'route_cache_ttl_seconds', Arr::get($companySettings, 'route_cache_ttl_seconds', Arr::get($config, 'route_cache_ttl_seconds', 600))),
44
+ staleLocationThresholdSeconds: (int) Arr::get($options, 'stale_location_threshold_seconds', Arr::get($companySettings, 'stale_location_threshold_seconds', Arr::get($config, 'stale_location_threshold_seconds', 300))),
45
+ defaultVehicleSpeedKph: (float) Arr::get($options, 'default_vehicle_speed_kph', Arr::get($companySettings, 'default_vehicle_speed_kph', Arr::get($config, 'default_vehicle_speed_kph', 35))),
46
+ raw: $options
47
+ );
48
+ }
49
+ }
@@ -0,0 +1,30 @@
1
+ <?php
2
+
3
+ namespace Fleetbase\FleetOps\Tracking;
4
+
5
+ class TrackingProviderCapabilities implements \JsonSerializable
6
+ {
7
+ public function __construct(
8
+ public bool $traffic = false,
9
+ public bool $perLegEta = false,
10
+ public bool $mapMatching = false,
11
+ public bool $routeGeometry = false,
12
+ public array $extras = [],
13
+ ) {
14
+ }
15
+
16
+ public function toArray(): array
17
+ {
18
+ return array_merge([
19
+ 'traffic' => $this->traffic,
20
+ 'per_leg_eta' => $this->perLegEta,
21
+ 'map_matching' => $this->mapMatching,
22
+ 'route_geometry' => $this->routeGeometry,
23
+ ], $this->extras);
24
+ }
25
+
26
+ public function jsonSerialize(): mixed
27
+ {
28
+ return $this->toArray();
29
+ }
30
+ }
@@ -0,0 +1,89 @@
1
+ <?php
2
+
3
+ namespace Fleetbase\FleetOps\Tracking;
4
+
5
+ use Fleetbase\FleetOps\Tracking\Contracts\TrackingProviderInterface;
6
+ use Illuminate\Support\Arr;
7
+ use Illuminate\Support\Facades\Cache;
8
+ use Illuminate\Support\Facades\Log;
9
+ use Illuminate\Support\Str;
10
+
11
+ class TrackingProviderManager
12
+ {
13
+ public function __construct(protected TrackingProviderRegistry $registry)
14
+ {
15
+ }
16
+
17
+ public function track(TrackingContext $context, TrackingOptions $options): TrackingProviderResult
18
+ {
19
+ $warnings = [];
20
+ foreach ($this->providerOrder($options) as $providerKey) {
21
+ $provider = $this->registry->get($providerKey);
22
+ if (!$provider instanceof TrackingProviderInterface) {
23
+ $warnings[] = 'provider_not_registered:' . $providerKey;
24
+ continue;
25
+ }
26
+
27
+ if (!$provider->canTrack($context)) {
28
+ $warnings[] = 'provider_unavailable:' . $providerKey;
29
+ continue;
30
+ }
31
+
32
+ try {
33
+ $result = $this->trackWithProviderCache($provider, $context, $options);
34
+ $result->warnings = array_values(array_unique([...$warnings, ...$result->warnings]));
35
+
36
+ if (Str::snake($result->provider) !== Str::snake((string) $options->provider)) {
37
+ $result->warnings[] = 'fallback_used';
38
+ }
39
+
40
+ return $result;
41
+ } catch (\Throwable $e) {
42
+ $warnings[] = 'provider_failed:' . $providerKey;
43
+ Log::warning('Tracking provider failed.', [
44
+ 'provider' => $providerKey,
45
+ 'order' => $context->order->public_id ?? $context->order->uuid,
46
+ 'error' => $e->getMessage(),
47
+ ]);
48
+ }
49
+ }
50
+
51
+ return new TrackingProviderResult(
52
+ provider: 'none',
53
+ warnings: array_values(array_unique([...$warnings, 'no_tracking_provider_available'])),
54
+ confidence: 'none'
55
+ );
56
+ }
57
+
58
+ protected function providerOrder(TrackingOptions $options): array
59
+ {
60
+ $provider = $options->provider ?: Arr::get(config('fleetops.tracking', []), 'provider', 'google_routes');
61
+ $fallbacks = $options->fallbacks ?: Arr::get(config('fleetops.tracking', []), 'fallbacks', ['osrm', 'calculated']);
62
+
63
+ return collect([$provider, ...$fallbacks])
64
+ ->filter()
65
+ ->map(fn ($key) => Str::snake($key))
66
+ ->unique()
67
+ ->values()
68
+ ->all();
69
+ }
70
+
71
+ protected function trackWithProviderCache(TrackingProviderInterface $provider, TrackingContext $context, TrackingOptions $options): TrackingProviderResult
72
+ {
73
+ return Cache::remember($this->providerCacheKey($provider, $context, $options), $options->routeCacheTtlSeconds, function () use ($provider, $context, $options) {
74
+ return $provider->track($context, $options);
75
+ });
76
+ }
77
+
78
+ protected function providerCacheKey(TrackingProviderInterface $provider, TrackingContext $context, TrackingOptions $options): string
79
+ {
80
+ return 'order_tracking_provider_route:' . md5(json_encode([
81
+ 'provider' => $provider->key(),
82
+ 'points' => array_map(function ($point) {
83
+ return [$point->getLat(), $point->getLng()];
84
+ }, $context->routePoints()),
85
+ 'traffic' => $options->trafficEnabled,
86
+ 'speed' => $options->defaultVehicleSpeedKph,
87
+ ]));
88
+ }
89
+ }
@@ -0,0 +1,35 @@
1
+ <?php
2
+
3
+ namespace Fleetbase\FleetOps\Tracking;
4
+
5
+ use Fleetbase\FleetOps\Tracking\Contracts\TrackingProviderInterface;
6
+ use Illuminate\Support\Str;
7
+
8
+ class TrackingProviderRegistry
9
+ {
10
+ protected array $providers = [];
11
+
12
+ public function register(TrackingProviderInterface|string $provider, ?string $key = null): self
13
+ {
14
+ $instance = is_string($provider) ? app($provider) : $provider;
15
+ $providerKey = Str::snake($key ?? $instance->key());
16
+ $this->providers[$providerKey] = $instance;
17
+
18
+ return $this;
19
+ }
20
+
21
+ public function has(string $key): bool
22
+ {
23
+ return isset($this->providers[Str::snake($key)]);
24
+ }
25
+
26
+ public function get(string $key): ?TrackingProviderInterface
27
+ {
28
+ return $this->providers[Str::snake($key)] ?? null;
29
+ }
30
+
31
+ public function all(): array
32
+ {
33
+ return $this->providers;
34
+ }
35
+ }
@@ -0,0 +1,20 @@
1
+ <?php
2
+
3
+ namespace Fleetbase\FleetOps\Tracking;
4
+
5
+ class TrackingProviderResult
6
+ {
7
+ public function __construct(
8
+ public string $provider,
9
+ public ?float $distanceMeters = null,
10
+ public ?float $durationSeconds = null,
11
+ public ?float $durationInTrafficSeconds = null,
12
+ public ?string $polyline = null,
13
+ public array $coordinates = [],
14
+ public array $legs = [],
15
+ public array $warnings = [],
16
+ public string $confidence = 'medium',
17
+ public ?array $raw = null,
18
+ ) {
19
+ }
20
+ }
@@ -0,0 +1,59 @@
1
+ <?php
2
+
3
+ namespace Fleetbase\FleetOps\Tracking;
4
+
5
+ use Fleetbase\FleetOps\Models\Place;
6
+ use Fleetbase\FleetOps\Models\Waypoint;
7
+ use Fleetbase\FleetOps\Support\Utils;
8
+ use Fleetbase\LaravelMysqlSpatial\Types\Point;
9
+
10
+ class TrackingStop implements \JsonSerializable
11
+ {
12
+ public function __construct(
13
+ public ?string $uuid,
14
+ public ?string $publicId,
15
+ public string $type,
16
+ public ?string $status,
17
+ public ?Place $place,
18
+ public ?Waypoint $waypoint = null,
19
+ public bool $completed = false,
20
+ public ?int $sequence = null,
21
+ ) {
22
+ }
23
+
24
+ public function point(): ?Point
25
+ {
26
+ try {
27
+ return Utils::getPointFromMixed($this->place);
28
+ } catch (\Throwable) {
29
+ return null;
30
+ }
31
+ }
32
+
33
+ public function toArray(): array
34
+ {
35
+ $point = $this->point();
36
+
37
+ return [
38
+ 'uuid' => $this->uuid,
39
+ 'public_id' => $this->publicId,
40
+ 'type' => $this->type,
41
+ 'status' => $this->status,
42
+ 'completed' => $this->completed,
43
+ 'sequence' => $this->sequence,
44
+ 'address' => data_get($this->place, 'address'),
45
+ 'name' => data_get($this->place, 'name'),
46
+ 'location' => $point ? [
47
+ 'type' => 'Point',
48
+ 'coordinates' => [$point->getLng(), $point->getLat()],
49
+ ] : null,
50
+ 'latitude' => $point ? $point->getLat() : null,
51
+ 'longitude' => $point ? $point->getLng() : null,
52
+ ];
53
+ }
54
+
55
+ public function jsonSerialize(): mixed
56
+ {
57
+ return $this->toArray();
58
+ }
59
+ }
@@ -203,6 +203,12 @@ Route::prefix(config('fleetops.api.routing.prefix', null))->namespace('Fleetbase
203
203
  $router->get('{id}', 'LabelController@getLabel');
204
204
  });
205
205
 
206
+ // orchestrator routes
207
+ $router->group(['prefix' => 'orchestrator'], function () use ($router) {
208
+ $router->post('run', 'OrchestrationController@run');
209
+ $router->post('commit', 'OrchestrationController@commit');
210
+ });
211
+
206
212
  // navigator routes
207
213
  $router->group(['prefix' => 'onboard'], function () use ($router) {
208
214
  $router->get('driver-onboard-settings/{companyId}', 'NavigatorController@getDriverOnboardSettings');
@@ -339,6 +345,7 @@ Route::prefix(config('fleetops.api.routing.prefix', null))->namespace('Fleetbase
339
345
  $router->get('next-activity/{id}', $controller('nextActivity'));
340
346
  $router->get('{id}/tracker', 'OrderController@trackerInfo');
341
347
  $router->get('{id}/eta', 'OrderController@waypointEtas');
348
+ $router->post('{id}/ping-driver', $controller('pingDriver'));
342
349
  $router->post('process-imports', $controller('importFromFiles'));
343
350
  $router->patch('route/{id}', $controller('editOrderRoute'));
344
351
  $router->patch('update-activity/{id}', $controller('updateActivity'));
@@ -546,6 +553,10 @@ Route::prefix(config('fleetops.api.routing.prefix', null))->namespace('Fleetbase
546
553
  $router->post('notification-settings', 'SettingController@saveNotificationSettings');
547
554
  $router->get('routing-settings', 'SettingController@getRoutingSettings');
548
555
  $router->post('routing-settings', 'SettingController@saveRoutingSettings');
556
+ $router->get('tracking-settings', 'SettingController@getTrackingSettings');
557
+ $router->post('tracking-settings', 'SettingController@saveTrackingSettings');
558
+ $router->get('admin-tracking-settings', 'SettingController@getAdminTrackingSettings');
559
+ $router->post('admin-tracking-settings', 'SettingController@saveAdminTrackingSettings');
549
560
  $router->get('map', 'SettingController@getMapSettings');
550
561
  $router->post('map', 'SettingController@saveMapSettings');
551
562
  $router->get('admin-map', 'SettingController@getAdminMapSettings');
@@ -562,6 +573,22 @@ Route::prefix(config('fleetops.api.routing.prefix', null))->namespace('Fleetbase
562
573
  ['prefix' => 'metrics'],
563
574
  function ($router) {
564
575
  $router->get('/', 'MetricsController@all');
576
+ $router->get('{slug}', 'MetricsController@show');
577
+ }
578
+ );
579
+ $router->group(
580
+ ['prefix' => 'analytics'],
581
+ function ($router) {
582
+ $router->get('operations-pulse', 'AnalyticsController@operationsPulse');
583
+ $router->get('revenue-trend', 'AnalyticsController@revenueTrend');
584
+ $router->get('orders-by-status', 'AnalyticsController@ordersByStatus');
585
+ $router->get('on-time-delivery', 'AnalyticsController@onTimeDelivery');
586
+ $router->get('top-drivers', 'AnalyticsController@topDrivers');
587
+ $router->get('fuel-efficiency', 'AnalyticsController@fuelEfficiency');
588
+ $router->get('issues-insights', 'AnalyticsController@issuesInsights');
589
+ $router->get('maintenance-overview', 'AnalyticsController@maintenanceOverview');
590
+ $router->get('geofence-violations', 'AnalyticsController@geofenceViolations');
591
+ $router->get('live-fleet', 'AnalyticsController@liveFleet');
565
592
  }
566
593
  );
567
594
  $router->group(
@@ -0,0 +1,41 @@
1
+ <?php
2
+
3
+ test('analytics routes are registered alongside metrics routes', function () {
4
+ $routes = file_get_contents(dirname(__DIR__) . '/src/routes.php');
5
+
6
+ expect($routes)->toContain("['prefix' => 'analytics']");
7
+ expect($routes)->toContain("'AnalyticsController@operationsPulse'");
8
+ expect($routes)->toContain("'AnalyticsController@revenueTrend'");
9
+ expect($routes)->toContain("'AnalyticsController@ordersByStatus'");
10
+ expect($routes)->toContain("'AnalyticsController@onTimeDelivery'");
11
+ expect($routes)->toContain("'AnalyticsController@topDrivers'");
12
+ expect($routes)->toContain("'AnalyticsController@fuelEfficiency'");
13
+ expect($routes)->toContain("'AnalyticsController@issuesInsights'");
14
+ expect($routes)->toContain("'AnalyticsController@maintenanceOverview'");
15
+ expect($routes)->toContain("'AnalyticsController@geofenceViolations'");
16
+ expect($routes)->toContain("'AnalyticsController@liveFleet'");
17
+ });
18
+
19
+ test('per-slug metrics endpoint is registered', function () {
20
+ $routes = file_get_contents(dirname(__DIR__) . '/src/routes.php');
21
+
22
+ expect($routes)->toContain("\$router->get('{slug}', 'MetricsController@show');");
23
+ });
24
+
25
+ test('legacy metrics endpoint is preserved for backward compat', function () {
26
+ $routes = file_get_contents(dirname(__DIR__) . '/src/routes.php');
27
+
28
+ expect($routes)->toContain("\$router->get('/', 'MetricsController@all');");
29
+ });
30
+
31
+ test('analytics controller has one method per widget', function () {
32
+ $controller = file_get_contents(dirname(__DIR__) . '/src/Http/Controllers/Internal/v1/AnalyticsController.php');
33
+
34
+ foreach ([
35
+ 'operationsPulse', 'revenueTrend', 'ordersByStatus', 'onTimeDelivery',
36
+ 'topDrivers', 'fuelEfficiency', 'issuesInsights', 'maintenanceOverview',
37
+ 'geofenceViolations', 'liveFleet',
38
+ ] as $method) {
39
+ expect($controller)->toContain("public function {$method}(");
40
+ }
41
+ });
@@ -0,0 +1,108 @@
1
+ <?php
2
+
3
+ use Fleetbase\FleetOps\Models\Order;
4
+ use Fleetbase\FleetOps\Models\Vehicle;
5
+ use Fleetbase\FleetOps\Orchestration\Engines\CapacityAllocationEngine;
6
+
7
+ function capacityTestOrder(string $publicId, float $weightKg, float $volumeLitres = 0, array $skills = []): Order
8
+ {
9
+ $payload = new class($weightKg, $volumeLitres) {
10
+ public $entities;
11
+
12
+ public function __construct(float $weightKg, float $volumeLitres)
13
+ {
14
+ $this->entities = collect([
15
+ new class($weightKg, $volumeLitres) {
16
+ public float $weight;
17
+ public string $weight_unit = 'kg';
18
+ public float $length;
19
+ public float $width = 1;
20
+ public float $height = 1;
21
+ public string $dimensions_unit = 'm';
22
+
23
+ public function __construct(float $weightKg, float $volumeLitres)
24
+ {
25
+ $this->weight = $weightKg;
26
+ $this->length = $volumeLitres / 1000;
27
+ }
28
+ },
29
+ ]);
30
+ }
31
+ };
32
+
33
+ $order = new Order();
34
+ $order->public_id = $publicId;
35
+ $order->required_skills = $skills;
36
+ $order->setRelation('payload', $payload);
37
+
38
+ return $order;
39
+ }
40
+
41
+ function capacityTestVehicle(string $publicId, float $weightKg, float $volumeM3 = 0, int $maxTasks = 0, array $skills = []): Vehicle
42
+ {
43
+ $vehicle = new Vehicle();
44
+ $vehicle->public_id = $publicId;
45
+ $vehicle->payload_capacity = $weightKg;
46
+ $vehicle->payload_capacity_volume = $volumeM3;
47
+ $vehicle->payload_capacity_pallets = 0;
48
+ $vehicle->payload_capacity_parcels = 100;
49
+ $vehicle->max_tasks = $maxTasks;
50
+ $vehicle->skills = $skills;
51
+ $vehicle->setRelation('driver', null);
52
+
53
+ return $vehicle;
54
+ }
55
+
56
+ test('capacity engine assigns orders without vehicle locations', function () {
57
+ $engine = new CapacityAllocationEngine();
58
+
59
+ $result = $engine->allocate(collect([
60
+ capacityTestOrder('order_light', 10, 250),
61
+ capacityTestOrder('order_medium', 20, 500),
62
+ ]), collect([
63
+ capacityTestVehicle('vehicle_van', 100, 2),
64
+ ]), [
65
+ 'balance_workload' => false,
66
+ ]);
67
+
68
+ expect($result['assignments'])->toHaveCount(2);
69
+ expect(array_column($result['assignments'], 'vehicle_id'))->toBe(['vehicle_van', 'vehicle_van']);
70
+ expect($result['unassigned'])->toBe([]);
71
+ expect($result['summary']['allocation_strategy'])->toBe('capacity_only');
72
+ });
73
+
74
+ test('capacity engine rejects over-capacity orders with useful reasons', function () {
75
+ $engine = new CapacityAllocationEngine();
76
+
77
+ $result = $engine->allocate(collect([
78
+ capacityTestOrder('order_heavy', 250),
79
+ ]), collect([
80
+ capacityTestVehicle('vehicle_small', 100),
81
+ ]));
82
+
83
+ expect($result['assignments'])->toBe([]);
84
+ expect($result['unassigned'])->toBe(['order_heavy']);
85
+ expect($result['summary']['unassigned_reasons'][0])->toMatchArray([
86
+ 'order_id' => 'order_heavy',
87
+ 'reason' => 'capacity_exceeded',
88
+ ]);
89
+ });
90
+
91
+ test('capacity engine respects skills max tasks and balanced workload', function () {
92
+ $engine = new CapacityAllocationEngine();
93
+
94
+ $result = $engine->allocate(collect([
95
+ capacityTestOrder('order_cold_1', 10, 0, ['cold_chain']),
96
+ capacityTestOrder('order_cold_2', 10, 0, ['cold_chain']),
97
+ capacityTestOrder('order_fragile', 10, 0, ['fragile']),
98
+ ]), collect([
99
+ capacityTestVehicle('vehicle_cold_a', 100, 0, 1, ['cold_chain']),
100
+ capacityTestVehicle('vehicle_cold_b', 100, 0, 1, ['cold_chain']),
101
+ ]), [
102
+ 'balance_workload' => true,
103
+ ]);
104
+
105
+ expect(array_column($result['assignments'], 'vehicle_id'))->toBe(['vehicle_cold_a', 'vehicle_cold_b']);
106
+ expect($result['unassigned'])->toBe(['order_fragile']);
107
+ expect($result['summary']['unassigned_reasons'][0]['reason'])->toBe('missing_required_skills');
108
+ });