@fleetbase/fleetops-engine 0.6.19 → 0.6.21
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/custom-entity/form.hbs +14 -14
- package/addon/components/device/details.hbs +92 -43
- package/addon/components/device/form.hbs +108 -60
- package/addon/components/device/form.js +36 -8
- package/addon/components/device/panel-header.hbs +32 -0
- package/addon/components/device/panel-header.js +3 -0
- package/addon/components/driver/form.hbs +1 -1
- package/addon/components/driver/form.js +49 -47
- package/addon/components/entity/form.hbs +7 -5
- package/addon/components/layout/fleet-ops-sidebar.js +12 -12
- package/addon/components/map/drawer/device-event-listing.hbs +58 -0
- package/addon/components/map/drawer/device-event-listing.js +181 -0
- package/addon/components/map/drawer/position-listing.hbs +84 -0
- package/addon/components/map/drawer/position-listing.js +289 -0
- package/addon/components/map/drawer.js +2 -0
- package/addon/components/map/leaflet-live-map.hbs +7 -2
- package/addon/components/order/details/payload.hbs +6 -4
- package/addon/components/order/details/payload.js +2 -0
- package/addon/components/order/kanban.hbs +12 -10
- package/addon/components/order/kanban.js +27 -3
- package/addon/components/order-config-manager/custom-fields.js +1 -1
- package/addon/components/positions-replay.hbs +333 -0
- package/addon/components/positions-replay.js +372 -0
- package/addon/components/sensor/details.hbs +64 -38
- package/addon/components/sensor/form.hbs +112 -63
- package/addon/components/sensor/form.js +36 -24
- package/addon/components/sensor/panel-header.hbs +32 -0
- package/addon/components/sensor/panel-header.js +3 -0
- package/addon/components/telematic/details.hbs +40 -16
- package/addon/components/telematic/form.hbs +63 -64
- package/addon/components/telematic/form.js +73 -4
- package/addon/components/vehicle/card.hbs +1 -1
- package/addon/controllers/analytics/reports/index/edit.js +1 -1
- package/addon/controllers/connectivity/devices/index/details.js +22 -1
- package/addon/controllers/connectivity/devices/index/edit.js +66 -1
- package/addon/controllers/connectivity/devices/index.js +51 -9
- package/addon/controllers/connectivity/events/index.js +65 -16
- package/addon/controllers/connectivity/sensors/index/details.js +22 -1
- package/addon/controllers/connectivity/sensors/index/edit.js +66 -1
- package/addon/controllers/connectivity/sensors/index.js +66 -6
- package/addon/controllers/connectivity/telematics/index/details.js +22 -1
- package/addon/controllers/connectivity/telematics/index/edit.js +66 -1
- package/addon/controllers/connectivity/telematics/index.js +20 -11
- package/addon/controllers/management/fleets/index/details.js +26 -21
- package/addon/controllers/management/fleets/index/edit.js +9 -6
- package/addon/controllers/management/vehicles/index/details.js +21 -13
- package/addon/controllers/operations/orders/index/new.js +4 -2
- package/addon/controllers/operations/orders/index.js +50 -45
- package/addon/controllers/settings/custom-fields.js +6 -0
- package/addon/helpers/get-fleet-ops-option-label.js +11 -0
- package/addon/routes/connectivity/devices/index/details.js +27 -1
- package/addon/routes/connectivity/devices/index/edit.js +27 -1
- package/addon/routes/connectivity/sensors/index/details.js +27 -1
- package/addon/routes/connectivity/sensors/index/edit.js +27 -1
- package/addon/routes/connectivity/telematics/index/details.js +27 -1
- package/addon/routes/connectivity/telematics/index/edit.js +27 -1
- package/addon/routes/management/vehicles/index/details/positions.js +3 -0
- package/addon/routes/operations/orders/index.js +0 -3
- package/addon/routes.js +1 -0
- package/addon/services/movement-tracker.js +81 -30
- package/addon/services/order-creation.js +4 -8
- package/addon/services/order-validation.js +3 -3
- package/addon/styles/fleetops-engine.css +192 -0
- package/addon/templates/connectivity/devices/index/details/index.hbs +2 -2
- package/addon/templates/connectivity/devices/index/details.hbs +15 -2
- package/addon/templates/connectivity/devices/index/edit.hbs +1 -1
- package/addon/templates/connectivity/events/index.hbs +1 -1
- package/addon/templates/connectivity/sensors/index/details/index.hbs +2 -2
- package/addon/templates/connectivity/sensors/index/details.hbs +15 -2
- package/addon/templates/connectivity/sensors/index/edit.hbs +1 -1
- package/addon/templates/connectivity/telematics/index/details/index.hbs +2 -2
- package/addon/templates/connectivity/telematics/index/details.hbs +14 -2
- package/addon/templates/connectivity/telematics/index/edit.hbs +1 -1
- package/addon/templates/management/vehicles/index/details/positions.hbs +1 -0
- package/addon/templates/operations/orders/index.hbs +26 -2
- package/addon/utils/fleet-ops-options.js +95 -0
- package/addon/utils/setup-customer-portal.js +7 -0
- package/app/components/device/panel-header.js +1 -0
- package/app/components/map/drawer/device-event-listing.js +1 -0
- package/app/components/map/drawer/position-listing.js +1 -0
- package/app/components/positions-replay.js +1 -0
- package/app/components/sensor/panel-header.js +1 -0
- package/app/helpers/get-fleet-ops-option-label.js +1 -0
- package/app/routes/management/vehicles/index/details/positions.js +1 -0
- package/app/templates/management/vehicles/index/details/positions.js +1 -0
- package/composer.json +1 -1
- package/extension.json +1 -1
- package/package.json +4 -4
- package/server/config/telematics.php +111 -0
- package/server/migrations/2025_10_27_000001_add_telematics_integration_fields.php +70 -0
- package/server/migrations/2025_10_27_171322_fix_device_column_names.php +107 -0
- package/server/migrations/2025_10_27_203023_add_company_uuid_to_device_events_table.php +28 -0
- package/server/src/Console/Commands/ReplayVehicleLocations.php +225 -0
- package/server/src/Contracts/TelematicProviderDescriptor.php +72 -0
- package/server/src/Contracts/TelematicProviderInterface.php +119 -0
- package/server/src/Exceptions/TelematicProviderException.php +14 -0
- package/server/src/Exceptions/TelematicRateLimitExceededException.php +12 -0
- package/server/src/Http/Controllers/Api/v1/DriverController.php +24 -14
- package/server/src/Http/Controllers/Api/v1/VehicleController.php +27 -7
- package/server/src/Http/Controllers/Internal/v1/DeviceController.php +22 -0
- package/server/src/Http/Controllers/Internal/v1/OrderController.php +50 -68
- package/server/src/Http/Controllers/Internal/v1/PositionController.php +240 -0
- package/server/src/Http/Controllers/Internal/v1/SensorController.php +11 -0
- package/server/src/Http/Controllers/Internal/v1/TelematicController.php +141 -0
- package/server/src/Http/Controllers/Internal/v1/TelematicWebhookController.php +170 -0
- package/server/src/Http/Filter/DeviceEventFilter.php +68 -0
- package/server/src/Http/Filter/PositionFilter.php +35 -0
- package/server/src/Http/Resources/v1/Position.php +44 -0
- package/server/src/Jobs/ReplayPositions.php +64 -0
- package/server/src/Jobs/SendPositionReplay.php +65 -0
- package/server/src/Jobs/SyncTelematicDevicesJob.php +106 -0
- package/server/src/Jobs/TestTelematicConnectionJob.php +102 -0
- package/server/src/Models/Device.php +72 -10
- package/server/src/Models/DeviceEvent.php +7 -0
- package/server/src/Models/Driver.php +28 -1
- package/server/src/Models/Payload.php +11 -3
- package/server/src/Models/Place.php +9 -2
- package/server/src/Models/Position.php +17 -17
- package/server/src/Models/Sensor.php +78 -13
- package/server/src/Models/Telematic.php +116 -6
- package/server/src/Models/Vehicle.php +104 -1
- package/server/src/Providers/FleetOpsServiceProvider.php +2 -0
- package/server/src/Support/Telematics/Providers/AbstractProvider.php +151 -0
- package/server/src/Support/Telematics/Providers/FlespiProvider.php +182 -0
- package/server/src/Support/Telematics/Providers/GeotabProvider.php +181 -0
- package/server/src/Support/Telematics/Providers/SamsaraProvider.php +177 -0
- package/server/src/Support/Telematics/TelematicProviderRegistry.php +147 -0
- package/server/src/Support/Telematics/TelematicService.php +223 -0
- package/server/src/Support/Utils.php +1 -1
- package/server/src/routes.php +12 -1
|
@@ -774,55 +774,30 @@ class OrderController extends FleetOpsController
|
|
|
774
774
|
}
|
|
775
775
|
|
|
776
776
|
/**
|
|
777
|
-
*
|
|
778
|
-
*
|
|
779
|
-
* This endpoint compiles:
|
|
780
|
-
* 1. All unique `status` values from the `orders` table for the current company.
|
|
781
|
-
* 2. Optionally, all `Activity` codes defined in `OrderConfig` records that are
|
|
782
|
-
* actually referenced by existing orders (via `order_config_uuid`).
|
|
783
|
-
*
|
|
784
|
-
* ---
|
|
785
|
-
* ### Query Parameters
|
|
786
|
-
* - `include_order_config_activities` (bool, optional)
|
|
787
|
-
* When true, includes `Activity` codes from relevant `OrderConfig` instances.
|
|
788
|
-
*
|
|
789
|
-
* - `order_config_key` (string, optional)
|
|
790
|
-
* Restricts both `orders` and `order_configs` to a specific configuration key.
|
|
791
|
-
*
|
|
792
|
-
* ---
|
|
793
|
-
* ### Behavior
|
|
794
|
-
* - Only includes order configs that are actually used by orders.
|
|
795
|
-
* - Uses the `activities()` method on each `OrderConfig` to extract all activity codes.
|
|
796
|
-
* - Merges and deduplicates both order statuses and activity codes.
|
|
797
|
-
*
|
|
798
|
-
* ---
|
|
799
|
-
* ### Example Response
|
|
800
|
-
* ```json
|
|
801
|
-
* [
|
|
802
|
-
* "created",
|
|
803
|
-
* "dispatched",
|
|
804
|
-
* "completed",
|
|
805
|
-
* "canceled",
|
|
806
|
-
* "pickup_ready",
|
|
807
|
-
* "awaiting_payment"
|
|
808
|
-
* ]
|
|
809
|
-
* ```
|
|
810
|
-
*
|
|
811
|
-
* @return \Illuminate\Http\JsonResponse
|
|
777
|
+
* Return distinct order statuses (and optionally activity codes) for a company,
|
|
778
|
+
* filtered by order_config_uuid or order_config_key if provided.
|
|
812
779
|
*/
|
|
813
780
|
public function statuses(Request $request)
|
|
814
781
|
{
|
|
815
|
-
$companyUuid
|
|
816
|
-
|
|
782
|
+
$companyUuid = $request->user()->company_uuid ?? session('company');
|
|
817
783
|
$includeActivities = $request->boolean('include_order_config_activities', true);
|
|
818
|
-
$orderConfigKey = trim((string) $request->string('order_config_key'));
|
|
819
784
|
|
|
820
|
-
//
|
|
785
|
+
// Use input() + trim to get plain strings (Request::string() returns Stringable in newer Laravel)
|
|
786
|
+
$orderConfigKey = trim((string) $request->input('order_config_key', ''));
|
|
787
|
+
$orderConfigId = trim((string) $request->input('order_config_uuid', ''));
|
|
788
|
+
|
|
789
|
+
// ---------------------------
|
|
790
|
+
// Build base orders query
|
|
791
|
+
// ---------------------------
|
|
821
792
|
$ordersQuery = DB::table('orders')
|
|
822
793
|
->where('company_uuid', $companyUuid)
|
|
823
|
-
->whereNotNull('status')
|
|
794
|
+
->whereNotNull('status')
|
|
795
|
+
->whereNull('deleted_at');
|
|
824
796
|
|
|
825
|
-
|
|
797
|
+
// Prefer filtering by UUID (most precise), else by key
|
|
798
|
+
if ($orderConfigId !== '') {
|
|
799
|
+
$ordersQuery->where('order_config_uuid', $orderConfigId);
|
|
800
|
+
} elseif ($orderConfigKey !== '') {
|
|
826
801
|
$ordersQuery->whereExists(function ($q) use ($companyUuid, $orderConfigKey) {
|
|
827
802
|
$q->select(DB::raw(1))
|
|
828
803
|
->from('order_configs as oc')
|
|
@@ -832,34 +807,38 @@ class OrderController extends FleetOpsController
|
|
|
832
807
|
});
|
|
833
808
|
}
|
|
834
809
|
|
|
810
|
+
// Distinct order statuses
|
|
835
811
|
$orderStatuses = $ordersQuery->distinct()->pluck('status')->filter();
|
|
836
812
|
|
|
837
|
-
//
|
|
813
|
+
// ---------------------------------------
|
|
814
|
+
// Optionally include activity codes
|
|
815
|
+
// (must use the SAME target config set)
|
|
816
|
+
// ---------------------------------------
|
|
838
817
|
$activityCodes = collect();
|
|
839
818
|
|
|
840
819
|
if ($includeActivities) {
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
820
|
+
// Determine target config UUIDs once, honoring UUID > key > all-on-company
|
|
821
|
+
if ($orderConfigId !== '') {
|
|
822
|
+
$targetConfigUuids = collect([$orderConfigId]);
|
|
823
|
+
} elseif ($orderConfigKey !== '') {
|
|
824
|
+
$targetConfigUuids = DB::table('order_configs')
|
|
825
|
+
->where('company_uuid', $companyUuid)
|
|
826
|
+
->where('key', $orderConfigKey)
|
|
827
|
+
->pluck('uuid');
|
|
828
|
+
} else {
|
|
829
|
+
// No filter given; derive from orders in this company
|
|
830
|
+
$targetConfigUuids = DB::table('orders')
|
|
831
|
+
->where('company_uuid', $companyUuid)
|
|
832
|
+
->whereNotNull('order_config_uuid')
|
|
833
|
+
->distinct()
|
|
834
|
+
->pluck('order_config_uuid');
|
|
835
|
+
}
|
|
856
836
|
|
|
857
|
-
if ($
|
|
837
|
+
if ($targetConfigUuids->isNotEmpty()) {
|
|
858
838
|
$orderConfigs = OrderConfig::where('company_uuid', $companyUuid)
|
|
859
|
-
->whereIn('uuid', $
|
|
839
|
+
->whereIn('uuid', $targetConfigUuids)
|
|
860
840
|
->get();
|
|
861
841
|
|
|
862
|
-
/** @var OrderConfig $config */
|
|
863
842
|
foreach ($orderConfigs as $config) {
|
|
864
843
|
if (!method_exists($config, 'activities')) {
|
|
865
844
|
continue;
|
|
@@ -867,19 +846,22 @@ class OrderController extends FleetOpsController
|
|
|
867
846
|
|
|
868
847
|
$activities = $config->activities();
|
|
869
848
|
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
849
|
+
// Handle Collection/array gracefully
|
|
850
|
+
$codes = collect($activities)
|
|
851
|
+
->map(function ($activity) {
|
|
852
|
+
return data_get($activity, 'code');
|
|
853
|
+
})
|
|
854
|
+
->filter()
|
|
855
|
+
->values();
|
|
875
856
|
|
|
876
|
-
|
|
877
|
-
}
|
|
857
|
+
$activityCodes = $activityCodes->merge($codes);
|
|
878
858
|
}
|
|
879
859
|
}
|
|
880
860
|
}
|
|
881
861
|
|
|
882
|
-
//
|
|
862
|
+
// ---------------------------------------
|
|
863
|
+
// Merge & return
|
|
864
|
+
// ---------------------------------------
|
|
883
865
|
$result = $orderStatuses
|
|
884
866
|
->merge($activityCodes)
|
|
885
867
|
->unique()
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
<?php
|
|
2
|
+
|
|
3
|
+
namespace Fleetbase\FleetOps\Http\Controllers\Internal\v1;
|
|
4
|
+
|
|
5
|
+
use Fleetbase\FleetOps\Http\Controllers\FleetOpsController;
|
|
6
|
+
use Fleetbase\FleetOps\Jobs\ReplayPositions;
|
|
7
|
+
use Fleetbase\FleetOps\Models\Position;
|
|
8
|
+
use Fleetbase\FleetOps\Support\Utils;
|
|
9
|
+
use Illuminate\Http\Request;
|
|
10
|
+
|
|
11
|
+
class PositionController extends FleetOpsController
|
|
12
|
+
{
|
|
13
|
+
/**
|
|
14
|
+
* The resource to query.
|
|
15
|
+
*
|
|
16
|
+
* @var string
|
|
17
|
+
*/
|
|
18
|
+
public $resource = 'position';
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Replay positions on a custom channel.
|
|
22
|
+
*
|
|
23
|
+
* @return \Illuminate\Http\Response
|
|
24
|
+
*/
|
|
25
|
+
public function replay(Request $request)
|
|
26
|
+
{
|
|
27
|
+
$positionIds = $request->input('position_ids', []);
|
|
28
|
+
$channelId = $request->input('channel_id');
|
|
29
|
+
$speed = max((float) $request->input('speed', 1), 0.1); // avoid division by zero
|
|
30
|
+
$subjectUuid = $request->input('subject_uuid');
|
|
31
|
+
|
|
32
|
+
if (!$channelId) {
|
|
33
|
+
return response()->error('Channel ID is required');
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (empty($positionIds)) {
|
|
37
|
+
return response()->error('Position IDs are required');
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
$positions = Position::whereIn('uuid', $positionIds)
|
|
41
|
+
->where('company_uuid', session('company'))
|
|
42
|
+
->orderBy('created_at')
|
|
43
|
+
->get();
|
|
44
|
+
|
|
45
|
+
if ($positions->isEmpty()) {
|
|
46
|
+
return response()->error('No positions found');
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Dispatch async job (will handle replay logic)
|
|
50
|
+
ReplayPositions::dispatch($positions, $channelId, $speed, $subjectUuid);
|
|
51
|
+
|
|
52
|
+
return response()->json([
|
|
53
|
+
'status' => 'ok',
|
|
54
|
+
'message' => 'Replay started',
|
|
55
|
+
'channel_id' => $channelId,
|
|
56
|
+
'total_positions' => $positions->count(),
|
|
57
|
+
]);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Get position statistics/metrics.
|
|
62
|
+
*
|
|
63
|
+
* @return \Illuminate\Http\Response
|
|
64
|
+
*/
|
|
65
|
+
public function metrics(Request $request)
|
|
66
|
+
{
|
|
67
|
+
$positionIds = $request->input('position_ids', []);
|
|
68
|
+
|
|
69
|
+
if (empty($positionIds)) {
|
|
70
|
+
return response()->error('Position IDs are required');
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
$positions = Position::whereIn('uuid', $positionIds)
|
|
74
|
+
->where('company_uuid', session('company'))
|
|
75
|
+
->orderBy('created_at', 'asc')
|
|
76
|
+
->get();
|
|
77
|
+
|
|
78
|
+
if ($positions->isEmpty()) {
|
|
79
|
+
return response()->json([
|
|
80
|
+
'metrics' => [],
|
|
81
|
+
]);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Calculate metrics
|
|
85
|
+
$metrics = $this->calculateMetrics($positions);
|
|
86
|
+
|
|
87
|
+
return response()->json([
|
|
88
|
+
'metrics' => $metrics,
|
|
89
|
+
]);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Calculate metrics from positions.
|
|
94
|
+
*
|
|
95
|
+
* @param \Illuminate\Support\Collection $positions
|
|
96
|
+
*
|
|
97
|
+
* @return array
|
|
98
|
+
*/
|
|
99
|
+
private function calculateMetrics($positions)
|
|
100
|
+
{
|
|
101
|
+
$totalDistance = 0;
|
|
102
|
+
$maxSpeed = 0;
|
|
103
|
+
$avgSpeed = 0;
|
|
104
|
+
$speedingEvents = [];
|
|
105
|
+
$dwellTimes = [];
|
|
106
|
+
$accelerationEvents = [];
|
|
107
|
+
$speedLimit = 100; // km/h - configurable
|
|
108
|
+
$dwellThreshold = 300; // 5 minutes in seconds
|
|
109
|
+
$accelerationThreshold = 2.5; // m/s²
|
|
110
|
+
|
|
111
|
+
$previousPosition = null;
|
|
112
|
+
$previousSpeed = null;
|
|
113
|
+
$dwellStart = null;
|
|
114
|
+
|
|
115
|
+
foreach ($positions as $index => $position) {
|
|
116
|
+
$speed = $position->speed ?? 0;
|
|
117
|
+
|
|
118
|
+
// Track max speed
|
|
119
|
+
if ($speed > $maxSpeed) {
|
|
120
|
+
$maxSpeed = $speed;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Check for speeding (convert m/s to km/h)
|
|
124
|
+
$speedKmh = $speed * 3.6;
|
|
125
|
+
if ($speedKmh > $speedLimit) {
|
|
126
|
+
$speedingEvents[] = [
|
|
127
|
+
'position_uuid' => $position->uuid,
|
|
128
|
+
'speed' => round($speedKmh, 2),
|
|
129
|
+
'timestamp' => $position->created_at->toDateTimeString(),
|
|
130
|
+
];
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Calculate distance and dwell time
|
|
134
|
+
if ($previousPosition) {
|
|
135
|
+
// Calculate distance using Haversine formula
|
|
136
|
+
// $distance = $this->calculateDistance($previousPosition, $position);
|
|
137
|
+
$distance = Utils::vincentyGreatCircleDistance($previousPosition->coordinates, $position->coordinates);
|
|
138
|
+
// dd($previousPosition, $position, $distance);
|
|
139
|
+
$totalDistance += $distance;
|
|
140
|
+
|
|
141
|
+
// Check for dwell (low speed or no movement)
|
|
142
|
+
if ($speed < 0.5) { // Less than 0.5 m/s
|
|
143
|
+
if ($dwellStart === null) {
|
|
144
|
+
$dwellStart = $previousPosition->created_at;
|
|
145
|
+
}
|
|
146
|
+
} else {
|
|
147
|
+
if ($dwellStart !== null) {
|
|
148
|
+
$dwellDuration = $previousPosition->created_at->diffInSeconds($dwellStart);
|
|
149
|
+
if ($dwellDuration >= $dwellThreshold) {
|
|
150
|
+
$dwellTimes[] = [
|
|
151
|
+
'start' => $dwellStart->toDateTimeString(),
|
|
152
|
+
'end' => $previousPosition->created_at->toDateTimeString(),
|
|
153
|
+
'duration' => $dwellDuration,
|
|
154
|
+
];
|
|
155
|
+
}
|
|
156
|
+
$dwellStart = null;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Calculate acceleration
|
|
161
|
+
if ($previousSpeed !== null) {
|
|
162
|
+
$timeDiff = $position->created_at->diffInSeconds($previousPosition->created_at);
|
|
163
|
+
if ($timeDiff > 0) {
|
|
164
|
+
$acceleration = abs($speed - $previousSpeed) / $timeDiff;
|
|
165
|
+
if ($acceleration > $accelerationThreshold) {
|
|
166
|
+
$accelerationEvents[] = [
|
|
167
|
+
'position_uuid' => $position->uuid,
|
|
168
|
+
'acceleration' => round($acceleration, 2),
|
|
169
|
+
'type' => $speed > $previousSpeed ? 'acceleration' : 'deceleration',
|
|
170
|
+
'timestamp' => $position->created_at->toDateTimeString(),
|
|
171
|
+
];
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
$previousPosition = $position;
|
|
178
|
+
$previousSpeed = $speed;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Calculate average speed
|
|
182
|
+
$speeds = $positions->pluck('speed')->filter()->toArray();
|
|
183
|
+
$avgSpeed = count($speeds) > 0 ? array_sum($speeds) / count($speeds) : 0;
|
|
184
|
+
|
|
185
|
+
// Calculate total duration
|
|
186
|
+
$firstPosition = $positions->first();
|
|
187
|
+
$lastPosition = $positions->last();
|
|
188
|
+
$totalDuration = $lastPosition->created_at->diffInSeconds($firstPosition->created_at);
|
|
189
|
+
|
|
190
|
+
return [
|
|
191
|
+
'total_distance' => round($totalDistance / 1000, 2), // Convert to km
|
|
192
|
+
'total_duration' => $totalDuration, // seconds
|
|
193
|
+
'max_speed' => round($maxSpeed * 3.6, 2), // km/h
|
|
194
|
+
'avg_speed' => round($avgSpeed * 3.6, 2), // km/h
|
|
195
|
+
'speeding_events' => $speedingEvents,
|
|
196
|
+
'speeding_count' => count($speedingEvents),
|
|
197
|
+
'dwell_times' => $dwellTimes,
|
|
198
|
+
'dwell_count' => count($dwellTimes),
|
|
199
|
+
'acceleration_events' => $accelerationEvents,
|
|
200
|
+
'acceleration_count' => count($accelerationEvents),
|
|
201
|
+
'total_positions' => $positions->count(),
|
|
202
|
+
];
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Calculate distance between two positions using Haversine formula.
|
|
207
|
+
*
|
|
208
|
+
* @param Position $pos1
|
|
209
|
+
* @param Position $pos2
|
|
210
|
+
*
|
|
211
|
+
* @return float Distance in meters
|
|
212
|
+
*/
|
|
213
|
+
private function calculateDistance($pos1, $pos2)
|
|
214
|
+
{
|
|
215
|
+
$coords1 = [$pos1->latitude, $pos1->longitude];
|
|
216
|
+
$coords2 = [$pos2->latitude, $pos2->longitude];
|
|
217
|
+
|
|
218
|
+
if (!isset($coords1[0]) || !isset($coords2[0]) || !isset($coords1[1]) || !isset($coords2[1])) {
|
|
219
|
+
return 0;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
$lat1 = deg2rad($coords1[0]);
|
|
223
|
+
$lon1 = deg2rad($coords1[1]);
|
|
224
|
+
$lat2 = deg2rad($coords2[0]);
|
|
225
|
+
$lon2 = deg2rad($coords2[1]);
|
|
226
|
+
|
|
227
|
+
$earthRadius = 6371000; // meters
|
|
228
|
+
|
|
229
|
+
$dLat = $lat2 - $lat1;
|
|
230
|
+
$dLon = $lon2 - $lon1;
|
|
231
|
+
|
|
232
|
+
$a = sin($dLat / 2) * sin($dLat / 2) +
|
|
233
|
+
cos($lat1) * cos($lat2) *
|
|
234
|
+
sin($dLon / 2) * sin($dLon / 2);
|
|
235
|
+
|
|
236
|
+
$c = 2 * atan2(sqrt($a), sqrt(1 - $a));
|
|
237
|
+
|
|
238
|
+
return $earthRadius * $c;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
@@ -12,4 +12,15 @@ class SensorController extends FleetOpsController
|
|
|
12
12
|
* @var string
|
|
13
13
|
*/
|
|
14
14
|
public $resource = 'sensor';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Query callback when querying record.
|
|
18
|
+
*
|
|
19
|
+
* @param \Illuminate\Database\Query\Builder $query
|
|
20
|
+
* @param Request $request
|
|
21
|
+
*/
|
|
22
|
+
public static function onQueryRecord($query, $request): void
|
|
23
|
+
{
|
|
24
|
+
$query->with(['telematic', 'warranty']);
|
|
25
|
+
}
|
|
15
26
|
}
|
|
@@ -3,6 +3,11 @@
|
|
|
3
3
|
namespace Fleetbase\FleetOps\Http\Controllers\Internal\v1;
|
|
4
4
|
|
|
5
5
|
use Fleetbase\FleetOps\Http\Controllers\FleetOpsController;
|
|
6
|
+
use Fleetbase\FleetOps\Models\Telematic;
|
|
7
|
+
use Fleetbase\FleetOps\Support\Telematics\TelematicProviderRegistry;
|
|
8
|
+
use Fleetbase\FleetOps\Support\Telematics\TelematicService;
|
|
9
|
+
use Illuminate\Http\JsonResponse;
|
|
10
|
+
use Illuminate\Http\Request;
|
|
6
11
|
|
|
7
12
|
class TelematicController extends FleetOpsController
|
|
8
13
|
{
|
|
@@ -12,4 +17,140 @@ class TelematicController extends FleetOpsController
|
|
|
12
17
|
* @var string
|
|
13
18
|
*/
|
|
14
19
|
public $resource = 'telematic';
|
|
20
|
+
|
|
21
|
+
protected TelematicService $telematicService;
|
|
22
|
+
protected TelematicProviderRegistry $registry;
|
|
23
|
+
|
|
24
|
+
public function __construct(TelematicService $service, TelematicProviderRegistry $registry)
|
|
25
|
+
{
|
|
26
|
+
parent::__construct();
|
|
27
|
+
$this->telematicService = $service;
|
|
28
|
+
$this->registry = $registry;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Query callback when querying record.
|
|
33
|
+
*
|
|
34
|
+
* @param \Illuminate\Database\Query\Builder $query
|
|
35
|
+
* @param Request $request
|
|
36
|
+
*/
|
|
37
|
+
public static function onQueryRecord($query, $request): void
|
|
38
|
+
{
|
|
39
|
+
$query->with(['warranty']);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* List available providers.
|
|
44
|
+
*/
|
|
45
|
+
public function providers(): JsonResponse
|
|
46
|
+
{
|
|
47
|
+
$providers = $this->registry->all()->map(fn ($p) => $p->toArray());
|
|
48
|
+
|
|
49
|
+
return response()->json($providers->values());
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Test connection to provider.
|
|
54
|
+
*/
|
|
55
|
+
public function testConnection(Request $request, string $id): JsonResponse
|
|
56
|
+
{
|
|
57
|
+
$telematic = Telematic::where('uuid', $id)
|
|
58
|
+
->where('company_uuid', session('company'))
|
|
59
|
+
->firstOrFail();
|
|
60
|
+
|
|
61
|
+
$async = $request->input('async', false);
|
|
62
|
+
|
|
63
|
+
$result = $this->telematicService->testConnection($telematic, $async);
|
|
64
|
+
|
|
65
|
+
if ($async) {
|
|
66
|
+
return response()->json($result, 202);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return response()->json($result);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Test connection to provider.
|
|
74
|
+
*/
|
|
75
|
+
public function testCredentials(Request $request, string $key): JsonResponse
|
|
76
|
+
{
|
|
77
|
+
$credentials = $request->array('credentials', []);
|
|
78
|
+
$async = $request->input('async', false);
|
|
79
|
+
|
|
80
|
+
try {
|
|
81
|
+
$provider = $this->registry->resolve($key);
|
|
82
|
+
if (!$provider) {
|
|
83
|
+
return response()->error('Unable to resolve telematic provider.');
|
|
84
|
+
}
|
|
85
|
+
$result = $provider->testConnection($credentials);
|
|
86
|
+
} catch (\Exception $e) {
|
|
87
|
+
return response()->error($e->getMessage());
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if ($async) {
|
|
91
|
+
return response()->json($result, 202);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return response()->json($result);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Discover devices from provider.
|
|
99
|
+
*/
|
|
100
|
+
public function discover(Request $request, string $id): JsonResponse
|
|
101
|
+
{
|
|
102
|
+
$telematic = Telematic::where('uuid', $id)
|
|
103
|
+
->where('company_uuid', session('company'))
|
|
104
|
+
->firstOrFail();
|
|
105
|
+
|
|
106
|
+
$jobId = $this->telematicService->discoverDevices($telematic, [
|
|
107
|
+
'limit' => $request->input('limit', 100),
|
|
108
|
+
'filters' => $request->input('filters', []),
|
|
109
|
+
]);
|
|
110
|
+
|
|
111
|
+
return response()->json([
|
|
112
|
+
'job_id' => $jobId,
|
|
113
|
+
'message' => 'Device discovery initiated',
|
|
114
|
+
], 202);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Get devices for a telematic.
|
|
119
|
+
*/
|
|
120
|
+
public function devices(Request $request, string $id): JsonResponse
|
|
121
|
+
{
|
|
122
|
+
$telematic = Telematic::where('uuid', $id)
|
|
123
|
+
->where('company_uuid', session('company'))
|
|
124
|
+
->firstOrFail();
|
|
125
|
+
|
|
126
|
+
$devices = $this->telematicService->getDevices($telematic, [
|
|
127
|
+
'status' => $request->input('status'),
|
|
128
|
+
'search' => $request->input('search'),
|
|
129
|
+
]);
|
|
130
|
+
|
|
131
|
+
return response()->json([
|
|
132
|
+
'data' => $devices,
|
|
133
|
+
]);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Link a device to a telematic.
|
|
138
|
+
*/
|
|
139
|
+
public function linkDevice(Request $request, string $id): JsonResponse
|
|
140
|
+
{
|
|
141
|
+
$telematic = Telematic::where('uuid', $id)
|
|
142
|
+
->where('company_uuid', session('company'))
|
|
143
|
+
->firstOrFail();
|
|
144
|
+
|
|
145
|
+
$request->validate([
|
|
146
|
+
'external_id' => 'required|string',
|
|
147
|
+
'device_name' => 'required|string',
|
|
148
|
+
]);
|
|
149
|
+
|
|
150
|
+
$device = $this->telematicService->linkDevice($telematic, $request->all());
|
|
151
|
+
|
|
152
|
+
return response()->json([
|
|
153
|
+
'device' => $device,
|
|
154
|
+
], 201);
|
|
155
|
+
}
|
|
15
156
|
}
|