@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
@@ -33,6 +33,7 @@ class Driver extends FleetbaseResource
33
33
  'current_job_uuid'=> $this->when($isInternal, $this->current_job_uuid),
34
34
  'name' => $this->name,
35
35
  'vehicle_name' => $this->when($isInternal, $this->vehicle_name),
36
+ 'email' => $this->email,
36
37
  'phone' => $this->phone,
37
38
  'photo_url' => $this->photo_url,
38
39
  'status' => $this->status,
@@ -41,6 +42,53 @@ class Driver extends FleetbaseResource
41
42
  'altitude' => (int) data_get($this, 'altitude', 0),
42
43
  'speed' => (int) data_get($this, 'speed', 0),
43
44
  'online' => data_get($this, 'online', false),
45
+ 'meta' => [
46
+ '_index_resource' => true,
47
+ 'location_coordinates' => $this->locationCoordinates(),
48
+ 'current_order_reference' => $this->currentOrderReference(),
49
+ 'speed_label' => $this->speedLabel(),
50
+ 'heading_label' => $this->headingLabel(),
51
+ 'status_label' => $this->statusLabel(),
52
+ ],
44
53
  ];
45
54
  }
55
+
56
+ protected function currentOrderReference(): ?string
57
+ {
58
+ $this->loadMissing('currentOrder');
59
+ $order = data_get($this, 'currentOrder');
60
+
61
+ return data_get($order, 'tracking') ?? data_get($order, 'public_id');
62
+ }
63
+
64
+ protected function locationCoordinates(): ?string
65
+ {
66
+ $location = Utils::castPoint($this->location);
67
+
68
+ return $location ? $this->formatCoordinate($location->getLat()) . ' ' . $this->formatCoordinate($location->getLng()) : null;
69
+ }
70
+
71
+ protected function speedLabel(): string
72
+ {
73
+ $speed = data_get($this, 'speed');
74
+
75
+ return is_numeric($speed) ? ((int) $speed) . ' km/h' : '-';
76
+ }
77
+
78
+ protected function headingLabel(): string
79
+ {
80
+ $heading = data_get($this, 'heading');
81
+
82
+ return is_numeric($heading) ? ((int) $heading) . ' deg' : '-';
83
+ }
84
+
85
+ protected function statusLabel(): ?string
86
+ {
87
+ return $this->status ? str($this->status)->replace(['_', '-'], ' ')->headline()->toString() : null;
88
+ }
89
+
90
+ protected function formatCoordinate(float $coordinate): string
91
+ {
92
+ return (string) round($coordinate, 4);
93
+ }
46
94
  }
@@ -2,6 +2,7 @@
2
2
 
3
3
  namespace Fleetbase\FleetOps\Http\Resources\v1\Index;
4
4
 
5
+ use Fleetbase\FleetOps\Models\Order;
5
6
  use Fleetbase\FleetOps\Support\Utils;
6
7
  use Fleetbase\Http\Resources\FleetbaseResource;
7
8
  use Fleetbase\Support\Http;
@@ -28,8 +29,12 @@ class Vehicle extends FleetbaseResource
28
29
  'company_uuid' => $this->when($isInternal, $this->company_uuid),
29
30
  'vendor_uuid' => $this->when($isInternal, $this->vendor_uuid),
30
31
  'photo_uuid' => $this->when($isInternal, $this->photo_uuid),
32
+ 'internal_id' => $this->internal_id,
31
33
  'display_name' => $this->display_name,
34
+ 'driver_name' => $this->driver_name,
32
35
  'plate_number' => $this->plate_number,
36
+ 'serial_number' => $this->serial_number,
37
+ 'vin' => $this->vin,
33
38
  'make' => $this->make,
34
39
  'model' => $this->model,
35
40
  'year' => $this->year,
@@ -43,8 +48,76 @@ class Vehicle extends FleetbaseResource
43
48
 
44
49
  // Meta flag to indicate this is an index resource
45
50
  'meta' => [
46
- '_index_resource' => true,
51
+ '_index_resource' => true,
52
+ 'current_order_reference' => $this->currentOrderReference(),
53
+ 'location_coordinates' => $this->locationCoordinates(),
54
+ 'speed_label' => $this->speedLabel(),
55
+ 'heading_label' => $this->headingLabel(),
56
+ 'status_label' => $this->statusLabel(),
47
57
  ],
48
58
  ];
49
59
  }
60
+
61
+ protected function currentOrderReference(): ?string
62
+ {
63
+ $this->loadMissing('driver.currentOrder');
64
+ $order = data_get($this, 'driver.currentOrder') ?? $this->currentPositionOrder() ?? $this->currentVehicleOrder();
65
+
66
+ return data_get($order, 'tracking') ?? data_get($order, 'public_id');
67
+ }
68
+
69
+ protected function locationCoordinates(): ?string
70
+ {
71
+ $location = Utils::castPoint($this->location);
72
+
73
+ return $location ? $this->formatCoordinate($location->getLat()) . ' ' . $this->formatCoordinate($location->getLng()) : null;
74
+ }
75
+
76
+ protected function speedLabel(): string
77
+ {
78
+ $speed = data_get($this->lastKnownPosition(), 'speed', data_get($this, 'speed'));
79
+
80
+ return is_numeric($speed) ? ((int) $speed) . ' km/h' : '-';
81
+ }
82
+
83
+ protected function headingLabel(): string
84
+ {
85
+ $heading = data_get($this->lastKnownPosition(), 'heading', data_get($this, 'heading'));
86
+
87
+ return is_numeric($heading) ? ((int) $heading) . ' deg' : '-';
88
+ }
89
+
90
+ protected function statusLabel(): ?string
91
+ {
92
+ return $this->status ? str($this->status)->replace(['_', '-'], ' ')->headline()->toString() : null;
93
+ }
94
+
95
+ protected function currentPositionOrder(): ?Order
96
+ {
97
+ $orderUuid = data_get($this->lastKnownPosition(), 'order_uuid');
98
+
99
+ return $orderUuid ? Order::where('uuid', $orderUuid)->first() : null;
100
+ }
101
+
102
+ protected function currentVehicleOrder(): ?Order
103
+ {
104
+ return Order::where('vehicle_assigned_uuid', $this->uuid)
105
+ ->whereNotIn('status', ['completed', 'canceled', 'cancelled'])
106
+ ->latest()
107
+ ->first();
108
+ }
109
+
110
+ protected function lastKnownPosition()
111
+ {
112
+ if (!$this->resource->relationLoaded('last_known_position')) {
113
+ $this->resource->setRelation('last_known_position', $this->getLastKnownPosition());
114
+ }
115
+
116
+ return $this->resource->getRelation('last_known_position');
117
+ }
118
+
119
+ protected function formatCoordinate(float $coordinate): string
120
+ {
121
+ return (string) round($coordinate, 4);
122
+ }
50
123
  }
@@ -97,6 +97,7 @@ class Driver extends Model
97
97
  'signup_token_used',
98
98
  'avatar_url',
99
99
  'drivers_license_number',
100
+ 'license_expiry',
100
101
  'location',
101
102
  'heading',
102
103
  'bearing',
@@ -141,6 +142,7 @@ class Driver extends Model
141
142
  'location' => Point::class,
142
143
  'online' => 'boolean',
143
144
  'meta' => Json::class,
145
+ 'license_expiry' => 'date',
144
146
  // Orchestrator
145
147
  'skills' => Json::class,
146
148
  'max_travel_time' => 'integer',
@@ -458,28 +458,31 @@ class OrderConfig extends Model
458
458
  /**
459
459
  * Resolves an OrderConfig from a given identifier or an array of identifiers within an optional company context.
460
460
  *
461
- * @param string|array $orderConfigIdentifier the identifier(s) for the OrderConfig (namespace, public_id, or key)
461
+ * @param string|array $orderConfigIdentifier the identifier(s) for the OrderConfig (uuid, namespace, public_id, or key)
462
462
  * @param Company|null $company the company context, if any, to narrow down the search
463
463
  *
464
464
  * @return OrderConfig|null the found OrderConfig or null if none found
465
465
  */
466
466
  public static function resolveFromIdentifier($orderConfigIdentifier, ?Company $company = null): ?OrderConfig
467
467
  {
468
- $query = static::query();
468
+ $companyUuid = $company instanceof Company ? $company->uuid : session('company');
469
+ $identifiers = is_array($orderConfigIdentifier) ? $orderConfigIdentifier : [$orderConfigIdentifier];
470
+ $identifiers = collect($identifiers)->filter(fn ($identifier) => filled($identifier))->values();
471
+
472
+ if ($identifiers->isEmpty()) {
473
+ return static::default($company);
474
+ }
475
+
476
+ foreach ($identifiers as $identifier) {
477
+ $query = static::query();
469
478
 
470
- if ($company instanceof Company) {
471
- $query->where('company_uuid', $company->uuid);
472
- } else {
473
- $companyUuid = session('company');
474
479
  if ($companyUuid) {
475
480
  $query->where('company_uuid', $companyUuid);
476
481
  }
477
- }
478
482
 
479
- $identifiers = is_array($orderConfigIdentifier) ? $orderConfigIdentifier : [$orderConfigIdentifier];
480
- foreach ($identifiers as $identifier) {
481
483
  $orderConfig = $query->where(function ($query) use ($identifier) {
482
- $query->where('namespace', $identifier)
484
+ $query->where('uuid', $identifier)
485
+ ->orWhere('namespace', $identifier)
483
486
  ->orWhere('public_id', $identifier)
484
487
  ->orWhere('key', $identifier);
485
488
  })->first();
@@ -489,7 +492,7 @@ class OrderConfig extends Model
489
492
  }
490
493
  }
491
494
 
492
- return static::default($company);
495
+ return null;
493
496
  }
494
497
 
495
498
  /**
@@ -0,0 +1,48 @@
1
+ <?php
2
+
3
+ namespace Fleetbase\FleetOps\Notifications;
4
+
5
+ use Fleetbase\FleetOps\Models\Order;
6
+ use Illuminate\Bus\Queueable;
7
+ use Illuminate\Contracts\Queue\ShouldQueue;
8
+ use Illuminate\Notifications\Messages\MailMessage;
9
+ use Illuminate\Notifications\Notification;
10
+
11
+ class LateDeparture extends Notification implements ShouldQueue
12
+ {
13
+ use Queueable;
14
+
15
+ public static string $name = 'Late Departure';
16
+ public static string $description = 'Notify when an order has not departed after the configured grace period.';
17
+ public static string $package = 'fleet-ops';
18
+
19
+ public function __construct(public Order $order, public array $context = [])
20
+ {
21
+ }
22
+
23
+ public function via($notifiable): array
24
+ {
25
+ return ['mail', 'database'];
26
+ }
27
+
28
+ public function toMail($notifiable): MailMessage
29
+ {
30
+ $orderId = $this->order->tracking ?? $this->order->public_id;
31
+
32
+ return (new MailMessage())
33
+ ->subject("Late departure detected for {$orderId}")
34
+ ->line("Order {$orderId} has not departed within the configured grace period.")
35
+ ->line('Please review the order and dispatch status.');
36
+ }
37
+
38
+ public function toArray($notifiable): array
39
+ {
40
+ return [
41
+ 'event' => 'order.late_departure',
42
+ 'order_id' => $this->order->public_id,
43
+ 'order_uuid' => $this->order->uuid,
44
+ 'context' => $this->context,
45
+ 'message' => sprintf('Order %s has not departed within the configured grace period.', $this->order->tracking ?? $this->order->public_id),
46
+ ];
47
+ }
48
+ }
@@ -0,0 +1,48 @@
1
+ <?php
2
+
3
+ namespace Fleetbase\FleetOps\Notifications;
4
+
5
+ use Fleetbase\FleetOps\Models\Order;
6
+ use Illuminate\Bus\Queueable;
7
+ use Illuminate\Contracts\Queue\ShouldQueue;
8
+ use Illuminate\Notifications\Messages\MailMessage;
9
+ use Illuminate\Notifications\Notification;
10
+
11
+ class ProlongedStoppage extends Notification implements ShouldQueue
12
+ {
13
+ use Queueable;
14
+
15
+ public static string $name = 'Prolonged Stoppage';
16
+ public static string $description = 'Notify when a vehicle remains stopped during an active trip beyond the configured threshold.';
17
+ public static string $package = 'fleet-ops';
18
+
19
+ public function __construct(public Order $order, public array $context = [])
20
+ {
21
+ }
22
+
23
+ public function via($notifiable): array
24
+ {
25
+ return ['mail', 'database'];
26
+ }
27
+
28
+ public function toMail($notifiable): MailMessage
29
+ {
30
+ $orderId = $this->order->tracking ?? $this->order->public_id;
31
+
32
+ return (new MailMessage())
33
+ ->subject("Prolonged stoppage detected for {$orderId}")
34
+ ->line("The vehicle assigned to order {$orderId} has remained stopped beyond the configured threshold.")
35
+ ->line('Please review the active trip and vehicle location.');
36
+ }
37
+
38
+ public function toArray($notifiable): array
39
+ {
40
+ return [
41
+ 'event' => 'order.prolonged_stoppage',
42
+ 'order_id' => $this->order->public_id,
43
+ 'order_uuid' => $this->order->uuid,
44
+ 'context' => $this->context,
45
+ 'message' => sprintf('The vehicle assigned to order %s has remained stopped beyond the configured threshold.', $this->order->tracking ?? $this->order->public_id),
46
+ ];
47
+ }
48
+ }
@@ -0,0 +1,48 @@
1
+ <?php
2
+
3
+ namespace Fleetbase\FleetOps\Notifications;
4
+
5
+ use Fleetbase\FleetOps\Models\Order;
6
+ use Illuminate\Bus\Queueable;
7
+ use Illuminate\Contracts\Queue\ShouldQueue;
8
+ use Illuminate\Notifications\Messages\MailMessage;
9
+ use Illuminate\Notifications\Notification;
10
+
11
+ class RouteDeviation extends Notification implements ShouldQueue
12
+ {
13
+ use Queueable;
14
+
15
+ public static string $name = 'Route Deviation';
16
+ public static string $description = 'Notify when tracked vehicle location deviates from the active route beyond the configured threshold.';
17
+ public static string $package = 'fleet-ops';
18
+
19
+ public function __construct(public Order $order, public array $context = [])
20
+ {
21
+ }
22
+
23
+ public function via($notifiable): array
24
+ {
25
+ return ['mail', 'database'];
26
+ }
27
+
28
+ public function toMail($notifiable): MailMessage
29
+ {
30
+ $orderId = $this->order->tracking ?? $this->order->public_id;
31
+
32
+ return (new MailMessage())
33
+ ->subject("Route deviation detected for {$orderId}")
34
+ ->line("The vehicle assigned to order {$orderId} has deviated from the active route beyond the configured threshold.")
35
+ ->line('Please review the route and current vehicle position.');
36
+ }
37
+
38
+ public function toArray($notifiable): array
39
+ {
40
+ return [
41
+ 'event' => 'order.route_deviation',
42
+ 'order_id' => $this->order->public_id,
43
+ 'order_uuid' => $this->order->uuid,
44
+ 'context' => $this->context,
45
+ 'message' => sprintf('The vehicle assigned to order %s has deviated from the active route beyond the configured threshold.', $this->order->tracking ?? $this->order->public_id),
46
+ ];
47
+ }
48
+ }
@@ -171,12 +171,17 @@ class VroomOrchestrationEngine implements OrchestrationEngineInterface
171
171
  $vroomPayload = $this->buildCapacityOnlyPayload($capacityTasks, $vroomVehicles, $options, $jobIdReverse);
172
172
 
173
173
  if (empty($vroomPayload['vehicles'])) {
174
+ $vehiclePacking = $this->resolveVehiclePacking($options);
175
+ $vehicleFixedCost = $this->resolveVehicleFixedCost($options);
176
+
174
177
  return $this->mergeInvalidTasks([
175
178
  'assignments' => [],
176
179
  'unassigned' => array_column($capacityTasks, 'id'),
177
180
  'summary' => [
178
181
  'engine' => 'vroom',
179
182
  'allocation_strategy' => 'capacity_only',
183
+ 'vehicle_packing' => $vehiclePacking,
184
+ 'vehicle_fixed_cost' => $vehiclePacking === 'minimize_vehicles' ? $vehicleFixedCost : null,
180
185
  'assigned' => 0,
181
186
  'unassigned' => count($capacityTasks),
182
187
  'unassigned_reasons' => array_map(fn (array $task) => [
@@ -192,6 +197,8 @@ class VroomOrchestrationEngine implements OrchestrationEngineInterface
192
197
  $result['summary'] = array_merge($result['summary'] ?? [], [
193
198
  'engine' => 'vroom',
194
199
  'allocation_strategy' => 'capacity_only',
200
+ 'vehicle_packing' => $this->resolveVehiclePacking($options),
201
+ 'vehicle_fixed_cost' => $this->resolveVehiclePacking($options) === 'minimize_vehicles' ? $this->resolveVehicleFixedCost($options) : null,
195
202
  ]);
196
203
 
197
204
  return $this->mergeInvalidTasks($result, $invalidTasks);
@@ -203,10 +210,12 @@ class VroomOrchestrationEngine implements OrchestrationEngineInterface
203
210
  $profile = $options['profile'] ?? 'capacity_only';
204
211
  $respectCapacity = (bool) ($options['respect_capacity'] ?? true);
205
212
  $respectSkills = (bool) ($options['respect_skills'] ?? true);
213
+ $vehiclePacking = $this->resolveVehiclePacking($options);
214
+ $fixedCost = $this->resolveVehicleFixedCost($options);
206
215
 
207
216
  $vroomPayload = [
208
217
  'jobs' => [],
209
- 'vehicles' => array_map(function (array $v) use ($profile, $respectCapacity, $respectSkills) {
218
+ 'vehicles' => array_map(function (array $v) use ($profile, $respectCapacity, $respectSkills, $vehiclePacking, $fixedCost) {
210
219
  $vehicle = [
211
220
  'id' => crc32($v['id']),
212
221
  'description' => json_encode(['vehicle_id' => $v['id'], 'driver_id' => $v['driver_id'] ?? null]),
@@ -214,6 +223,10 @@ class VroomOrchestrationEngine implements OrchestrationEngineInterface
214
223
  'start_index' => 0,
215
224
  ];
216
225
 
226
+ if ($vehiclePacking === 'minimize_vehicles') {
227
+ $vehicle['costs'] = ['fixed' => $fixedCost];
228
+ }
229
+
217
230
  if ($respectCapacity) {
218
231
  $vehicle['capacity'] = $v['capacity'];
219
232
  }
@@ -265,6 +278,20 @@ class VroomOrchestrationEngine implements OrchestrationEngineInterface
265
278
  return $vroomPayload;
266
279
  }
267
280
 
281
+ protected function resolveVehiclePacking(array $options): string
282
+ {
283
+ $packing = strtolower((string) ($options['vehicle_packing'] ?? 'minimize_vehicles'));
284
+
285
+ return in_array($packing, ['minimize_vehicles', 'balanced', 'none'], true)
286
+ ? $packing
287
+ : 'minimize_vehicles';
288
+ }
289
+
290
+ protected function resolveVehicleFixedCost(array $options): int
291
+ {
292
+ return max(0, (int) ($options['vehicle_fixed_cost'] ?? 100000));
293
+ }
294
+
268
295
  protected function buildUniformMatrix(int $size): array
269
296
  {
270
297
  $matrix = [];
@@ -65,6 +65,7 @@ class FleetOpsServiceProvider extends CoreServiceProvider
65
65
  \Fleetbase\FleetOps\Console\Commands\TestEmail::class,
66
66
  \Fleetbase\FleetOps\Console\Commands\ProcessMaintenanceTriggers::class,
67
67
  \Fleetbase\FleetOps\Console\Commands\SendMaintenanceReminders::class,
68
+ \Fleetbase\FleetOps\Console\Commands\ProcessOperationalAlerts::class,
68
69
  ];
69
70
 
70
71
  /**
@@ -126,6 +127,7 @@ class FleetOpsServiceProvider extends CoreServiceProvider
126
127
  $schedule->command('fleetops:purge-service-quotes')->daily()->withoutOverlapping();
127
128
  $schedule->command('fleetops:process-maintenance-triggers')->daily()->withoutOverlapping()->storeOutputInDb();
128
129
  $schedule->command('fleetops:send-maintenance-reminders')->daily()->withoutOverlapping()->storeOutputInDb();
130
+ $schedule->command('fleetops:process-operational-alerts')->everyMinute()->withoutOverlapping()->storeOutputInDb();
129
131
  });
130
132
  $this->registerNotifications();
131
133
  $this->registerExpansionsFrom(__DIR__ . '/../Expansions');
@@ -192,6 +194,9 @@ class FleetOpsServiceProvider extends CoreServiceProvider
192
194
  \Fleetbase\FleetOps\Notifications\OrderFailed::class,
193
195
  \Fleetbase\FleetOps\Notifications\OrderCompleted::class,
194
196
  \Fleetbase\FleetOps\Notifications\DriverArrivedAtGeofence::class,
197
+ \Fleetbase\FleetOps\Notifications\LateDeparture::class,
198
+ \Fleetbase\FleetOps\Notifications\RouteDeviation::class,
199
+ \Fleetbase\FleetOps\Notifications\ProlongedStoppage::class,
195
200
  ]);
196
201
 
197
202
  // Register Notifiables
@@ -31,6 +31,9 @@ class NotificationServiceProvider extends CoreServiceProvider
31
31
  \Fleetbase\FleetOps\Notifications\OrderDispatched::class,
32
32
  \Fleetbase\FleetOps\Notifications\OrderDispatchFailed::class,
33
33
  \Fleetbase\FleetOps\Notifications\OrderPing::class,
34
+ \Fleetbase\FleetOps\Notifications\LateDeparture::class,
35
+ \Fleetbase\FleetOps\Notifications\RouteDeviation::class,
36
+ \Fleetbase\FleetOps\Notifications\ProlongedStoppage::class,
34
37
  ]);
35
38
 
36
39
  // Register Notifiables
@@ -0,0 +1,77 @@
1
+ <?php
2
+
3
+ namespace Fleetbase\FleetOps\Support\Analytics;
4
+
5
+ use Fleetbase\Models\Company;
6
+ use Illuminate\Support\Carbon;
7
+
8
+ /**
9
+ * Base for analytics-widget data providers. Unlike scalar Metrics, each
10
+ * subclass returns a widget-specific JSON shape (Chart.js datasets, ranked
11
+ * rows, scalar tiles, map points, etc.) so the base just provides company
12
+ * scoping + period parsing.
13
+ */
14
+ abstract class AbstractAnalytics
15
+ {
16
+ protected Company $company;
17
+ protected ?\DateTimeInterface $start = null;
18
+ protected ?\DateTimeInterface $end = null;
19
+
20
+ abstract public function get(): array;
21
+
22
+ public static function forCompany(Company $company): static
23
+ {
24
+ return (new static())->setCompany($company);
25
+ }
26
+
27
+ public function between(?\DateTimeInterface $start, ?\DateTimeInterface $end): static
28
+ {
29
+ $this->start = $start;
30
+ $this->end = $end;
31
+
32
+ return $this;
33
+ }
34
+
35
+ /**
36
+ * Resolve a [start, end] tuple from a `period` shorthand or explicit dates,
37
+ * falling back to a 30-day window. Shared by every analytics call.
38
+ */
39
+ public static function resolvePeriod(?string $period, ?\DateTimeInterface $start, ?\DateTimeInterface $end, int $defaultDays = 30): array
40
+ {
41
+ if ($period !== null && $period !== '') {
42
+ $days = match ($period) {
43
+ '7d' => 7,
44
+ '14d' => 14,
45
+ '30d' => 30,
46
+ '90d' => 90,
47
+ '180d' => 180,
48
+ '365d' => 365,
49
+ default => null,
50
+ };
51
+
52
+ if ($days !== null) {
53
+ return [
54
+ Carbon::now()->subDays($days)->toDateTime(),
55
+ Carbon::now()->toDateTime(),
56
+ ];
57
+ }
58
+ }
59
+
60
+ return [
61
+ $start ?? Carbon::now()->subDays($defaultDays)->toDateTime(),
62
+ $end ?? Carbon::now()->toDateTime(),
63
+ ];
64
+ }
65
+
66
+ protected function setCompany(Company $company): static
67
+ {
68
+ $this->company = $company;
69
+
70
+ return $this;
71
+ }
72
+
73
+ protected function companyCurrency(): string
74
+ {
75
+ return $this->company->currency ?? 'USD';
76
+ }
77
+ }
@@ -0,0 +1,83 @@
1
+ <?php
2
+
3
+ namespace Fleetbase\FleetOps\Support\Analytics;
4
+
5
+ use Fleetbase\FleetOps\Models\FuelReport;
6
+ use Fleetbase\FleetOps\Models\Order;
7
+ use Illuminate\Support\Carbon;
8
+
9
+ /**
10
+ * Dual-axis fuel chart: weekly total cost (bars) + cost-per-km (line).
11
+ * Cost-per-km is sum(FuelReport.amount in primary currency) divided by
12
+ * sum(Order.distance, in km) for that bucket.
13
+ */
14
+ class FuelEfficiency extends AbstractAnalytics
15
+ {
16
+ public function get(): array
17
+ {
18
+ $currency = $this->companyCurrency();
19
+ $start = $this->start ?? Carbon::now()->subDays(90)->toDateTime();
20
+ $end = $this->end ?? Carbon::now()->toDateTime();
21
+
22
+ $fuelByWeek = FuelReport::where('company_uuid', $this->company->uuid)
23
+ ->where('currency', $currency)
24
+ ->whereBetween('created_at', [$start, $end])
25
+ ->selectRaw("YEARWEEK(created_at, 1) as wk, MIN(DATE(created_at)) as wk_start, SUM(amount) as total_cost")
26
+ ->groupBy('wk')
27
+ ->orderBy('wk')
28
+ ->get();
29
+
30
+ $distanceByWeek = Order::where('company_uuid', $this->company->uuid)
31
+ ->where('status', 'completed')
32
+ ->whereBetween('updated_at', [$start, $end])
33
+ ->selectRaw("YEARWEEK(updated_at, 1) as wk, SUM(distance) as total_distance")
34
+ ->groupBy('wk')
35
+ ->orderBy('wk')
36
+ ->pluck('total_distance', 'wk');
37
+
38
+ $labels = [];
39
+ $costData = [];
40
+ $cpkmData = [];
41
+
42
+ foreach ($fuelByWeek as $row) {
43
+ $labels[] = Carbon::parse($row->wk_start)->format('M j');
44
+ $cost = (float) $row->total_cost;
45
+ $distanceM = (float) ($distanceByWeek[$row->wk] ?? 0);
46
+ $distanceKm = $distanceM / 1000.0;
47
+ $costData[] = round($cost, 2);
48
+ $cpkmData[] = $distanceKm > 0 ? round($cost / $distanceKm, 3) : null;
49
+ }
50
+
51
+ $totalCost = array_sum($costData);
52
+ $totalDistanceM = array_sum($distanceByWeek->all());
53
+ $avgCostPerKm = $totalDistanceM > 0
54
+ ? round($totalCost / ($totalDistanceM / 1000.0), 3)
55
+ : 0.0;
56
+
57
+ return [
58
+ 'labels' => $labels,
59
+ 'datasets' => [
60
+ [
61
+ 'type' => 'bar',
62
+ 'label' => 'Fuel Cost',
63
+ 'data' => $costData,
64
+ 'yAxisID' => 'y1',
65
+ 'backgroundColor' => '#3485e2',
66
+ ],
67
+ [
68
+ 'type' => 'line',
69
+ 'label' => 'Cost per km',
70
+ 'data' => $cpkmData,
71
+ 'yAxisID' => 'y2',
72
+ 'borderColor' => '#f59e0b',
73
+ 'tension' => 0.3,
74
+ ],
75
+ ],
76
+ 'summary' => [
77
+ 'total_cost' => round($totalCost, 2),
78
+ 'currency' => $currency,
79
+ 'avg_cost_per_km' => $avgCostPerKm,
80
+ ],
81
+ ];
82
+ }
83
+ }