@fleetbase/fleetops-engine 0.6.48 → 0.6.50

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (266) hide show
  1. package/addon/components/customer/create-order-form.hbs +2 -2
  2. package/addon/components/customer/form.hbs +2 -2
  3. package/addon/components/customer/order-form.hbs +2 -2
  4. package/addon/components/customer/orders.hbs +2 -2
  5. package/addon/components/display-place.hbs +2 -2
  6. package/addon/components/driver/details.hbs +5 -1
  7. package/addon/components/driver/form.hbs +11 -1
  8. package/addon/components/fleet-listing-panel.hbs +2 -2
  9. package/addon/components/fleet-listing-panel.js +10 -0
  10. package/addon/components/map/google-live-map.js +18 -75
  11. package/addon/components/map/leaflet-live-map.hbs +187 -37
  12. package/addon/components/map/leaflet-live-map.js +184 -20
  13. package/addon/components/modals/order-change-destination.hbs +46 -0
  14. package/addon/components/modals/order-change-destination.js +106 -0
  15. package/addon/components/modals/stop-activity.hbs +28 -0
  16. package/addon/components/modals/stop-activity.js +52 -0
  17. package/addon/components/modals/update-order-activity.hbs +45 -3
  18. package/addon/components/modals/update-order-activity.js +143 -5
  19. package/addon/components/orchestrator/phase-builder.hbs +21 -0
  20. package/addon/components/orchestrator/phase-builder.js +13 -0
  21. package/addon/components/orchestrator/resource-panel.hbs +0 -10
  22. package/addon/components/orchestrator/resource-panel.js +2 -4
  23. package/addon/components/orchestrator-workbench.js +1 -0
  24. package/addon/components/order/activity-list.hbs +2 -2
  25. package/addon/components/order/activity-timeline.hbs +31 -29
  26. package/addon/components/order/details/activity.hbs +3 -3
  27. package/addon/components/order/details/activity.js +11 -0
  28. package/addon/components/order/details/documents.js +2 -0
  29. package/addon/components/order/details/purchase-rate.hbs +2 -2
  30. package/addon/components/order/details/route.hbs +11 -3
  31. package/addon/components/order/details/route.js +91 -14
  32. package/addon/components/order/details/tracking.hbs +101 -72
  33. package/addon/components/order/details/tracking.js +149 -2
  34. package/addon/components/order/form/details.hbs +3 -3
  35. package/addon/components/order/form/documents.js +11 -4
  36. package/addon/components/order/form/route.hbs +108 -104
  37. package/addon/components/order/form/route.js +37 -1
  38. package/addon/components/order/form/service-rate.hbs +2 -2
  39. package/addon/components/order/form.hbs +2 -2
  40. package/addon/components/order-config-manager/activity-flow.hbs +4 -1
  41. package/addon/components/order-config-manager/activity-flow.js +178 -12
  42. package/addon/components/place/address.js +1 -1
  43. package/addon/components/route-list.hbs +17 -17
  44. package/addon/components/route-list.js +42 -10
  45. package/addon/components/service-rate/details.hbs +41 -0
  46. package/addon/components/service-rate/form.hbs +118 -0
  47. package/addon/components/service-rate/form.js +55 -0
  48. package/addon/components/vehicle/form.js +1 -1
  49. package/addon/components/vehicle/pill.hbs +1 -1
  50. package/addon/components/widget/fleet-ops-key-metrics.hbs +19 -17
  51. package/addon/components/widget/fleet-ops-key-metrics.js +26 -0
  52. package/addon/components/widget/fuel-efficiency.hbs +26 -0
  53. package/addon/components/widget/fuel-efficiency.js +43 -0
  54. package/addon/components/widget/geofence-violations.hbs +44 -0
  55. package/addon/components/widget/geofence-violations.js +55 -0
  56. package/addon/components/widget/issues-insights.hbs +43 -0
  57. package/addon/components/widget/issues-insights.js +62 -0
  58. package/addon/components/widget/kpi-active-orders.hbs +10 -0
  59. package/addon/components/widget/kpi-active-orders.js +5 -0
  60. package/addon/components/widget/kpi-aov.hbs +10 -0
  61. package/addon/components/widget/kpi-aov.js +5 -0
  62. package/addon/components/widget/kpi-distance.hbs +10 -0
  63. package/addon/components/widget/kpi-distance.js +5 -0
  64. package/addon/components/widget/kpi-drivers-online.hbs +10 -0
  65. package/addon/components/widget/kpi-drivers-online.js +5 -0
  66. package/addon/components/widget/kpi-earnings.hbs +10 -0
  67. package/addon/components/widget/kpi-earnings.js +5 -0
  68. package/addon/components/widget/kpi-open-issues.hbs +11 -0
  69. package/addon/components/widget/kpi-open-issues.js +5 -0
  70. package/addon/components/widget/kpi-tile.hbs +59 -0
  71. package/addon/components/widget/kpi-tile.js +168 -0
  72. package/addon/components/widget/live-fleet.hbs +122 -0
  73. package/addon/components/widget/live-fleet.js +365 -0
  74. package/addon/components/widget/maintenance-overview.hbs +45 -0
  75. package/addon/components/widget/maintenance-overview.js +40 -0
  76. package/addon/components/widget/on-time-delivery.hbs +27 -0
  77. package/addon/components/widget/on-time-delivery.js +73 -0
  78. package/addon/components/widget/operations-pulse.hbs +51 -0
  79. package/addon/components/widget/operations-pulse.js +173 -0
  80. package/addon/components/widget/orders-by-status.hbs +26 -0
  81. package/addon/components/widget/orders-by-status.js +54 -0
  82. package/addon/components/widget/revenue-trend.hbs +34 -0
  83. package/addon/components/widget/revenue-trend.js +67 -0
  84. package/addon/components/widget/top-drivers.hbs +44 -0
  85. package/addon/components/widget/top-drivers.js +54 -0
  86. package/addon/controllers/management/vehicles/index/new.js +1 -1
  87. package/addon/controllers/operations/orders/index/details.js +4 -4
  88. package/addon/controllers/operations/orders/index.js +14 -0
  89. package/addon/controllers/settings/routing.js +26 -0
  90. package/addon/extension.js +172 -3
  91. package/addon/helpers/place-address-html.js +2 -2
  92. package/addon/routes/operations/service-rates/index.js +1 -1
  93. package/addon/services/geofence.js +163 -19
  94. package/addon/services/leaflet-contextmenu-manager.js +27 -0
  95. package/addon/services/map-adapter/google.js +13 -2
  96. package/addon/services/vehicle-actions.js +1 -1
  97. package/addon/styles/fleetops-engine.css +205 -88
  98. package/addon/templates/maintenance/equipment/index.hbs +1 -1
  99. package/addon/templates/maintenance/parts/index.hbs +1 -1
  100. package/addon/templates/maintenance/schedules/index.hbs +1 -1
  101. package/addon/templates/management/vehicles/index.hbs +1 -1
  102. package/addon/templates/settings/avatars.hbs +1 -1
  103. package/addon/templates/settings/custom-fields.hbs +1 -1
  104. package/addon/templates/settings/map.hbs +1 -1
  105. package/addon/templates/settings/navigator-app.hbs +1 -1
  106. package/addon/templates/settings/notifications.hbs +1 -1
  107. package/addon/templates/settings/orchestrator.hbs +1 -1
  108. package/addon/templates/settings/payments/index.hbs +1 -1
  109. package/addon/templates/settings/payments/onboard.hbs +1 -1
  110. package/addon/templates/settings/routing.hbs +22 -2
  111. package/addon/templates/settings/scheduling.hbs +1 -1
  112. package/addon/utils/fleet-ops-options.js +2 -1
  113. package/addon/utils/live-map-card-content.js +171 -0
  114. package/addon/utils/normalize-order-config-flow.js +41 -0
  115. package/addon/utils/place-address-html.js +3 -2
  116. package/addon/utils/route-visualization.js +54 -7
  117. package/app/components/modals/order-change-destination.js +1 -0
  118. package/app/components/modals/stop-activity.js +1 -0
  119. package/app/components/widget/fuel-efficiency.js +1 -0
  120. package/app/components/widget/geofence-violations.js +1 -0
  121. package/app/components/widget/issues-insights.js +1 -0
  122. package/app/components/widget/kpi-active-orders.js +1 -0
  123. package/app/components/widget/kpi-aov.js +1 -0
  124. package/app/components/widget/kpi-distance.js +1 -0
  125. package/app/components/widget/kpi-drivers-online.js +1 -0
  126. package/app/components/widget/kpi-earnings.js +1 -0
  127. package/app/components/widget/kpi-open-issues.js +1 -0
  128. package/app/components/widget/kpi-tile.js +1 -0
  129. package/app/components/widget/live-fleet.js +1 -0
  130. package/app/components/widget/maintenance-overview.js +1 -0
  131. package/app/components/widget/on-time-delivery.js +1 -0
  132. package/app/components/widget/operations-pulse.js +1 -0
  133. package/app/components/widget/orders-by-status.js +1 -0
  134. package/app/components/widget/revenue-trend.js +1 -0
  135. package/app/components/widget/top-drivers.js +1 -0
  136. package/app/utils/live-map-card-content.js +1 -0
  137. package/composer.json +1 -1
  138. package/extension.json +1 -1
  139. package/package.json +3 -3
  140. package/pnpm-workspace.yaml +1 -0
  141. package/server/config/telematics.php +111 -1
  142. package/server/migrations/2026_05_18_000001_add_license_expiry_to_drivers_table.php +25 -0
  143. package/server/migrations/2026_05_21_000001_add_geographic_rules_to_service_rate_fees_table.php +37 -0
  144. package/server/migrations/2026_05_25_000001_add_endpoint_tracking_refs_to_payloads_table.php +50 -0
  145. package/server/migrations/2026_05_27_000001_default_driver_vehicle_statuses_to_available.php +48 -0
  146. package/server/resources/views/labels/default.php +57 -49
  147. package/server/resources/views/labels/waypoint-label.php +27 -20
  148. package/server/seeders/Testing/Concerns/SeedsTestingData.php +183 -0
  149. package/server/seeders/Testing/ConnectivitySeeder.php +213 -0
  150. package/server/seeders/Testing/FleetSeeder.php +341 -0
  151. package/server/seeders/Testing/MaintenanceSeeder.php +238 -0
  152. package/server/seeders/Testing/NetworkSeeder.php +250 -0
  153. package/server/seeders/Testing/OrdersSeeder.php +198 -0
  154. package/server/seeders/Testing/TestingSeeder.php +40 -0
  155. package/server/seeders/{VroomOrchestrationSeeder.php → Testing/VroomOrchestrationSeeder.php} +29 -22
  156. package/server/src/Console/Commands/AuditCustomerUserConflicts.php +75 -0
  157. package/server/src/Console/Commands/ProcessOperationalAlerts.php +279 -0
  158. package/server/src/Exceptions/CustomerUserConflictException.php +13 -0
  159. package/server/src/Http/Controllers/Api/v1/ContactController.php +17 -2
  160. package/server/src/Http/Controllers/Api/v1/CustomerController.php +876 -0
  161. package/server/src/Http/Controllers/Api/v1/DriverController.php +1 -1
  162. package/server/src/Http/Controllers/Api/v1/OrderConfigController.php +54 -0
  163. package/server/src/Http/Controllers/Api/v1/OrderController.php +98 -70
  164. package/server/src/Http/Controllers/Api/v1/ServiceAreaController.php +2 -2
  165. package/server/src/Http/Controllers/Api/v1/VehicleController.php +20 -3
  166. package/server/src/Http/Controllers/Internal/v1/AnalyticsController.php +119 -0
  167. package/server/src/Http/Controllers/Internal/v1/ContactController.php +19 -0
  168. package/server/src/Http/Controllers/Internal/v1/LiveController.php +101 -66
  169. package/server/src/Http/Controllers/Internal/v1/MetricsController.php +82 -3
  170. package/server/src/Http/Controllers/Internal/v1/OrderController.php +441 -32
  171. package/server/src/Http/Controllers/Internal/v1/ServiceQuoteController.php +1 -1
  172. package/server/src/Http/Controllers/Internal/v1/SettingController.php +46 -1
  173. package/server/src/Http/Controllers/Internal/v1/VehicleController.php +54 -1
  174. package/server/src/Http/Middleware/AuthenticateCustomerToken.php +34 -0
  175. package/server/src/Http/Requests/CreateCustomerOrderRequest.php +63 -0
  176. package/server/src/Http/Requests/CreateCustomerRequest.php +48 -0
  177. package/server/src/Http/Requests/CreateDriverRequest.php +1 -1
  178. package/server/src/Http/Requests/CreateVehicleRequest.php +25 -1
  179. package/server/src/Http/Requests/Internal/CreateDriverRequest.php +1 -1
  180. package/server/src/Http/Requests/VerifyCreateCustomerRequest.php +29 -0
  181. package/server/src/Http/Resources/Internal/v1/OrderConfig.php +41 -0
  182. package/server/src/Http/Resources/v1/Customer.php +79 -0
  183. package/server/src/Http/Resources/v1/Driver.php +2 -0
  184. package/server/src/Http/Resources/v1/Fleet.php +4 -0
  185. package/server/src/Http/Resources/v1/Index/Driver.php +48 -0
  186. package/server/src/Http/Resources/v1/Index/Vehicle.php +74 -1
  187. package/server/src/Http/Resources/v1/OrderConfig.php +74 -0
  188. package/server/src/Http/Resources/v1/Payload.php +23 -20
  189. package/server/src/Http/Resources/v1/ServiceArea.php +33 -19
  190. package/server/src/Http/Resources/v1/ServiceRateFee.php +7 -0
  191. package/server/src/Listeners/HandleOrderDispatched.php +2 -2
  192. package/server/src/Models/Contact.php +92 -15
  193. package/server/src/Models/Customer.php +64 -0
  194. package/server/src/Models/Driver.php +14 -3
  195. package/server/src/Models/Entity.php +2 -2
  196. package/server/src/Models/Order.php +29 -4
  197. package/server/src/Models/OrderConfig.php +14 -11
  198. package/server/src/Models/Payload.php +17 -1
  199. package/server/src/Models/ServiceRate.php +273 -1
  200. package/server/src/Models/ServiceRateFee.php +39 -9
  201. package/server/src/Models/TrackingNumber.php +8 -2
  202. package/server/src/Models/Vehicle.php +18 -2
  203. package/server/src/Models/Waypoint.php +2 -2
  204. package/server/src/Notifications/LateDeparture.php +48 -0
  205. package/server/src/Notifications/ProlongedStoppage.php +48 -0
  206. package/server/src/Notifications/RouteDeviation.php +48 -0
  207. package/server/src/Observers/ContactObserver.php +2 -0
  208. package/server/src/Observers/ServiceRateObserver.php +6 -6
  209. package/server/src/Orchestration/Engines/VroomOrchestrationEngine.php +29 -2
  210. package/server/src/Providers/FleetOpsServiceProvider.php +6 -0
  211. package/server/src/Providers/NotificationServiceProvider.php +3 -0
  212. package/server/src/Support/Analytics/AbstractAnalytics.php +77 -0
  213. package/server/src/Support/Analytics/FuelEfficiency.php +83 -0
  214. package/server/src/Support/Analytics/GeofenceViolations.php +63 -0
  215. package/server/src/Support/Analytics/IssuesInsights.php +67 -0
  216. package/server/src/Support/Analytics/LiveFleet.php +127 -0
  217. package/server/src/Support/Analytics/MaintenanceOverview.php +70 -0
  218. package/server/src/Support/Analytics/OnTimeDelivery.php +75 -0
  219. package/server/src/Support/Analytics/OperationsPulse.php +86 -0
  220. package/server/src/Support/Analytics/OrdersByStatus.php +64 -0
  221. package/server/src/Support/Analytics/RevenueTrend.php +78 -0
  222. package/server/src/Support/Analytics/TopDrivers.php +92 -0
  223. package/server/src/Support/CustomerAuth.php +98 -0
  224. package/server/src/Support/LabelPdf.php +52 -0
  225. package/server/src/Support/Metrics/AbstractMetric.php +170 -0
  226. package/server/src/Support/Metrics/ActiveLiveOrdersMetric.php +28 -0
  227. package/server/src/Support/Metrics/AvgOrderValueMetric.php +53 -0
  228. package/server/src/Support/Metrics/DriversOnlineMetric.php +30 -0
  229. package/server/src/Support/Metrics/EarningsMetric.php +30 -0
  230. package/server/src/Support/Metrics/FuelCostsMetric.php +30 -0
  231. package/server/src/Support/Metrics/MoneyMetric.php +30 -0
  232. package/server/src/Support/Metrics/OpenIssuesMetric.php +35 -0
  233. package/server/src/Support/Metrics/OrdersCanceledMetric.php +35 -0
  234. package/server/src/Support/Metrics/OrdersCompletedMetric.php +35 -0
  235. package/server/src/Support/Metrics/OrdersInProgressMetric.php +51 -0
  236. package/server/src/Support/Metrics/OrdersScheduledMetric.php +37 -0
  237. package/server/src/Support/Metrics/Registry.php +46 -0
  238. package/server/src/Support/Metrics/ResolvedIssuesMetric.php +35 -0
  239. package/server/src/Support/Metrics/TotalCustomersMetric.php +29 -0
  240. package/server/src/Support/Metrics/TotalDistanceTraveledMetric.php +35 -0
  241. package/server/src/Support/Metrics/TotalDriversMetric.php +28 -0
  242. package/server/src/Support/Metrics/TotalTimeTraveledMetric.php +35 -0
  243. package/server/src/Support/Metrics.php +46 -249
  244. package/server/src/Support/ResolvesOrderServiceStops.php +512 -0
  245. package/server/src/Support/Telematics/Providers/AfaqyProvider.php +290 -0
  246. package/server/src/Support/Telematics/Providers/SafeeProvider.php +333 -0
  247. package/server/src/Tracking/TrackingContextBuilder.php +41 -38
  248. package/server/src/Tracking/TrackingIntelligenceService.php +109 -9
  249. package/server/src/Tracking/TrackingStop.php +13 -11
  250. package/server/src/routes.php +50 -0
  251. package/server/tests/AnalyticsRoutesTest.php +41 -0
  252. package/server/tests/CapacityAllocationEngineTest.php +10 -10
  253. package/server/tests/CustomerContactIdentitySafetyTest.php +54 -0
  254. package/server/tests/CustomerEndpointTest.php +199 -0
  255. package/server/tests/DriverVehicleStatusDefaultTest.php +39 -0
  256. package/server/tests/LabelPdfRenderingTest.php +23 -0
  257. package/server/tests/LiveControllerViewportTest.php +60 -0
  258. package/server/tests/MetricsRegistryTest.php +84 -0
  259. package/server/tests/OrchestrationPayloadBuilderTest.php +2 -2
  260. package/server/tests/OrderConfigEndpointTest.php +86 -0
  261. package/server/tests/OrderFileAttachmentTest.php +20 -0
  262. package/server/tests/ServiceAreaZoneApiShapeTest.php +34 -0
  263. package/server/tests/VehicleDriverAssignmentTest.php +18 -0
  264. package/server/tests/VroomOrchestrationEngineTest.php +91 -0
  265. package/translations/en-us.yaml +38 -0
  266. package/server/migrations/2026_05_13_000001_repair_customer_contact_user_types.php +0 -86
@@ -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,78 @@
1
+ <?php
2
+
3
+ namespace Fleetbase\FleetOps\Support\Analytics;
4
+
5
+ use Fleetbase\Models\Transaction;
6
+ use Illuminate\Support\Carbon;
7
+
8
+ /**
9
+ * Time-series of revenue (sum of transactions) per bucket, filtered to the
10
+ * company's primary currency. Returns Chart.js-shaped { labels, datasets, summary }.
11
+ */
12
+ class RevenueTrend extends AbstractAnalytics
13
+ {
14
+ protected string $groupBy = 'day';
15
+
16
+ public function groupBy(string $unit): self
17
+ {
18
+ $this->groupBy = in_array($unit, ['day', 'week', 'month'], true) ? $unit : 'day';
19
+
20
+ return $this;
21
+ }
22
+
23
+ public function get(): array
24
+ {
25
+ $currency = $this->companyCurrency();
26
+ $start = $this->start ?? Carbon::now()->subDays(30)->toDateTime();
27
+ $end = $this->end ?? Carbon::now()->toDateTime();
28
+
29
+ $format = match ($this->groupBy) {
30
+ 'week' => '%x-W%v',
31
+ 'month' => '%Y-%m',
32
+ default => '%Y-%m-%d',
33
+ };
34
+
35
+ $rows = Transaction::where('company_uuid', $this->company->uuid)
36
+ ->where('currency', $currency)
37
+ ->whereBetween('created_at', [$start, $end])
38
+ ->selectRaw('DATE_FORMAT(created_at, ?) as bucket, SUM(amount) as total', [$format])
39
+ ->groupBy('bucket')
40
+ ->orderBy('bucket')
41
+ ->get();
42
+
43
+ $labels = $rows->pluck('bucket')->all();
44
+ $data = $rows->pluck('total')->map(fn ($v) => (float) $v)->all();
45
+ $total = array_sum($data);
46
+
47
+ $duration = $end->getTimestamp() - $start->getTimestamp();
48
+ $compareStart = (clone $start);
49
+ $compareStart = (new \DateTime())->setTimestamp($start->getTimestamp() - $duration);
50
+ $previousTotal = (float) Transaction::where('company_uuid', $this->company->uuid)
51
+ ->where('currency', $currency)
52
+ ->whereBetween('created_at', [$compareStart, $start])
53
+ ->sum('amount');
54
+
55
+ $deltaPct = $previousTotal > 0
56
+ ? round((($total - $previousTotal) / $previousTotal) * 100, 1)
57
+ : ($total > 0 ? 100.0 : 0.0);
58
+
59
+ return [
60
+ 'labels' => $labels,
61
+ 'datasets' => [
62
+ [
63
+ 'label' => 'Revenue',
64
+ 'data' => $data,
65
+ 'borderColor' => '#3485e2',
66
+ 'backgroundColor' => 'rgba(52, 133, 226, 0.1)',
67
+ 'fill' => true,
68
+ 'tension' => 0.3,
69
+ ],
70
+ ],
71
+ 'summary' => [
72
+ 'total' => round($total, 2),
73
+ 'currency' => $currency,
74
+ 'delta_pct' => $deltaPct,
75
+ ],
76
+ ];
77
+ }
78
+ }
@@ -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
+ }
@@ -0,0 +1,98 @@
1
+ <?php
2
+
3
+ namespace Fleetbase\FleetOps\Support;
4
+
5
+ use Fleetbase\FleetOps\Models\Contact;
6
+ use Illuminate\Http\Request;
7
+ use Illuminate\Support\Str;
8
+ use Laravel\Sanctum\PersonalAccessToken;
9
+
10
+ /**
11
+ * Helper for resolving the authenticated customer from a request.
12
+ *
13
+ * The customer token is issued by `CustomerController` as a Sanctum
14
+ * `PersonalAccessToken` whose `name` column carries the owning Contact's UUID.
15
+ * Mirrors {@see \Fleetbase\Storefront\Support\Storefront::getCustomerFromToken}
16
+ * but with no Storefront-specific session.
17
+ */
18
+ class CustomerAuth
19
+ {
20
+ public const HEADER = 'Customer-Token';
21
+
22
+ public const APP_BINDING = 'currentCustomer';
23
+
24
+ /**
25
+ * Resolve the Contact represented by the `Customer-Token` header.
26
+ *
27
+ * Preference order when a User has more than one customer-type Contact:
28
+ * 1. Contact whose UUID matches the token's `name` column (Storefront convention).
29
+ * 2. Contact in the same company as the resolved API credential (`session('company')`).
30
+ * 3. The first remaining customer-type Contact for the token's User.
31
+ */
32
+ public static function resolveFromHeader(?Request $request = null): ?Contact
33
+ {
34
+ $request = $request ?? request();
35
+ $token = $request->header(self::HEADER);
36
+ if (!$token) {
37
+ return null;
38
+ }
39
+
40
+ $accessToken = PersonalAccessToken::findToken($token);
41
+ if (!$accessToken) {
42
+ return null;
43
+ }
44
+
45
+ // Storefront convention: token name = Contact UUID.
46
+ if (is_string($accessToken->name) && Str::isUuid($accessToken->name)) {
47
+ $contact = Contact::where('uuid', $accessToken->name)
48
+ ->where('type', 'customer')
49
+ ->first();
50
+ if ($contact) {
51
+ return $contact;
52
+ }
53
+ }
54
+
55
+ // Fallback: pick the customer Contact bound to the token's user, preferring
56
+ // the same company as the API credential when available.
57
+ $userId = $accessToken->tokenable_id ?? null;
58
+ $userIdentifier = $accessToken->tokenable->uuid ?? $userId;
59
+ if (!$userIdentifier) {
60
+ return null;
61
+ }
62
+
63
+ $contactQuery = Contact::where('user_uuid', $userIdentifier)
64
+ ->where('type', 'customer');
65
+
66
+ $sessionCompany = session('company');
67
+ if ($sessionCompany) {
68
+ $companyPreferred = (clone $contactQuery)
69
+ ->where('company_uuid', $sessionCompany)
70
+ ->first();
71
+ if ($companyPreferred) {
72
+ return $companyPreferred;
73
+ }
74
+ }
75
+
76
+ return $contactQuery->first();
77
+ }
78
+
79
+ /**
80
+ * Return the currently-bound customer (set by AuthenticateCustomerToken middleware).
81
+ */
82
+ public static function current(): ?Contact
83
+ {
84
+ return app()->bound(self::APP_BINDING) ? app(self::APP_BINDING) : null;
85
+ }
86
+
87
+ /**
88
+ * Bind the resolved customer for the current request lifecycle.
89
+ */
90
+ public static function setCurrent(Contact $contact): void
91
+ {
92
+ app()->instance(self::APP_BINDING, $contact);
93
+ session([
94
+ 'customer_id' => $contact->uuid,
95
+ 'contact_id' => $contact->uuid,
96
+ ]);
97
+ }
98
+ }
@@ -0,0 +1,52 @@
1
+ <?php
2
+
3
+ namespace Fleetbase\FleetOps\Support;
4
+
5
+ use Barryvdh\DomPDF\Facade\Pdf;
6
+
7
+ class LabelPdf
8
+ {
9
+ /**
10
+ * Create a PDF instance for FleetOps labels with UTF-8 and embedded-font
11
+ * options applied consistently across order, waypoint, and entity labels.
12
+ */
13
+ public static function fromHtml(string $html)
14
+ {
15
+ $pdf = Pdf::loadHTML($html, 'UTF-8');
16
+
17
+ if (method_exists($pdf, 'setOptions')) {
18
+ $pdf->setOptions(static::options());
19
+ }
20
+
21
+ if (method_exists($pdf, 'getDomPDF')) {
22
+ $dompdf = $pdf->getDomPDF();
23
+ foreach (static::dompdfOptions() as $key => $value) {
24
+ if (method_exists($dompdf, 'set_option')) {
25
+ $dompdf->set_option($key, $value);
26
+ }
27
+ }
28
+ }
29
+
30
+ return $pdf;
31
+ }
32
+
33
+ public static function options(): array
34
+ {
35
+ return [
36
+ 'defaultFont' => 'DejaVu Sans',
37
+ 'isHtml5ParserEnabled' => true,
38
+ 'isFontSubsettingEnabled' => true,
39
+ 'isRemoteEnabled' => false,
40
+ ];
41
+ }
42
+
43
+ public static function dompdfOptions(): array
44
+ {
45
+ return [
46
+ 'defaultFont' => 'DejaVu Sans',
47
+ 'isHtml5ParserEnabled' => true,
48
+ 'isFontSubsettingEnabled' => true,
49
+ 'isRemoteEnabled' => false,
50
+ ];
51
+ }
52
+ }