@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.
- package/addon/components/driver/details.hbs +5 -1
- package/addon/components/driver/form.hbs +11 -1
- package/addon/components/fleet-listing-panel.hbs +2 -2
- package/addon/components/fleet-listing-panel.js +10 -0
- package/addon/components/map/google-live-map.js +12 -73
- package/addon/components/map/leaflet-live-map.hbs +149 -33
- package/addon/components/orchestrator/phase-builder.hbs +21 -0
- package/addon/components/orchestrator/phase-builder.js +13 -0
- package/addon/components/orchestrator-workbench.js +1 -0
- package/addon/components/order/form.hbs +2 -2
- package/addon/components/widget/fleet-ops-key-metrics.hbs +19 -17
- package/addon/components/widget/fleet-ops-key-metrics.js +26 -0
- package/addon/components/widget/fuel-efficiency.hbs +26 -0
- package/addon/components/widget/fuel-efficiency.js +43 -0
- package/addon/components/widget/geofence-violations.hbs +44 -0
- package/addon/components/widget/geofence-violations.js +55 -0
- package/addon/components/widget/issues-insights.hbs +43 -0
- package/addon/components/widget/issues-insights.js +62 -0
- package/addon/components/widget/kpi-active-orders.hbs +10 -0
- package/addon/components/widget/kpi-active-orders.js +5 -0
- package/addon/components/widget/kpi-aov.hbs +10 -0
- package/addon/components/widget/kpi-aov.js +5 -0
- package/addon/components/widget/kpi-distance.hbs +10 -0
- package/addon/components/widget/kpi-distance.js +5 -0
- package/addon/components/widget/kpi-drivers-online.hbs +10 -0
- package/addon/components/widget/kpi-drivers-online.js +5 -0
- package/addon/components/widget/kpi-earnings.hbs +10 -0
- package/addon/components/widget/kpi-earnings.js +5 -0
- package/addon/components/widget/kpi-open-issues.hbs +11 -0
- package/addon/components/widget/kpi-open-issues.js +5 -0
- package/addon/components/widget/kpi-tile.hbs +59 -0
- package/addon/components/widget/kpi-tile.js +168 -0
- package/addon/components/widget/live-fleet.hbs +122 -0
- package/addon/components/widget/live-fleet.js +365 -0
- package/addon/components/widget/maintenance-overview.hbs +45 -0
- package/addon/components/widget/maintenance-overview.js +40 -0
- package/addon/components/widget/on-time-delivery.hbs +27 -0
- package/addon/components/widget/on-time-delivery.js +73 -0
- package/addon/components/widget/operations-pulse.hbs +51 -0
- package/addon/components/widget/operations-pulse.js +173 -0
- package/addon/components/widget/orders-by-status.hbs +26 -0
- package/addon/components/widget/orders-by-status.js +54 -0
- package/addon/components/widget/revenue-trend.hbs +34 -0
- package/addon/components/widget/revenue-trend.js +67 -0
- package/addon/components/widget/top-drivers.hbs +44 -0
- package/addon/components/widget/top-drivers.js +54 -0
- package/addon/controllers/settings/routing.js +26 -0
- package/addon/extension.js +172 -3
- package/addon/services/map-adapter/google.js +13 -2
- package/addon/styles/fleetops-engine.css +114 -87
- package/addon/templates/maintenance/equipment/index.hbs +1 -1
- package/addon/templates/maintenance/parts/index.hbs +1 -1
- package/addon/templates/maintenance/schedules/index.hbs +1 -1
- package/addon/templates/management/vehicles/index.hbs +1 -1
- package/addon/templates/settings/avatars.hbs +1 -1
- package/addon/templates/settings/custom-fields.hbs +1 -1
- package/addon/templates/settings/map.hbs +1 -1
- package/addon/templates/settings/navigator-app.hbs +1 -1
- package/addon/templates/settings/notifications.hbs +1 -1
- package/addon/templates/settings/orchestrator.hbs +1 -1
- package/addon/templates/settings/payments/index.hbs +1 -1
- package/addon/templates/settings/payments/onboard.hbs +1 -1
- package/addon/templates/settings/routing.hbs +22 -2
- package/addon/templates/settings/scheduling.hbs +1 -1
- package/addon/utils/live-map-card-content.js +171 -0
- package/app/components/widget/fuel-efficiency.js +1 -0
- package/app/components/widget/geofence-violations.js +1 -0
- package/app/components/widget/issues-insights.js +1 -0
- package/app/components/widget/kpi-active-orders.js +1 -0
- package/app/components/widget/kpi-aov.js +1 -0
- package/app/components/widget/kpi-distance.js +1 -0
- package/app/components/widget/kpi-drivers-online.js +1 -0
- package/app/components/widget/kpi-earnings.js +1 -0
- package/app/components/widget/kpi-open-issues.js +1 -0
- package/app/components/widget/kpi-tile.js +1 -0
- package/app/components/widget/live-fleet.js +1 -0
- package/app/components/widget/maintenance-overview.js +1 -0
- package/app/components/widget/on-time-delivery.js +1 -0
- package/app/components/widget/operations-pulse.js +1 -0
- package/app/components/widget/orders-by-status.js +1 -0
- package/app/components/widget/revenue-trend.js +1 -0
- package/app/components/widget/top-drivers.js +1 -0
- package/app/utils/live-map-card-content.js +1 -0
- package/composer.json +1 -1
- package/extension.json +1 -1
- package/package.json +3 -3
- package/pnpm-workspace.yaml +1 -0
- package/server/config/telematics.php +111 -1
- package/server/migrations/2026_05_18_000001_add_license_expiry_to_drivers_table.php +25 -0
- package/server/seeders/Testing/Concerns/SeedsTestingData.php +183 -0
- package/server/seeders/Testing/ConnectivitySeeder.php +212 -0
- package/server/seeders/Testing/FleetSeeder.php +341 -0
- package/server/seeders/Testing/MaintenanceSeeder.php +238 -0
- package/server/seeders/Testing/NetworkSeeder.php +250 -0
- package/server/seeders/Testing/OrdersSeeder.php +214 -0
- package/server/seeders/Testing/TestingSeeder.php +40 -0
- package/server/seeders/{VroomOrchestrationSeeder.php → Testing/VroomOrchestrationSeeder.php} +8 -1
- package/server/src/Console/Commands/ProcessOperationalAlerts.php +279 -0
- package/server/src/Http/Controllers/Internal/v1/AnalyticsController.php +119 -0
- package/server/src/Http/Controllers/Internal/v1/MetricsController.php +82 -3
- package/server/src/Http/Controllers/Internal/v1/OrderController.php +12 -6
- package/server/src/Http/Controllers/Internal/v1/SettingController.php +46 -1
- package/server/src/Http/Resources/v1/Driver.php +2 -0
- package/server/src/Http/Resources/v1/Fleet.php +4 -0
- package/server/src/Http/Resources/v1/Index/Driver.php +48 -0
- package/server/src/Http/Resources/v1/Index/Vehicle.php +74 -1
- package/server/src/Models/Driver.php +2 -0
- package/server/src/Models/OrderConfig.php +14 -11
- package/server/src/Notifications/LateDeparture.php +48 -0
- package/server/src/Notifications/ProlongedStoppage.php +48 -0
- package/server/src/Notifications/RouteDeviation.php +48 -0
- package/server/src/Orchestration/Engines/VroomOrchestrationEngine.php +28 -1
- package/server/src/Providers/FleetOpsServiceProvider.php +5 -0
- package/server/src/Providers/NotificationServiceProvider.php +3 -0
- package/server/src/Support/Analytics/AbstractAnalytics.php +77 -0
- package/server/src/Support/Analytics/FuelEfficiency.php +83 -0
- package/server/src/Support/Analytics/GeofenceViolations.php +63 -0
- package/server/src/Support/Analytics/IssuesInsights.php +67 -0
- package/server/src/Support/Analytics/LiveFleet.php +127 -0
- package/server/src/Support/Analytics/MaintenanceOverview.php +70 -0
- package/server/src/Support/Analytics/OnTimeDelivery.php +75 -0
- package/server/src/Support/Analytics/OperationsPulse.php +86 -0
- package/server/src/Support/Analytics/OrdersByStatus.php +64 -0
- package/server/src/Support/Analytics/RevenueTrend.php +79 -0
- package/server/src/Support/Analytics/TopDrivers.php +92 -0
- package/server/src/Support/Metrics/AbstractMetric.php +170 -0
- package/server/src/Support/Metrics/ActiveLiveOrdersMetric.php +28 -0
- package/server/src/Support/Metrics/AvgOrderValueMetric.php +53 -0
- package/server/src/Support/Metrics/DriversOnlineMetric.php +30 -0
- package/server/src/Support/Metrics/EarningsMetric.php +30 -0
- package/server/src/Support/Metrics/FuelCostsMetric.php +30 -0
- package/server/src/Support/Metrics/MoneyMetric.php +30 -0
- package/server/src/Support/Metrics/OpenIssuesMetric.php +35 -0
- package/server/src/Support/Metrics/OrdersCanceledMetric.php +35 -0
- package/server/src/Support/Metrics/OrdersCompletedMetric.php +35 -0
- package/server/src/Support/Metrics/OrdersInProgressMetric.php +51 -0
- package/server/src/Support/Metrics/OrdersScheduledMetric.php +37 -0
- package/server/src/Support/Metrics/Registry.php +46 -0
- package/server/src/Support/Metrics/ResolvedIssuesMetric.php +35 -0
- package/server/src/Support/Metrics/TotalCustomersMetric.php +29 -0
- package/server/src/Support/Metrics/TotalDistanceTraveledMetric.php +35 -0
- package/server/src/Support/Metrics/TotalDriversMetric.php +28 -0
- package/server/src/Support/Metrics/TotalTimeTraveledMetric.php +35 -0
- package/server/src/Support/Metrics.php +46 -249
- package/server/src/Support/Telematics/Providers/AfaqyProvider.php +290 -0
- package/server/src/Support/Telematics/Providers/SafeeProvider.php +333 -0
- package/server/src/routes.php +16 -0
- package/server/tests/AnalyticsRoutesTest.php +41 -0
- package/server/tests/MetricsRegistryTest.php +88 -0
- package/server/tests/VroomOrchestrationEngineTest.php +91 -0
- 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'
|
|
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
|
-
$
|
|
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('
|
|
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
|
|
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
|
+
}
|