@fleetbase/fleetops-engine 0.6.48 → 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 (151) hide show
  1. package/addon/components/driver/details.hbs +5 -1
  2. package/addon/components/driver/form.hbs +11 -1
  3. package/addon/components/fleet-listing-panel.hbs +2 -2
  4. package/addon/components/fleet-listing-panel.js +10 -0
  5. package/addon/components/map/google-live-map.js +12 -73
  6. package/addon/components/map/leaflet-live-map.hbs +149 -33
  7. package/addon/components/orchestrator/phase-builder.hbs +21 -0
  8. package/addon/components/orchestrator/phase-builder.js +13 -0
  9. package/addon/components/orchestrator-workbench.js +1 -0
  10. package/addon/components/order/form.hbs +2 -2
  11. package/addon/components/widget/fleet-ops-key-metrics.hbs +19 -17
  12. package/addon/components/widget/fleet-ops-key-metrics.js +26 -0
  13. package/addon/components/widget/fuel-efficiency.hbs +26 -0
  14. package/addon/components/widget/fuel-efficiency.js +43 -0
  15. package/addon/components/widget/geofence-violations.hbs +44 -0
  16. package/addon/components/widget/geofence-violations.js +55 -0
  17. package/addon/components/widget/issues-insights.hbs +43 -0
  18. package/addon/components/widget/issues-insights.js +62 -0
  19. package/addon/components/widget/kpi-active-orders.hbs +10 -0
  20. package/addon/components/widget/kpi-active-orders.js +5 -0
  21. package/addon/components/widget/kpi-aov.hbs +10 -0
  22. package/addon/components/widget/kpi-aov.js +5 -0
  23. package/addon/components/widget/kpi-distance.hbs +10 -0
  24. package/addon/components/widget/kpi-distance.js +5 -0
  25. package/addon/components/widget/kpi-drivers-online.hbs +10 -0
  26. package/addon/components/widget/kpi-drivers-online.js +5 -0
  27. package/addon/components/widget/kpi-earnings.hbs +10 -0
  28. package/addon/components/widget/kpi-earnings.js +5 -0
  29. package/addon/components/widget/kpi-open-issues.hbs +11 -0
  30. package/addon/components/widget/kpi-open-issues.js +5 -0
  31. package/addon/components/widget/kpi-tile.hbs +59 -0
  32. package/addon/components/widget/kpi-tile.js +168 -0
  33. package/addon/components/widget/live-fleet.hbs +122 -0
  34. package/addon/components/widget/live-fleet.js +365 -0
  35. package/addon/components/widget/maintenance-overview.hbs +45 -0
  36. package/addon/components/widget/maintenance-overview.js +40 -0
  37. package/addon/components/widget/on-time-delivery.hbs +27 -0
  38. package/addon/components/widget/on-time-delivery.js +73 -0
  39. package/addon/components/widget/operations-pulse.hbs +51 -0
  40. package/addon/components/widget/operations-pulse.js +173 -0
  41. package/addon/components/widget/orders-by-status.hbs +26 -0
  42. package/addon/components/widget/orders-by-status.js +54 -0
  43. package/addon/components/widget/revenue-trend.hbs +34 -0
  44. package/addon/components/widget/revenue-trend.js +67 -0
  45. package/addon/components/widget/top-drivers.hbs +44 -0
  46. package/addon/components/widget/top-drivers.js +54 -0
  47. package/addon/controllers/settings/routing.js +26 -0
  48. package/addon/extension.js +172 -3
  49. package/addon/services/map-adapter/google.js +13 -2
  50. package/addon/styles/fleetops-engine.css +114 -87
  51. package/addon/templates/maintenance/equipment/index.hbs +1 -1
  52. package/addon/templates/maintenance/parts/index.hbs +1 -1
  53. package/addon/templates/maintenance/schedules/index.hbs +1 -1
  54. package/addon/templates/management/vehicles/index.hbs +1 -1
  55. package/addon/templates/settings/avatars.hbs +1 -1
  56. package/addon/templates/settings/custom-fields.hbs +1 -1
  57. package/addon/templates/settings/map.hbs +1 -1
  58. package/addon/templates/settings/navigator-app.hbs +1 -1
  59. package/addon/templates/settings/notifications.hbs +1 -1
  60. package/addon/templates/settings/orchestrator.hbs +1 -1
  61. package/addon/templates/settings/payments/index.hbs +1 -1
  62. package/addon/templates/settings/payments/onboard.hbs +1 -1
  63. package/addon/templates/settings/routing.hbs +22 -2
  64. package/addon/templates/settings/scheduling.hbs +1 -1
  65. package/addon/utils/live-map-card-content.js +171 -0
  66. package/app/components/widget/fuel-efficiency.js +1 -0
  67. package/app/components/widget/geofence-violations.js +1 -0
  68. package/app/components/widget/issues-insights.js +1 -0
  69. package/app/components/widget/kpi-active-orders.js +1 -0
  70. package/app/components/widget/kpi-aov.js +1 -0
  71. package/app/components/widget/kpi-distance.js +1 -0
  72. package/app/components/widget/kpi-drivers-online.js +1 -0
  73. package/app/components/widget/kpi-earnings.js +1 -0
  74. package/app/components/widget/kpi-open-issues.js +1 -0
  75. package/app/components/widget/kpi-tile.js +1 -0
  76. package/app/components/widget/live-fleet.js +1 -0
  77. package/app/components/widget/maintenance-overview.js +1 -0
  78. package/app/components/widget/on-time-delivery.js +1 -0
  79. package/app/components/widget/operations-pulse.js +1 -0
  80. package/app/components/widget/orders-by-status.js +1 -0
  81. package/app/components/widget/revenue-trend.js +1 -0
  82. package/app/components/widget/top-drivers.js +1 -0
  83. package/app/utils/live-map-card-content.js +1 -0
  84. package/composer.json +1 -1
  85. package/extension.json +1 -1
  86. package/package.json +3 -3
  87. package/pnpm-workspace.yaml +1 -0
  88. package/server/config/telematics.php +111 -1
  89. package/server/migrations/2026_05_18_000001_add_license_expiry_to_drivers_table.php +25 -0
  90. package/server/seeders/Testing/Concerns/SeedsTestingData.php +183 -0
  91. package/server/seeders/Testing/ConnectivitySeeder.php +212 -0
  92. package/server/seeders/Testing/FleetSeeder.php +341 -0
  93. package/server/seeders/Testing/MaintenanceSeeder.php +238 -0
  94. package/server/seeders/Testing/NetworkSeeder.php +250 -0
  95. package/server/seeders/Testing/OrdersSeeder.php +214 -0
  96. package/server/seeders/Testing/TestingSeeder.php +40 -0
  97. package/server/seeders/{VroomOrchestrationSeeder.php → Testing/VroomOrchestrationSeeder.php} +8 -1
  98. package/server/src/Console/Commands/ProcessOperationalAlerts.php +279 -0
  99. package/server/src/Http/Controllers/Internal/v1/AnalyticsController.php +119 -0
  100. package/server/src/Http/Controllers/Internal/v1/MetricsController.php +82 -3
  101. package/server/src/Http/Controllers/Internal/v1/OrderController.php +12 -6
  102. package/server/src/Http/Controllers/Internal/v1/SettingController.php +46 -1
  103. package/server/src/Http/Resources/v1/Driver.php +2 -0
  104. package/server/src/Http/Resources/v1/Fleet.php +4 -0
  105. package/server/src/Http/Resources/v1/Index/Driver.php +48 -0
  106. package/server/src/Http/Resources/v1/Index/Vehicle.php +74 -1
  107. package/server/src/Models/Driver.php +2 -0
  108. package/server/src/Models/OrderConfig.php +14 -11
  109. package/server/src/Notifications/LateDeparture.php +48 -0
  110. package/server/src/Notifications/ProlongedStoppage.php +48 -0
  111. package/server/src/Notifications/RouteDeviation.php +48 -0
  112. package/server/src/Orchestration/Engines/VroomOrchestrationEngine.php +28 -1
  113. package/server/src/Providers/FleetOpsServiceProvider.php +5 -0
  114. package/server/src/Providers/NotificationServiceProvider.php +3 -0
  115. package/server/src/Support/Analytics/AbstractAnalytics.php +77 -0
  116. package/server/src/Support/Analytics/FuelEfficiency.php +83 -0
  117. package/server/src/Support/Analytics/GeofenceViolations.php +63 -0
  118. package/server/src/Support/Analytics/IssuesInsights.php +67 -0
  119. package/server/src/Support/Analytics/LiveFleet.php +127 -0
  120. package/server/src/Support/Analytics/MaintenanceOverview.php +70 -0
  121. package/server/src/Support/Analytics/OnTimeDelivery.php +75 -0
  122. package/server/src/Support/Analytics/OperationsPulse.php +86 -0
  123. package/server/src/Support/Analytics/OrdersByStatus.php +64 -0
  124. package/server/src/Support/Analytics/RevenueTrend.php +79 -0
  125. package/server/src/Support/Analytics/TopDrivers.php +92 -0
  126. package/server/src/Support/Metrics/AbstractMetric.php +170 -0
  127. package/server/src/Support/Metrics/ActiveLiveOrdersMetric.php +28 -0
  128. package/server/src/Support/Metrics/AvgOrderValueMetric.php +53 -0
  129. package/server/src/Support/Metrics/DriversOnlineMetric.php +30 -0
  130. package/server/src/Support/Metrics/EarningsMetric.php +30 -0
  131. package/server/src/Support/Metrics/FuelCostsMetric.php +30 -0
  132. package/server/src/Support/Metrics/MoneyMetric.php +30 -0
  133. package/server/src/Support/Metrics/OpenIssuesMetric.php +35 -0
  134. package/server/src/Support/Metrics/OrdersCanceledMetric.php +35 -0
  135. package/server/src/Support/Metrics/OrdersCompletedMetric.php +35 -0
  136. package/server/src/Support/Metrics/OrdersInProgressMetric.php +51 -0
  137. package/server/src/Support/Metrics/OrdersScheduledMetric.php +37 -0
  138. package/server/src/Support/Metrics/Registry.php +46 -0
  139. package/server/src/Support/Metrics/ResolvedIssuesMetric.php +35 -0
  140. package/server/src/Support/Metrics/TotalCustomersMetric.php +29 -0
  141. package/server/src/Support/Metrics/TotalDistanceTraveledMetric.php +35 -0
  142. package/server/src/Support/Metrics/TotalDriversMetric.php +28 -0
  143. package/server/src/Support/Metrics/TotalTimeTraveledMetric.php +35 -0
  144. package/server/src/Support/Metrics.php +46 -249
  145. package/server/src/Support/Telematics/Providers/AfaqyProvider.php +290 -0
  146. package/server/src/Support/Telematics/Providers/SafeeProvider.php +333 -0
  147. package/server/src/routes.php +16 -0
  148. package/server/tests/AnalyticsRoutesTest.php +41 -0
  149. package/server/tests/MetricsRegistryTest.php +88 -0
  150. package/server/tests/VroomOrchestrationEngineTest.php +91 -0
  151. package/translations/en-us.yaml +37 -0
@@ -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,170 @@
1
+ <?php
2
+
3
+ namespace Fleetbase\FleetOps\Support\Metrics;
4
+
5
+ use Fleetbase\Models\Company;
6
+ use Illuminate\Support\Carbon;
7
+
8
+ /**
9
+ * Base class for FleetOps scalar metrics.
10
+ *
11
+ * Subclasses implement query() + aggregate() and declare slug() + format(). The base
12
+ * handles period selection, optional period-over-period delta, and optional sparkline.
13
+ */
14
+ abstract class AbstractMetric
15
+ {
16
+ protected Company $company;
17
+ protected ?\DateTimeInterface $start = null;
18
+ protected ?\DateTimeInterface $end = null;
19
+ protected ?\DateTimeInterface $compareStart = null;
20
+ protected ?\DateTimeInterface $compareEnd = null;
21
+ protected int $sparklineBuckets = 0;
22
+ protected string $sparklineUnit = 'day';
23
+
24
+ /**
25
+ * Period boundaries for the currently-executing aggregate call.
26
+ * Set by value()/delta()/sparkline() before invoking aggregate(); subclasses
27
+ * that need to issue a second parallel query (e.g. AvgOrderValueMetric) read
28
+ * these instead of re-introspecting the query's where clauses.
29
+ */
30
+ protected ?\DateTimeInterface $currentStart = null;
31
+ protected ?\DateTimeInterface $currentEnd = null;
32
+
33
+ abstract public static function slug(): string;
34
+
35
+ abstract public function format(): string;
36
+
37
+ /**
38
+ * Build the query for the given period. The base class invokes this with the
39
+ * current period, the comparison period (for delta), and one sub-range per
40
+ * sparkline bucket — so it must be cheap and idempotent.
41
+ */
42
+ abstract protected function query(?\DateTimeInterface $start, ?\DateTimeInterface $end);
43
+
44
+ /**
45
+ * Reduce a query to a single numeric value. Override for sums (`->sum('amount')`),
46
+ * counts (`->count()`), or computed expressions.
47
+ */
48
+ abstract protected function aggregate($query): float|int;
49
+
50
+ public function currency(): ?string
51
+ {
52
+ return null;
53
+ }
54
+
55
+ public static function forCompany(Company $company): static
56
+ {
57
+ return (new static())->setCompany($company);
58
+ }
59
+
60
+ public function between(?\DateTimeInterface $start, ?\DateTimeInterface $end): static
61
+ {
62
+ $this->start = $start;
63
+ $this->end = $end;
64
+
65
+ return $this;
66
+ }
67
+
68
+ public function compareTo(\DateTimeInterface $start, \DateTimeInterface $end): static
69
+ {
70
+ $this->compareStart = $start;
71
+ $this->compareEnd = $end;
72
+
73
+ return $this;
74
+ }
75
+
76
+ public function withSparkline(int $buckets = 14, string $unit = 'day'): static
77
+ {
78
+ $this->sparklineBuckets = max(0, $buckets);
79
+ $this->sparklineUnit = $unit;
80
+
81
+ return $this;
82
+ }
83
+
84
+ public function value(): float|int
85
+ {
86
+ return $this->runAggregate($this->start, $this->end);
87
+ }
88
+
89
+ public function delta(): ?float
90
+ {
91
+ if (!$this->compareStart || !$this->compareEnd) {
92
+ return null;
93
+ }
94
+
95
+ $current = $this->value();
96
+ $previous = $this->runAggregate($this->compareStart, $this->compareEnd);
97
+
98
+ if ((float) $previous === 0.0) {
99
+ return $current > 0 ? 100.0 : 0.0;
100
+ }
101
+
102
+ return round((($current - $previous) / $previous) * 100, 1);
103
+ }
104
+
105
+ public function sparkline(): ?array
106
+ {
107
+ if ($this->sparklineBuckets === 0 || $this->end === null) {
108
+ return null;
109
+ }
110
+
111
+ $end = Carbon::instance($this->end);
112
+ $cursor = $end->copy()->sub($this->sparklineUnit, $this->sparklineBuckets);
113
+ $labels = [];
114
+ $data = [];
115
+
116
+ for ($i = 0; $i < $this->sparklineBuckets; $i++) {
117
+ $next = $cursor->copy()->add($this->sparklineUnit, 1);
118
+ $labels[] = $cursor->format('Y-m-d');
119
+ $data[] = $this->runAggregate($cursor->toDateTime(), $next->toDateTime());
120
+ $cursor = $next;
121
+ }
122
+
123
+ return ['labels' => $labels, 'data' => $data];
124
+ }
125
+
126
+ /**
127
+ * Run a single aggregation for a sub-period. Sets $currentStart/$currentEnd
128
+ * so subclasses that need to issue a parallel query can read the bucket range.
129
+ */
130
+ protected function runAggregate(?\DateTimeInterface $start, ?\DateTimeInterface $end): float|int
131
+ {
132
+ $this->currentStart = $start;
133
+ $this->currentEnd = $end;
134
+
135
+ return $this->aggregate($this->query($start, $end));
136
+ }
137
+
138
+ public function get(): array
139
+ {
140
+ $payload = [
141
+ 'slug' => static::slug(),
142
+ 'value' => $this->value(),
143
+ 'format' => $this->format(),
144
+ ];
145
+
146
+ $currency = $this->currency();
147
+ if ($currency !== null) {
148
+ $payload['currency'] = $currency;
149
+ }
150
+
151
+ $delta = $this->delta();
152
+ if ($delta !== null) {
153
+ $payload['delta_pct'] = $delta;
154
+ }
155
+
156
+ $sparkline = $this->sparkline();
157
+ if ($sparkline !== null) {
158
+ $payload['sparkline'] = $sparkline;
159
+ }
160
+
161
+ return $payload;
162
+ }
163
+
164
+ protected function setCompany(Company $company): static
165
+ {
166
+ $this->company = $company;
167
+
168
+ return $this;
169
+ }
170
+ }
@@ -0,0 +1,28 @@
1
+ <?php
2
+
3
+ namespace Fleetbase\FleetOps\Support\Metrics;
4
+
5
+ use Fleetbase\FleetOps\Support\LiveOrderQuery;
6
+
7
+ class ActiveLiveOrdersMetric extends AbstractMetric
8
+ {
9
+ public static function slug(): string
10
+ {
11
+ return 'active_live_orders';
12
+ }
13
+
14
+ public function format(): string
15
+ {
16
+ return 'count';
17
+ }
18
+
19
+ protected function query(?\DateTimeInterface $start, ?\DateTimeInterface $end)
20
+ {
21
+ return LiveOrderQuery::make($this->company->uuid, ['active' => true]);
22
+ }
23
+
24
+ protected function aggregate($query): int
25
+ {
26
+ return (int) $query->count();
27
+ }
28
+ }
@@ -0,0 +1,53 @@
1
+ <?php
2
+
3
+ namespace Fleetbase\FleetOps\Support\Metrics;
4
+
5
+ use Fleetbase\FleetOps\Models\Order;
6
+ use Fleetbase\Models\Transaction;
7
+
8
+ /**
9
+ * Average revenue per completed order in the period. Computed as
10
+ * sum(transactions.amount, primary_currency) / count(completed orders).
11
+ *
12
+ * Uses $this->currentStart/$currentEnd (set by the base class) so the metric
13
+ * cooperates with the sparkline machinery — each bucket recomputes both
14
+ * numerator and denominator over its own sub-range.
15
+ */
16
+ class AvgOrderValueMetric extends MoneyMetric
17
+ {
18
+ public static function slug(): string
19
+ {
20
+ return 'avg_order_value';
21
+ }
22
+
23
+ protected function query(?\DateTimeInterface $start, ?\DateTimeInterface $end)
24
+ {
25
+ $query = Order::where('company_uuid', $this->company->uuid)
26
+ ->where('status', 'completed');
27
+
28
+ if ($start && $end) {
29
+ $query->whereBetween('created_at', [$start, $end]);
30
+ }
31
+
32
+ return $query;
33
+ }
34
+
35
+ protected function aggregate($query): float
36
+ {
37
+ $orderCount = (int) $query->count();
38
+ if ($orderCount === 0) {
39
+ return 0.0;
40
+ }
41
+
42
+ $revenueQuery = Transaction::where('company_uuid', $this->company->uuid)
43
+ ->where('currency', $this->currency());
44
+
45
+ if ($this->currentStart && $this->currentEnd) {
46
+ $revenueQuery->whereBetween('created_at', [$this->currentStart, $this->currentEnd]);
47
+ }
48
+
49
+ $totalRevenue = (float) $revenueQuery->sum('amount');
50
+
51
+ return round($totalRevenue / $orderCount, 2);
52
+ }
53
+ }
@@ -0,0 +1,30 @@
1
+ <?php
2
+
3
+ namespace Fleetbase\FleetOps\Support\Metrics;
4
+
5
+ use Fleetbase\FleetOps\Models\Driver;
6
+
7
+ class DriversOnlineMetric extends AbstractMetric
8
+ {
9
+ public static function slug(): string
10
+ {
11
+ return 'drivers_online';
12
+ }
13
+
14
+ public function format(): string
15
+ {
16
+ return 'count';
17
+ }
18
+
19
+ protected function query(?\DateTimeInterface $start, ?\DateTimeInterface $end)
20
+ {
21
+ return Driver::where('company_uuid', $this->company->uuid)
22
+ ->where('online', true)
23
+ ->whereNotNull('current_job_uuid');
24
+ }
25
+
26
+ protected function aggregate($query): int
27
+ {
28
+ return (int) $query->count();
29
+ }
30
+ }
@@ -0,0 +1,30 @@
1
+ <?php
2
+
3
+ namespace Fleetbase\FleetOps\Support\Metrics;
4
+
5
+ use Fleetbase\Models\Transaction;
6
+
7
+ class EarningsMetric extends MoneyMetric
8
+ {
9
+ public static function slug(): string
10
+ {
11
+ return 'earnings';
12
+ }
13
+
14
+ protected function query(?\DateTimeInterface $start, ?\DateTimeInterface $end)
15
+ {
16
+ $query = Transaction::where('company_uuid', $this->company->uuid)
17
+ ->where('currency', $this->currency());
18
+
19
+ if ($start && $end) {
20
+ $query->whereBetween('created_at', [$start, $end]);
21
+ }
22
+
23
+ return $query;
24
+ }
25
+
26
+ protected function aggregate($query): float
27
+ {
28
+ return (float) $query->sum('amount');
29
+ }
30
+ }
@@ -0,0 +1,30 @@
1
+ <?php
2
+
3
+ namespace Fleetbase\FleetOps\Support\Metrics;
4
+
5
+ use Fleetbase\FleetOps\Models\FuelReport;
6
+
7
+ class FuelCostsMetric extends MoneyMetric
8
+ {
9
+ public static function slug(): string
10
+ {
11
+ return 'fuel_costs';
12
+ }
13
+
14
+ protected function query(?\DateTimeInterface $start, ?\DateTimeInterface $end)
15
+ {
16
+ $query = FuelReport::where('company_uuid', $this->company->uuid)
17
+ ->where('currency', $this->currency());
18
+
19
+ if ($start && $end) {
20
+ $query->whereBetween('created_at', [$start, $end]);
21
+ }
22
+
23
+ return $query;
24
+ }
25
+
26
+ protected function aggregate($query): float
27
+ {
28
+ return (float) $query->sum('amount');
29
+ }
30
+ }
@@ -0,0 +1,30 @@
1
+ <?php
2
+
3
+ namespace Fleetbase\FleetOps\Support\Metrics;
4
+
5
+ /**
6
+ * Base for money-valued metrics. Enforces currency awareness so we never repeat
7
+ * the legacy bug of summing mixed-currency rows.
8
+ */
9
+ abstract class MoneyMetric extends AbstractMetric
10
+ {
11
+ protected ?string $currencyOverride = null;
12
+
13
+ public function inCurrency(?string $currency): static
14
+ {
15
+ $this->currencyOverride = $currency;
16
+
17
+ return $this;
18
+ }
19
+
20
+ public function format(): string
21
+ {
22
+ return 'money';
23
+ }
24
+
25
+ public function currency(): string
26
+ {
27
+ return $this->currencyOverride
28
+ ?? ($this->company->currency ?? 'USD');
29
+ }
30
+ }
@@ -0,0 +1,35 @@
1
+ <?php
2
+
3
+ namespace Fleetbase\FleetOps\Support\Metrics;
4
+
5
+ use Fleetbase\FleetOps\Models\Issue;
6
+
7
+ class OpenIssuesMetric extends AbstractMetric
8
+ {
9
+ public static function slug(): string
10
+ {
11
+ return 'open_issues';
12
+ }
13
+
14
+ public function format(): string
15
+ {
16
+ return 'count';
17
+ }
18
+
19
+ protected function query(?\DateTimeInterface $start, ?\DateTimeInterface $end)
20
+ {
21
+ $query = Issue::where('company_uuid', $this->company->uuid)
22
+ ->where('status', 'pending');
23
+
24
+ if ($start && $end) {
25
+ $query->whereBetween('created_at', [$start, $end]);
26
+ }
27
+
28
+ return $query;
29
+ }
30
+
31
+ protected function aggregate($query): int
32
+ {
33
+ return (int) $query->count();
34
+ }
35
+ }
@@ -0,0 +1,35 @@
1
+ <?php
2
+
3
+ namespace Fleetbase\FleetOps\Support\Metrics;
4
+
5
+ use Fleetbase\FleetOps\Models\Order;
6
+
7
+ class OrdersCanceledMetric extends AbstractMetric
8
+ {
9
+ public static function slug(): string
10
+ {
11
+ return 'orders_canceled';
12
+ }
13
+
14
+ public function format(): string
15
+ {
16
+ return 'count';
17
+ }
18
+
19
+ protected function query(?\DateTimeInterface $start, ?\DateTimeInterface $end)
20
+ {
21
+ $query = Order::where('company_uuid', $this->company->uuid)
22
+ ->where('status', 'canceled');
23
+
24
+ if ($start && $end) {
25
+ $query->whereBetween('created_at', [$start, $end]);
26
+ }
27
+
28
+ return $query;
29
+ }
30
+
31
+ protected function aggregate($query): int
32
+ {
33
+ return (int) $query->count();
34
+ }
35
+ }
@@ -0,0 +1,35 @@
1
+ <?php
2
+
3
+ namespace Fleetbase\FleetOps\Support\Metrics;
4
+
5
+ use Fleetbase\FleetOps\Models\Order;
6
+
7
+ class OrdersCompletedMetric extends AbstractMetric
8
+ {
9
+ public static function slug(): string
10
+ {
11
+ return 'orders_completed';
12
+ }
13
+
14
+ public function format(): string
15
+ {
16
+ return 'count';
17
+ }
18
+
19
+ protected function query(?\DateTimeInterface $start, ?\DateTimeInterface $end)
20
+ {
21
+ $query = Order::where('company_uuid', $this->company->uuid)
22
+ ->where('status', 'completed');
23
+
24
+ if ($start && $end) {
25
+ $query->whereBetween('created_at', [$start, $end]);
26
+ }
27
+
28
+ return $query;
29
+ }
30
+
31
+ protected function aggregate($query): int
32
+ {
33
+ return (int) $query->count();
34
+ }
35
+ }
@@ -0,0 +1,51 @@
1
+ <?php
2
+
3
+ namespace Fleetbase\FleetOps\Support\Metrics;
4
+
5
+ use Fleetbase\FleetOps\Models\Order;
6
+
7
+ /**
8
+ * Counts orders currently in flight. Uses an explicit allowlist of "active" statuses
9
+ * rather than the legacy exclusion list, which was brittle against new statuses.
10
+ */
11
+ class OrdersInProgressMetric extends AbstractMetric
12
+ {
13
+ public const IN_PROGRESS_STATUSES = [
14
+ 'dispatched',
15
+ 'assigned',
16
+ 'started',
17
+ 'enroute',
18
+ 'in_progress',
19
+ 'driver_enroute',
20
+ 'picking_up',
21
+ 'picked_up',
22
+ 'dropping_off',
23
+ ];
24
+
25
+ public static function slug(): string
26
+ {
27
+ return 'orders_in_progress';
28
+ }
29
+
30
+ public function format(): string
31
+ {
32
+ return 'count';
33
+ }
34
+
35
+ protected function query(?\DateTimeInterface $start, ?\DateTimeInterface $end)
36
+ {
37
+ $query = Order::where('company_uuid', $this->company->uuid)
38
+ ->whereIn('status', self::IN_PROGRESS_STATUSES);
39
+
40
+ if ($start && $end) {
41
+ $query->whereBetween('created_at', [$start, $end]);
42
+ }
43
+
44
+ return $query;
45
+ }
46
+
47
+ protected function aggregate($query): int
48
+ {
49
+ return (int) $query->count();
50
+ }
51
+ }
@@ -0,0 +1,37 @@
1
+ <?php
2
+
3
+ namespace Fleetbase\FleetOps\Support\Metrics;
4
+
5
+ use Fleetbase\FleetOps\Models\Order;
6
+ use Illuminate\Support\Carbon;
7
+
8
+ class OrdersScheduledMetric extends AbstractMetric
9
+ {
10
+ public static function slug(): string
11
+ {
12
+ return 'orders_scheduled';
13
+ }
14
+
15
+ public function format(): string
16
+ {
17
+ return 'count';
18
+ }
19
+
20
+ protected function query(?\DateTimeInterface $start, ?\DateTimeInterface $end)
21
+ {
22
+ $query = Order::where('company_uuid', $this->company->uuid)
23
+ ->where('status', 'created')
24
+ ->whereDate('scheduled_at', '>', Carbon::now());
25
+
26
+ if ($start && $end) {
27
+ $query->whereBetween('created_at', [$start, $end]);
28
+ }
29
+
30
+ return $query;
31
+ }
32
+
33
+ protected function aggregate($query): int
34
+ {
35
+ return (int) $query->count();
36
+ }
37
+ }