@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.
Files changed (130) hide show
  1. package/addon/components/custom-entity/form.hbs +14 -14
  2. package/addon/components/device/details.hbs +92 -43
  3. package/addon/components/device/form.hbs +108 -60
  4. package/addon/components/device/form.js +36 -8
  5. package/addon/components/device/panel-header.hbs +32 -0
  6. package/addon/components/device/panel-header.js +3 -0
  7. package/addon/components/driver/form.hbs +1 -1
  8. package/addon/components/driver/form.js +49 -47
  9. package/addon/components/entity/form.hbs +7 -5
  10. package/addon/components/layout/fleet-ops-sidebar.js +12 -12
  11. package/addon/components/map/drawer/device-event-listing.hbs +58 -0
  12. package/addon/components/map/drawer/device-event-listing.js +181 -0
  13. package/addon/components/map/drawer/position-listing.hbs +84 -0
  14. package/addon/components/map/drawer/position-listing.js +289 -0
  15. package/addon/components/map/drawer.js +2 -0
  16. package/addon/components/map/leaflet-live-map.hbs +7 -2
  17. package/addon/components/order/details/payload.hbs +6 -4
  18. package/addon/components/order/details/payload.js +2 -0
  19. package/addon/components/order/kanban.hbs +12 -10
  20. package/addon/components/order/kanban.js +27 -3
  21. package/addon/components/order-config-manager/custom-fields.js +1 -1
  22. package/addon/components/positions-replay.hbs +333 -0
  23. package/addon/components/positions-replay.js +372 -0
  24. package/addon/components/sensor/details.hbs +64 -38
  25. package/addon/components/sensor/form.hbs +112 -63
  26. package/addon/components/sensor/form.js +36 -24
  27. package/addon/components/sensor/panel-header.hbs +32 -0
  28. package/addon/components/sensor/panel-header.js +3 -0
  29. package/addon/components/telematic/details.hbs +40 -16
  30. package/addon/components/telematic/form.hbs +63 -64
  31. package/addon/components/telematic/form.js +73 -4
  32. package/addon/components/vehicle/card.hbs +1 -1
  33. package/addon/controllers/analytics/reports/index/edit.js +1 -1
  34. package/addon/controllers/connectivity/devices/index/details.js +22 -1
  35. package/addon/controllers/connectivity/devices/index/edit.js +66 -1
  36. package/addon/controllers/connectivity/devices/index.js +51 -9
  37. package/addon/controllers/connectivity/events/index.js +65 -16
  38. package/addon/controllers/connectivity/sensors/index/details.js +22 -1
  39. package/addon/controllers/connectivity/sensors/index/edit.js +66 -1
  40. package/addon/controllers/connectivity/sensors/index.js +66 -6
  41. package/addon/controllers/connectivity/telematics/index/details.js +22 -1
  42. package/addon/controllers/connectivity/telematics/index/edit.js +66 -1
  43. package/addon/controllers/connectivity/telematics/index.js +20 -11
  44. package/addon/controllers/management/fleets/index/details.js +26 -21
  45. package/addon/controllers/management/fleets/index/edit.js +9 -6
  46. package/addon/controllers/management/vehicles/index/details.js +21 -13
  47. package/addon/controllers/operations/orders/index/new.js +4 -2
  48. package/addon/controllers/operations/orders/index.js +50 -45
  49. package/addon/controllers/settings/custom-fields.js +6 -0
  50. package/addon/helpers/get-fleet-ops-option-label.js +11 -0
  51. package/addon/routes/connectivity/devices/index/details.js +27 -1
  52. package/addon/routes/connectivity/devices/index/edit.js +27 -1
  53. package/addon/routes/connectivity/sensors/index/details.js +27 -1
  54. package/addon/routes/connectivity/sensors/index/edit.js +27 -1
  55. package/addon/routes/connectivity/telematics/index/details.js +27 -1
  56. package/addon/routes/connectivity/telematics/index/edit.js +27 -1
  57. package/addon/routes/management/vehicles/index/details/positions.js +3 -0
  58. package/addon/routes/operations/orders/index.js +0 -3
  59. package/addon/routes.js +1 -0
  60. package/addon/services/movement-tracker.js +81 -30
  61. package/addon/services/order-creation.js +4 -8
  62. package/addon/services/order-validation.js +3 -3
  63. package/addon/styles/fleetops-engine.css +192 -0
  64. package/addon/templates/connectivity/devices/index/details/index.hbs +2 -2
  65. package/addon/templates/connectivity/devices/index/details.hbs +15 -2
  66. package/addon/templates/connectivity/devices/index/edit.hbs +1 -1
  67. package/addon/templates/connectivity/events/index.hbs +1 -1
  68. package/addon/templates/connectivity/sensors/index/details/index.hbs +2 -2
  69. package/addon/templates/connectivity/sensors/index/details.hbs +15 -2
  70. package/addon/templates/connectivity/sensors/index/edit.hbs +1 -1
  71. package/addon/templates/connectivity/telematics/index/details/index.hbs +2 -2
  72. package/addon/templates/connectivity/telematics/index/details.hbs +14 -2
  73. package/addon/templates/connectivity/telematics/index/edit.hbs +1 -1
  74. package/addon/templates/management/vehicles/index/details/positions.hbs +1 -0
  75. package/addon/templates/operations/orders/index.hbs +26 -2
  76. package/addon/utils/fleet-ops-options.js +95 -0
  77. package/addon/utils/setup-customer-portal.js +7 -0
  78. package/app/components/device/panel-header.js +1 -0
  79. package/app/components/map/drawer/device-event-listing.js +1 -0
  80. package/app/components/map/drawer/position-listing.js +1 -0
  81. package/app/components/positions-replay.js +1 -0
  82. package/app/components/sensor/panel-header.js +1 -0
  83. package/app/helpers/get-fleet-ops-option-label.js +1 -0
  84. package/app/routes/management/vehicles/index/details/positions.js +1 -0
  85. package/app/templates/management/vehicles/index/details/positions.js +1 -0
  86. package/composer.json +1 -1
  87. package/extension.json +1 -1
  88. package/package.json +4 -4
  89. package/server/config/telematics.php +111 -0
  90. package/server/migrations/2025_10_27_000001_add_telematics_integration_fields.php +70 -0
  91. package/server/migrations/2025_10_27_171322_fix_device_column_names.php +107 -0
  92. package/server/migrations/2025_10_27_203023_add_company_uuid_to_device_events_table.php +28 -0
  93. package/server/src/Console/Commands/ReplayVehicleLocations.php +225 -0
  94. package/server/src/Contracts/TelematicProviderDescriptor.php +72 -0
  95. package/server/src/Contracts/TelematicProviderInterface.php +119 -0
  96. package/server/src/Exceptions/TelematicProviderException.php +14 -0
  97. package/server/src/Exceptions/TelematicRateLimitExceededException.php +12 -0
  98. package/server/src/Http/Controllers/Api/v1/DriverController.php +24 -14
  99. package/server/src/Http/Controllers/Api/v1/VehicleController.php +27 -7
  100. package/server/src/Http/Controllers/Internal/v1/DeviceController.php +22 -0
  101. package/server/src/Http/Controllers/Internal/v1/OrderController.php +50 -68
  102. package/server/src/Http/Controllers/Internal/v1/PositionController.php +240 -0
  103. package/server/src/Http/Controllers/Internal/v1/SensorController.php +11 -0
  104. package/server/src/Http/Controllers/Internal/v1/TelematicController.php +141 -0
  105. package/server/src/Http/Controllers/Internal/v1/TelematicWebhookController.php +170 -0
  106. package/server/src/Http/Filter/DeviceEventFilter.php +68 -0
  107. package/server/src/Http/Filter/PositionFilter.php +35 -0
  108. package/server/src/Http/Resources/v1/Position.php +44 -0
  109. package/server/src/Jobs/ReplayPositions.php +64 -0
  110. package/server/src/Jobs/SendPositionReplay.php +65 -0
  111. package/server/src/Jobs/SyncTelematicDevicesJob.php +106 -0
  112. package/server/src/Jobs/TestTelematicConnectionJob.php +102 -0
  113. package/server/src/Models/Device.php +72 -10
  114. package/server/src/Models/DeviceEvent.php +7 -0
  115. package/server/src/Models/Driver.php +28 -1
  116. package/server/src/Models/Payload.php +11 -3
  117. package/server/src/Models/Place.php +9 -2
  118. package/server/src/Models/Position.php +17 -17
  119. package/server/src/Models/Sensor.php +78 -13
  120. package/server/src/Models/Telematic.php +116 -6
  121. package/server/src/Models/Vehicle.php +104 -1
  122. package/server/src/Providers/FleetOpsServiceProvider.php +2 -0
  123. package/server/src/Support/Telematics/Providers/AbstractProvider.php +151 -0
  124. package/server/src/Support/Telematics/Providers/FlespiProvider.php +182 -0
  125. package/server/src/Support/Telematics/Providers/GeotabProvider.php +181 -0
  126. package/server/src/Support/Telematics/Providers/SamsaraProvider.php +177 -0
  127. package/server/src/Support/Telematics/TelematicProviderRegistry.php +147 -0
  128. package/server/src/Support/Telematics/TelematicService.php +223 -0
  129. package/server/src/Support/Utils.php +1 -1
  130. package/server/src/routes.php +12 -1
@@ -774,55 +774,30 @@ class OrderController extends FleetOpsController
774
774
  }
775
775
 
776
776
  /**
777
- * Retrieve all distinct order statuses for the authenticated company.
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 = $request->user()->company_uuid ?? session('company');
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
- // Get distinct statuses from orders
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
- if ($orderConfigKey !== '') {
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
- // Optionally include activity codes from used order configs
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
- $configUuidsOnOrders = DB::table('orders')
842
- ->where('company_uuid', $companyUuid)
843
- ->when($orderConfigKey !== '', function ($q) use ($companyUuid, $orderConfigKey) {
844
- $q->whereExists(function ($sub) use ($companyUuid, $orderConfigKey) {
845
- $sub->select(DB::raw(1))
846
- ->from('order_configs as oc')
847
- ->whereColumn('oc.uuid', 'orders.order_config_uuid')
848
- ->where('oc.company_uuid', $companyUuid)
849
- ->where('oc.key', $orderConfigKey);
850
- });
851
- })
852
- ->whereNotNull('order_config_uuid')
853
- ->distinct()
854
- ->pluck('order_config_uuid')
855
- ->filter();
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 ($configUuidsOnOrders->isNotEmpty()) {
837
+ if ($targetConfigUuids->isNotEmpty()) {
858
838
  $orderConfigs = OrderConfig::where('company_uuid', $companyUuid)
859
- ->whereIn('uuid', $configUuidsOnOrders)
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
- if ($activities instanceof Collection) {
871
- $codes = $activities
872
- ->map(fn ($activity) => $activity->code ?? null)
873
- ->filter()
874
- ->values();
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
- $activityCodes = $activityCodes->merge($codes);
877
- }
857
+ $activityCodes = $activityCodes->merge($codes);
878
858
  }
879
859
  }
880
860
  }
881
861
 
882
- // Merge & deduplicate everything
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
  }