@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,67 @@
1
+ <?php
2
+
3
+ namespace Fleetbase\FleetOps\Support\Analytics;
4
+
5
+ use Fleetbase\FleetOps\Models\Issue;
6
+ use Illuminate\Support\Carbon;
7
+
8
+ /**
9
+ * Issues breakdown: category pie, priority histogram, open vs resolved counts,
10
+ * and average resolution time (hours).
11
+ */
12
+ class IssuesInsights extends AbstractAnalytics
13
+ {
14
+ public function get(): array
15
+ {
16
+ $companyUuid = $this->company->uuid;
17
+ $start = $this->start ?? Carbon::now()->subDays(30)->toDateTime();
18
+ $end = $this->end ?? Carbon::now()->toDateTime();
19
+
20
+ $byCategory = Issue::where('company_uuid', $companyUuid)
21
+ ->whereBetween('created_at', [$start, $end])
22
+ ->selectRaw('category, COUNT(*) as total')
23
+ ->groupBy('category')
24
+ ->orderByRaw('total DESC')
25
+ ->get();
26
+
27
+ $byPriority = Issue::where('company_uuid', $companyUuid)
28
+ ->whereBetween('created_at', [$start, $end])
29
+ ->selectRaw('priority, COUNT(*) as total')
30
+ ->groupBy('priority')
31
+ ->pluck('total', 'priority');
32
+
33
+ $open = Issue::where('company_uuid', $companyUuid)
34
+ ->where('status', 'pending')
35
+ ->count();
36
+
37
+ $resolvedInWindow = Issue::where('company_uuid', $companyUuid)
38
+ ->whereNotNull('resolved_at')
39
+ ->whereBetween('resolved_at', [$start, $end])
40
+ ->get(['created_at', 'resolved_at']);
41
+
42
+ $totalHours = 0.0;
43
+ foreach ($resolvedInWindow as $issue) {
44
+ $totalHours += (Carbon::parse($issue->resolved_at)->getTimestamp()
45
+ - Carbon::parse($issue->created_at)->getTimestamp()) / 3600;
46
+ }
47
+
48
+ $avgResolutionHours = $resolvedInWindow->count() > 0
49
+ ? round($totalHours / $resolvedInWindow->count(), 1)
50
+ : null;
51
+
52
+ return [
53
+ 'by_category' => [
54
+ 'labels' => $byCategory->pluck('category')->map(fn ($c) => $c ?: 'Uncategorized')->all(),
55
+ 'data' => $byCategory->pluck('total')->map(fn ($v) => (int) $v)->all(),
56
+ ],
57
+ 'by_priority' => [
58
+ 'high' => (int) ($byPriority['high'] ?? 0),
59
+ 'medium' => (int) ($byPriority['medium'] ?? 0),
60
+ 'low' => (int) ($byPriority['low'] ?? 0),
61
+ ],
62
+ 'open' => $open,
63
+ 'resolved_this_period' => $resolvedInWindow->count(),
64
+ 'avg_resolution_hours' => $avgResolutionHours,
65
+ ];
66
+ }
67
+ }
@@ -0,0 +1,127 @@
1
+ <?php
2
+
3
+ namespace Fleetbase\FleetOps\Support\Analytics;
4
+
5
+ use Fleetbase\FleetOps\Models\Driver;
6
+ use Fleetbase\FleetOps\Models\Order;
7
+ use Fleetbase\FleetOps\Models\Vehicle;
8
+ use Fleetbase\FleetOps\Support\Metrics\OrdersInProgressMetric;
9
+
10
+ /**
11
+ * Real-time fleet snapshot used as the initial-state payload for Live Fleet Map.
12
+ * After this single GET the frontend subscribes to `company.{uuid}` and applies
13
+ * `driver.location_changed` / `order.*` events incrementally instead of polling.
14
+ *
15
+ * Reads from `Driver.location` and `Vehicle.location` directly — does NOT scan the
16
+ * high-frequency Position table.
17
+ */
18
+ class LiveFleet extends AbstractAnalytics
19
+ {
20
+ public function get(): array
21
+ {
22
+ $companyUuid = $this->company->uuid;
23
+
24
+ // Driver.name is a virtual attribute backed by users.name — eager-load the
25
+ // user so the accessor resolves cleanly without a per-record query.
26
+ $drivers = Driver::with(['user:uuid,name,avatar_uuid'])
27
+ ->where('drivers.company_uuid', $companyUuid)
28
+ ->whereNotNull('drivers.location')
29
+ ->where(function ($q) {
30
+ $q->where('drivers.online', true)->orWhereNotNull('drivers.current_job_uuid');
31
+ })
32
+ ->get();
33
+
34
+ // Vehicles tracked independently of drivers (telematics-connected rigs may
35
+ // report position without an active driver session).
36
+ $vehicles = Vehicle::where('company_uuid', $companyUuid)
37
+ ->whereNotNull('location')
38
+ ->get();
39
+
40
+ $activeOrderUuids = $drivers->pluck('current_job_uuid')->filter()->unique()->values();
41
+
42
+ $activeOrders = Order::whereIn('uuid', $activeOrderUuids)
43
+ ->where('company_uuid', $companyUuid)
44
+ ->whereIn('status', OrdersInProgressMetric::IN_PROGRESS_STATUSES)
45
+ ->get(['uuid', 'driver_assigned_uuid', 'status', 'tracking_number_uuid']);
46
+
47
+ return [
48
+ 'drivers' => $drivers->map(fn ($d) => $this->driverPayload($d))
49
+ ->filter(fn ($d) => $d['lat'] !== null && $d['lng'] !== null)
50
+ ->values()
51
+ ->all(),
52
+ 'vehicles' => $vehicles->map(fn ($v) => $this->vehiclePayload($v))
53
+ ->filter(fn ($v) => $v['lat'] !== null && $v['lng'] !== null)
54
+ ->values()
55
+ ->all(),
56
+ 'active_orders' => $activeOrders->map(fn ($o) => [
57
+ 'uuid' => $o->uuid,
58
+ 'driver_uuid' => $o->driver_assigned_uuid,
59
+ 'status' => $o->status,
60
+ ])->all(),
61
+ ];
62
+ }
63
+
64
+ private function driverPayload(Driver $d): array
65
+ {
66
+ [$lat, $lng] = $this->extractLatLng($d->location);
67
+
68
+ return [
69
+ 'uuid' => $d->uuid,
70
+ 'public_id' => $d->public_id,
71
+ 'name' => $d->name,
72
+ // avatar_url falls back to vehicle avatar then a default;
73
+ // vehicle_avatar is what the operational live-map renders.
74
+ 'avatar_url' => $d->avatar_url,
75
+ 'vehicle_avatar' => $d->vehicle_avatar,
76
+ 'online' => (bool) $d->online,
77
+ 'heading' => (float) ($d->heading ?? 0),
78
+ 'current_order_uuid' => $d->current_job_uuid,
79
+ 'lat' => $lat,
80
+ 'lng' => $lng,
81
+ 'updated_at' => $d->last_location_update_at,
82
+ ];
83
+ }
84
+
85
+ private function vehiclePayload(Vehicle $v): array
86
+ {
87
+ [$lat, $lng] = $this->extractLatLng($v->location);
88
+
89
+ return [
90
+ 'uuid' => $v->uuid,
91
+ 'public_id' => $v->public_id,
92
+ 'name' => $v->display_name,
93
+ 'plate_number' => $v->plate_number,
94
+ 'avatar_url' => $v->avatar_url,
95
+ 'photo_url' => $v->photo_url,
96
+ 'online' => (bool) ($v->online ?? false),
97
+ 'driver_name' => $v->driver_name,
98
+ 'heading' => (float) ($v->heading ?? 0),
99
+ 'lat' => $lat,
100
+ 'lng' => $lng,
101
+ ];
102
+ }
103
+
104
+ /**
105
+ * Pull [lat, lng] out of whatever shape the spatial accessor handed us.
106
+ * Supports Point objects, [lat, lng] arrays, and {lat, lng} hashes.
107
+ *
108
+ * @return array{0:float|null,1:float|null}
109
+ */
110
+ private function extractLatLng($location): array
111
+ {
112
+ if (is_object($location) && method_exists($location, 'getLat')) {
113
+ return [$location->getLat(), $location->getLng()];
114
+ }
115
+
116
+ if (is_array($location)) {
117
+ // Point::toArray usually returns ['lat' => ..., 'lng' => ...] but
118
+ // GeoJSON-shape `coordinates` is [lng, lat]; handle both.
119
+ return [
120
+ $location['lat'] ?? $location[1] ?? null,
121
+ $location['lng'] ?? $location[0] ?? null,
122
+ ];
123
+ }
124
+
125
+ return [null, null];
126
+ }
127
+ }
@@ -0,0 +1,70 @@
1
+ <?php
2
+
3
+ namespace Fleetbase\FleetOps\Support\Analytics;
4
+
5
+ use Fleetbase\FleetOps\Models\Maintenance;
6
+ use Illuminate\Support\Carbon;
7
+
8
+ /**
9
+ * Maintenance summary: overdue count, next-7-day schedule, in-progress count,
10
+ * month-to-date and year-to-date spend, plus a short list of upcoming items.
11
+ */
12
+ class MaintenanceOverview extends AbstractAnalytics
13
+ {
14
+ public function get(): array
15
+ {
16
+ $companyUuid = $this->company->uuid;
17
+ $currency = $this->companyCurrency();
18
+ $now = Carbon::now();
19
+
20
+ $overdue = Maintenance::where('company_uuid', $companyUuid)
21
+ ->where('scheduled_at', '<', $now)
22
+ ->where('status', '!=', 'completed')
23
+ ->where('status', '!=', 'canceled')
24
+ ->count();
25
+
26
+ $next7d = Maintenance::where('company_uuid', $companyUuid)
27
+ ->whereBetween('scheduled_at', [$now, $now->copy()->addDays(7)])
28
+ ->whereIn('status', ['scheduled', 'pending'])
29
+ ->count();
30
+
31
+ $inProgress = Maintenance::where('company_uuid', $companyUuid)
32
+ ->where('status', 'in_progress')
33
+ ->count();
34
+
35
+ $costThisMonth = (float) Maintenance::where('company_uuid', $companyUuid)
36
+ ->where('currency', $currency)
37
+ ->where('status', 'completed')
38
+ ->whereBetween('completed_at', [$now->copy()->startOfMonth(), $now])
39
+ ->sum('total_cost');
40
+
41
+ $costYtd = (float) Maintenance::where('company_uuid', $companyUuid)
42
+ ->where('currency', $currency)
43
+ ->where('status', 'completed')
44
+ ->whereBetween('completed_at', [$now->copy()->startOfYear(), $now])
45
+ ->sum('total_cost');
46
+
47
+ $upcoming = Maintenance::where('company_uuid', $companyUuid)
48
+ ->whereIn('status', ['scheduled', 'pending'])
49
+ ->whereNotNull('scheduled_at')
50
+ ->where('scheduled_at', '>=', $now)
51
+ ->orderBy('scheduled_at', 'asc')
52
+ ->limit(8)
53
+ ->get(['uuid', 'maintainable_uuid', 'maintainable_type', 'type', 'priority', 'scheduled_at']);
54
+
55
+ return [
56
+ 'overdue' => $overdue,
57
+ 'scheduled_next_7d' => $next7d,
58
+ 'in_progress' => $inProgress,
59
+ 'cost_this_month' => round($costThisMonth, 2),
60
+ 'cost_ytd' => round($costYtd, 2),
61
+ 'currency' => $currency,
62
+ 'upcoming' => $upcoming->map(fn ($m) => [
63
+ 'uuid' => $m->uuid,
64
+ 'type' => $m->type,
65
+ 'priority' => $m->priority,
66
+ 'scheduled_at' => $m->scheduled_at,
67
+ ])->all(),
68
+ ];
69
+ }
70
+ }
@@ -0,0 +1,75 @@
1
+ <?php
2
+
3
+ namespace Fleetbase\FleetOps\Support\Analytics;
4
+
5
+ use Fleetbase\FleetOps\Models\Order;
6
+ use Illuminate\Support\Carbon;
7
+
8
+ /**
9
+ * Percentage of completed orders that finished within the SLA window
10
+ * (scheduled_at + sla_minutes). Orders without a scheduled_at are excluded
11
+ * — you can't be on-time against an undefined target.
12
+ */
13
+ class OnTimeDelivery extends AbstractAnalytics
14
+ {
15
+ protected int $slaMinutes = 30;
16
+
17
+ public function slaMinutes(int $minutes): self
18
+ {
19
+ $this->slaMinutes = max(0, $minutes);
20
+
21
+ return $this;
22
+ }
23
+
24
+ public function get(): array
25
+ {
26
+ $start = $this->start ?? Carbon::now()->subDays(30)->toDateTime();
27
+ $end = $this->end ?? Carbon::now()->toDateTime();
28
+
29
+ [$onTime, $late, $total] = $this->bucketCounts($start, $end);
30
+
31
+ $duration = $end->getTimestamp() - $start->getTimestamp();
32
+ $compareEnd = clone $start;
33
+ $compareStart = (new \DateTime())->setTimestamp($start->getTimestamp() - $duration);
34
+ [, , $prevTotal] = $this->bucketCounts($compareStart, $compareEnd);
35
+ $prevOnTime = $this->bucketCounts($compareStart, $compareEnd)[0];
36
+ $prevPct = $prevTotal > 0 ? ($prevOnTime / $prevTotal) * 100 : 0;
37
+ $currentPct = $total > 0 ? ($onTime / $total) * 100 : 0;
38
+ $deltaPct = round($currentPct - $prevPct, 1);
39
+
40
+ return [
41
+ 'on_time_pct' => round($currentPct, 1),
42
+ 'on_time' => $onTime,
43
+ 'late' => $late,
44
+ 'total' => $total,
45
+ 'sla_minutes' => $this->slaMinutes,
46
+ 'delta_pct' => $deltaPct,
47
+ ];
48
+ }
49
+
50
+ /** @return array{0:int,1:int,2:int} [onTime, late, total] */
51
+ private function bucketCounts(\DateTimeInterface $start, \DateTimeInterface $end): array
52
+ {
53
+ $slaSeconds = $this->slaMinutes * 60;
54
+
55
+ $orders = Order::where('company_uuid', $this->company->uuid)
56
+ ->where('status', 'completed')
57
+ ->whereNotNull('scheduled_at')
58
+ ->whereNotNull('updated_at')
59
+ ->whereBetween('updated_at', [$start, $end])
60
+ ->selectRaw('TIMESTAMPDIFF(SECOND, scheduled_at, updated_at) as drift_seconds')
61
+ ->get();
62
+
63
+ $onTime = 0;
64
+ $late = 0;
65
+ foreach ($orders as $row) {
66
+ if ((int) $row->drift_seconds <= $slaSeconds) {
67
+ $onTime++;
68
+ } else {
69
+ $late++;
70
+ }
71
+ }
72
+
73
+ return [$onTime, $late, $onTime + $late];
74
+ }
75
+ }
@@ -0,0 +1,86 @@
1
+ <?php
2
+
3
+ namespace Fleetbase\FleetOps\Support\Analytics;
4
+
5
+ use Fleetbase\FleetOps\Models\Driver;
6
+ use Fleetbase\FleetOps\Models\Issue;
7
+ use Fleetbase\FleetOps\Models\Order;
8
+ use Fleetbase\FleetOps\Models\Vehicle;
9
+ use Fleetbase\FleetOps\Support\Metrics\OrdersInProgressMetric;
10
+ use Illuminate\Support\Carbon;
11
+
12
+ /**
13
+ * Real-time operational snapshot. Returns a small map of tile data + deltas
14
+ * vs the same window 24 hours ago for the rate-of-change indicators.
15
+ */
16
+ class OperationsPulse extends AbstractAnalytics
17
+ {
18
+ public function get(): array
19
+ {
20
+ $companyUuid = $this->company->uuid;
21
+
22
+ $activeOrders = Order::where('company_uuid', $companyUuid)
23
+ ->whereIn('status', OrdersInProgressMetric::IN_PROGRESS_STATUSES)
24
+ ->count();
25
+
26
+ $driversOnline = Driver::where('company_uuid', $companyUuid)
27
+ ->where('online', true)
28
+ ->whereNotNull('current_job_uuid')
29
+ ->count();
30
+
31
+ $totalDrivers = Driver::where('company_uuid', $companyUuid)->count();
32
+
33
+ $vehiclesDeployed = Vehicle::where('company_uuid', $companyUuid)
34
+ ->whereHas('driver', fn ($q) => $q->whereNotNull('current_job_uuid'))
35
+ ->count();
36
+
37
+ $totalVehicles = Vehicle::where('company_uuid', $companyUuid)->count();
38
+
39
+ $issuesOpen = Issue::where('company_uuid', $companyUuid)
40
+ ->where('status', 'pending')
41
+ ->count();
42
+
43
+ $todayStart = Carbon::today();
44
+ $yesterdayStart = Carbon::yesterday();
45
+ $todaySoFar = Carbon::now();
46
+ $yesterdaySamePoint = $yesterdayStart->copy()->setTime($todaySoFar->hour, $todaySoFar->minute);
47
+
48
+ $completedToday = Order::where('company_uuid', $companyUuid)
49
+ ->where('status', 'completed')
50
+ ->whereBetween('updated_at', [$todayStart, $todaySoFar])
51
+ ->count();
52
+
53
+ $completedYesterdayToTime = Order::where('company_uuid', $companyUuid)
54
+ ->where('status', 'completed')
55
+ ->whereBetween('updated_at', [$yesterdayStart, $yesterdaySamePoint])
56
+ ->count();
57
+
58
+ return [
59
+ 'active_orders' => ['value' => $activeOrders, 'delta_pct' => null],
60
+ 'completed_today' => [
61
+ 'value' => $completedToday,
62
+ 'delta_pct' => $this->deltaPct($completedToday, $completedYesterdayToTime),
63
+ ],
64
+ 'drivers_online' => [
65
+ 'value' => $driversOnline,
66
+ 'of' => $totalDrivers,
67
+ 'pct_of_max' => $totalDrivers > 0 ? round(($driversOnline / $totalDrivers) * 100, 1) : 0.0,
68
+ ],
69
+ 'vehicles_deployed' => [
70
+ 'value' => $vehiclesDeployed,
71
+ 'of' => $totalVehicles,
72
+ 'pct_of_max' => $totalVehicles > 0 ? round(($vehiclesDeployed / $totalVehicles) * 100, 1) : 0.0,
73
+ ],
74
+ 'issues_open' => ['value' => $issuesOpen, 'delta_pct' => null],
75
+ ];
76
+ }
77
+
78
+ private function deltaPct(int $current, int $previous): ?float
79
+ {
80
+ if ($previous === 0) {
81
+ return $current > 0 ? 100.0 : null;
82
+ }
83
+
84
+ return round((($current - $previous) / $previous) * 100, 1);
85
+ }
86
+ }
@@ -0,0 +1,64 @@
1
+ <?php
2
+
3
+ namespace Fleetbase\FleetOps\Support\Analytics;
4
+
5
+ use Fleetbase\FleetOps\Models\Order;
6
+ use Illuminate\Support\Carbon;
7
+
8
+ /**
9
+ * Daily count of orders bucketed by status. One stacked bar per day, with
10
+ * a fixed status palette so the colour coding stays consistent over time.
11
+ */
12
+ class OrdersByStatus extends AbstractAnalytics
13
+ {
14
+ /** Status → display label + colour for stacked bars. */
15
+ private const STATUS_PALETTE = [
16
+ 'completed' => ['label' => 'Completed', 'color' => '#22c55e'],
17
+ 'in_progress' => ['label' => 'In Progress', 'color' => '#3485e2'],
18
+ 'dispatched' => ['label' => 'Dispatched', 'color' => '#8b5cf6'],
19
+ 'canceled' => ['label' => 'Canceled', 'color' => '#ef4444'],
20
+ 'failed' => ['label' => 'Failed', 'color' => '#f59e0b'],
21
+ ];
22
+
23
+ public function get(): array
24
+ {
25
+ $start = $this->start ?? Carbon::now()->subDays(14)->toDateTime();
26
+ $end = $this->end ?? Carbon::now()->toDateTime();
27
+
28
+ $rows = Order::where('company_uuid', $this->company->uuid)
29
+ ->whereBetween('created_at', [$start, $end])
30
+ ->whereIn('status', array_keys(self::STATUS_PALETTE))
31
+ ->selectRaw('DATE(created_at) as bucket, status, COUNT(*) as total')
32
+ ->groupBy('bucket', 'status')
33
+ ->orderBy('bucket')
34
+ ->get();
35
+
36
+ $cursor = Carbon::instance($start)->startOfDay();
37
+ $endDay = Carbon::instance($end)->startOfDay();
38
+ $labels = [];
39
+ $byStatus = array_fill_keys(array_keys(self::STATUS_PALETTE), []);
40
+
41
+ while ($cursor <= $endDay) {
42
+ $labels[] = $cursor->format('M j');
43
+ foreach (array_keys(self::STATUS_PALETTE) as $status) {
44
+ $match = $rows->first(fn ($r) => $r->bucket === $cursor->format('Y-m-d') && $r->status === $status);
45
+ $byStatus[$status][] = $match ? (int) $match->total : 0;
46
+ }
47
+ $cursor->addDay();
48
+ }
49
+
50
+ $datasets = [];
51
+ foreach (self::STATUS_PALETTE as $status => $cfg) {
52
+ $datasets[] = [
53
+ 'label' => $cfg['label'],
54
+ 'data' => $byStatus[$status],
55
+ 'backgroundColor' => $cfg['color'],
56
+ ];
57
+ }
58
+
59
+ return [
60
+ 'labels' => $labels,
61
+ 'datasets' => $datasets,
62
+ ];
63
+ }
64
+ }
@@ -0,0 +1,79 @@
1
+ <?php
2
+
3
+ namespace Fleetbase\FleetOps\Support\Analytics;
4
+
5
+ use Fleetbase\Models\Transaction;
6
+ use Illuminate\Support\Carbon;
7
+ use Illuminate\Support\Facades\DB;
8
+
9
+ /**
10
+ * Time-series of revenue (sum of transactions) per bucket, filtered to the
11
+ * company's primary currency. Returns Chart.js-shaped { labels, datasets, summary }.
12
+ */
13
+ class RevenueTrend extends AbstractAnalytics
14
+ {
15
+ protected string $groupBy = 'day';
16
+
17
+ public function groupBy(string $unit): self
18
+ {
19
+ $this->groupBy = in_array($unit, ['day', 'week', 'month'], true) ? $unit : 'day';
20
+
21
+ return $this;
22
+ }
23
+
24
+ public function get(): array
25
+ {
26
+ $currency = $this->companyCurrency();
27
+ $start = $this->start ?? Carbon::now()->subDays(30)->toDateTime();
28
+ $end = $this->end ?? Carbon::now()->toDateTime();
29
+
30
+ $format = match ($this->groupBy) {
31
+ 'week' => '%x-W%v',
32
+ 'month' => '%Y-%m',
33
+ default => '%Y-%m-%d',
34
+ };
35
+
36
+ $rows = Transaction::where('company_uuid', $this->company->uuid)
37
+ ->where('currency', $currency)
38
+ ->whereBetween('created_at', [$start, $end])
39
+ ->selectRaw('DATE_FORMAT(created_at, ?) as bucket, SUM(amount) as total', [$format])
40
+ ->groupBy('bucket')
41
+ ->orderBy('bucket')
42
+ ->get();
43
+
44
+ $labels = $rows->pluck('bucket')->all();
45
+ $data = $rows->pluck('total')->map(fn ($v) => (float) $v)->all();
46
+ $total = array_sum($data);
47
+
48
+ $duration = $end->getTimestamp() - $start->getTimestamp();
49
+ $compareStart = (clone $start);
50
+ $compareStart = (new \DateTime())->setTimestamp($start->getTimestamp() - $duration);
51
+ $previousTotal = (float) Transaction::where('company_uuid', $this->company->uuid)
52
+ ->where('currency', $currency)
53
+ ->whereBetween('created_at', [$compareStart, $start])
54
+ ->sum('amount');
55
+
56
+ $deltaPct = $previousTotal > 0
57
+ ? round((($total - $previousTotal) / $previousTotal) * 100, 1)
58
+ : ($total > 0 ? 100.0 : 0.0);
59
+
60
+ return [
61
+ 'labels' => $labels,
62
+ 'datasets' => [
63
+ [
64
+ 'label' => 'Revenue',
65
+ 'data' => $data,
66
+ 'borderColor' => '#3485e2',
67
+ 'backgroundColor' => 'rgba(52, 133, 226, 0.1)',
68
+ 'fill' => true,
69
+ 'tension' => 0.3,
70
+ ],
71
+ ],
72
+ 'summary' => [
73
+ 'total' => round($total, 2),
74
+ 'currency' => $currency,
75
+ 'delta_pct' => $deltaPct,
76
+ ],
77
+ ];
78
+ }
79
+ }
@@ -0,0 +1,92 @@
1
+ <?php
2
+
3
+ namespace Fleetbase\FleetOps\Support\Analytics;
4
+
5
+ use Fleetbase\FleetOps\Models\Driver;
6
+ use Fleetbase\FleetOps\Models\Order;
7
+ use Illuminate\Support\Carbon;
8
+
9
+ /**
10
+ * Ranked driver leaderboard. Single GROUP BY join on orders → drivers so this
11
+ * scales without N+1 even on large fleets.
12
+ */
13
+ class TopDrivers extends AbstractAnalytics
14
+ {
15
+ protected int $limit = 10;
16
+ protected string $sortBy = 'orders_completed';
17
+
18
+ public function limit(int $limit): self
19
+ {
20
+ $this->limit = max(1, min(50, $limit));
21
+
22
+ return $this;
23
+ }
24
+
25
+ public function sortBy(string $key): self
26
+ {
27
+ $this->sortBy = in_array($key, ['orders_completed', 'distance', 'on_time'], true)
28
+ ? $key
29
+ : 'orders_completed';
30
+
31
+ return $this;
32
+ }
33
+
34
+ public function get(): array
35
+ {
36
+ $start = $this->start ?? Carbon::now()->subDays(30)->toDateTime();
37
+ $end = $this->end ?? Carbon::now()->toDateTime();
38
+
39
+ $sortColumn = match ($this->sortBy) {
40
+ 'distance' => 'distance_m',
41
+ 'on_time' => 'on_time_pct',
42
+ default => 'orders_completed',
43
+ };
44
+
45
+ // `drivers.name` and `drivers.photo_uuid` aren't real columns — they're
46
+ // virtual attributes that resolve via the related users row. Join users
47
+ // explicitly so we can SELECT them at the SQL level.
48
+ $rows = Order::where('orders.company_uuid', $this->company->uuid)
49
+ ->where('orders.status', 'completed')
50
+ ->whereNotNull('orders.driver_assigned_uuid')
51
+ ->whereBetween('orders.updated_at', [$start, $end])
52
+ ->join('drivers', 'drivers.uuid', '=', 'orders.driver_assigned_uuid')
53
+ ->join('users', 'users.uuid', '=', 'drivers.user_uuid')
54
+ ->groupBy('drivers.uuid', 'users.name', 'users.avatar_uuid')
55
+ ->selectRaw('
56
+ drivers.uuid as driver_uuid,
57
+ users.name as name,
58
+ users.avatar_uuid as avatar_uuid,
59
+ COUNT(orders.uuid) as orders_completed,
60
+ COALESCE(SUM(orders.distance), 0) as distance_m,
61
+ SUM(CASE
62
+ WHEN orders.scheduled_at IS NOT NULL
63
+ AND TIMESTAMPDIFF(SECOND, orders.scheduled_at, orders.updated_at) <= 1800
64
+ THEN 1 ELSE 0 END) as on_time_count,
65
+ SUM(CASE WHEN orders.scheduled_at IS NOT NULL THEN 1 ELSE 0 END) as scheduled_count
66
+ ')
67
+ ->orderByRaw("{$sortColumn} DESC")
68
+ ->limit($this->limit)
69
+ ->get();
70
+
71
+ $result = $rows->map(function ($r) {
72
+ $scheduled = (int) $r->scheduled_count;
73
+ $onTimePct = $scheduled > 0
74
+ ? round(((int) $r->on_time_count / $scheduled) * 100, 1)
75
+ : null;
76
+
77
+ return [
78
+ 'driver_uuid' => $r->driver_uuid,
79
+ 'name' => $r->name,
80
+ 'avatar_uuid' => $r->avatar_uuid,
81
+ 'orders_completed' => (int) $r->orders_completed,
82
+ 'distance_m' => (float) $r->distance_m,
83
+ 'on_time_pct' => $onTimePct,
84
+ ];
85
+ })->all();
86
+
87
+ return [
88
+ 'rows' => $result,
89
+ 'sort_by' => $this->sortBy,
90
+ ];
91
+ }
92
+ }