@fleetbase/fleetops-engine 0.6.52 → 0.6.53

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 (60) hide show
  1. package/addon/components/layout/fleet-ops-sidebar/operations-monitor.hbs +201 -0
  2. package/addon/components/layout/fleet-ops-sidebar/operations-monitor.js +589 -0
  3. package/addon/components/layout/fleet-ops-sidebar.hbs +19 -233
  4. package/addon/components/layout/fleet-ops-sidebar.js +287 -394
  5. package/addon/components/maintenance-schedule/form.js +1 -0
  6. package/addon/components/order/form/payload.js +3 -1
  7. package/addon/controllers/connectivity/devices/index.js +4 -1
  8. package/addon/controllers/maintenance/index.js +100 -0
  9. package/addon/controllers/management/fuel-transactions/index.js +1 -1
  10. package/addon/controllers/management/index.js +88 -0
  11. package/addon/controllers/settings/index.js +142 -0
  12. package/addon/extension.js +21 -0
  13. package/addon/routes/analytics/index.js +3 -0
  14. package/addon/routes/connectivity/devices/index.js +3 -0
  15. package/addon/routes/maintenance/index.js +3 -0
  16. package/addon/routes/maintenance/work-orders/index.js +2 -0
  17. package/addon/routes/settings/index.js +3 -0
  18. package/addon/styles/fleetops-engine.css +1801 -2
  19. package/addon/templates/analytics/index.hbs +14 -0
  20. package/addon/templates/analytics/reports/index.hbs +4 -4
  21. package/addon/templates/maintenance/index.hbs +113 -0
  22. package/addon/templates/management/fuel-transactions/index/details.hbs +1 -1
  23. package/addon/templates/management/index.hbs +109 -73
  24. package/addon/templates/operations/orders/index.hbs +1 -1
  25. package/addon/templates/operations/service-rates/index.hbs +1 -1
  26. package/addon/templates/settings/index.hbs +83 -0
  27. package/addon/templates/settings/routing.hbs +2 -2
  28. package/app/controllers/maintenance/index.js +1 -0
  29. package/app/controllers/management/index.js +1 -0
  30. package/app/controllers/settings/index.js +1 -0
  31. package/app/routes/analytics/index.js +1 -0
  32. package/app/routes/maintenance/index.js +1 -0
  33. package/app/routes/settings/index.js +1 -0
  34. package/app/templates/analytics/index.js +1 -0
  35. package/app/templates/maintenance/index.js +1 -0
  36. package/app/templates/management/index.js +1 -0
  37. package/app/templates/settings/index.js +1 -0
  38. package/composer.json +1 -1
  39. package/extension.json +1 -1
  40. package/package.json +3 -3
  41. package/server/migrations/2026_06_15_000001_normalize_device_attachable_vehicle_morph_types.php +30 -0
  42. package/server/src/Console/Commands/FixInvalidPolymorphicRelationTypeNamespaces.php +1 -0
  43. package/server/src/Http/Controllers/Internal/v1/DeviceController.php +75 -11
  44. package/server/src/Http/Controllers/Internal/v1/HubController.php +300 -0
  45. package/server/src/Http/Controllers/Internal/v1/SearchController.php +506 -0
  46. package/server/src/Http/Controllers/Internal/v1/TelematicController.php +1 -1
  47. package/server/src/Http/Controllers/Internal/v1/VehicleController.php +94 -10
  48. package/server/src/Http/Filter/DeviceFilter.php +74 -0
  49. package/server/src/Http/Filter/DriverFilter.php +6 -0
  50. package/server/src/Http/Filter/VehicleFilter.php +6 -0
  51. package/server/src/Jobs/SyncTelematicDevicesJob.php +82 -24
  52. package/server/src/Models/Device.php +1 -1
  53. package/server/src/Models/Driver.php +1 -1
  54. package/server/src/Providers/FleetOpsServiceProvider.php +10 -0
  55. package/server/src/Support/Telematics/Providers/AfaqyProvider.php +51 -13
  56. package/server/src/routes.php +9 -0
  57. package/server/tests/HubRoutesTest.php +62 -0
  58. package/server/tests/TelematicsHardeningTest.php +94 -3
  59. package/translations/ar-ae.yml +1 -1
  60. package/translations/vi-vn.yaml +1 -1
@@ -0,0 +1,506 @@
1
+ <?php
2
+
3
+ namespace Fleetbase\FleetOps\Http\Controllers\Internal\v1;
4
+
5
+ use Fleetbase\FleetOps\Models\Contact;
6
+ use Fleetbase\FleetOps\Models\Device;
7
+ use Fleetbase\FleetOps\Models\DeviceEvent;
8
+ use Fleetbase\FleetOps\Models\Driver;
9
+ use Fleetbase\FleetOps\Models\Equipment;
10
+ use Fleetbase\FleetOps\Models\Fleet;
11
+ use Fleetbase\FleetOps\Models\FuelProviderConnection;
12
+ use Fleetbase\FleetOps\Models\FuelProviderTransaction;
13
+ use Fleetbase\FleetOps\Models\FuelReport;
14
+ use Fleetbase\FleetOps\Models\Issue;
15
+ use Fleetbase\FleetOps\Models\Maintenance;
16
+ use Fleetbase\FleetOps\Models\MaintenanceSchedule;
17
+ use Fleetbase\FleetOps\Models\Order;
18
+ use Fleetbase\FleetOps\Models\OrderConfig;
19
+ use Fleetbase\FleetOps\Models\Part;
20
+ use Fleetbase\FleetOps\Models\Place;
21
+ use Fleetbase\FleetOps\Models\Sensor;
22
+ use Fleetbase\FleetOps\Models\ServiceRate;
23
+ use Fleetbase\FleetOps\Models\Telematic;
24
+ use Fleetbase\FleetOps\Models\Vehicle;
25
+ use Fleetbase\FleetOps\Models\Vendor;
26
+ use Fleetbase\FleetOps\Models\WorkOrder;
27
+ use Fleetbase\Http\Controllers\Controller;
28
+ use Fleetbase\Support\Auth;
29
+ use Illuminate\Database\Eloquent\Builder;
30
+ use Illuminate\Http\JsonResponse;
31
+ use Illuminate\Http\Request;
32
+ use Illuminate\Support\Collection;
33
+ use Illuminate\Support\Str;
34
+
35
+ class SearchController extends Controller
36
+ {
37
+ private const SEARCH_TYPES = [
38
+ 'orders',
39
+ 'drivers',
40
+ 'vehicles',
41
+ 'fleets',
42
+ 'vendors',
43
+ 'contacts',
44
+ 'places',
45
+ 'issues',
46
+ 'fuel_reports',
47
+ 'fuel_transactions',
48
+ 'maintenance_schedules',
49
+ 'work_orders',
50
+ 'maintenances',
51
+ 'equipment',
52
+ 'parts',
53
+ 'fuel_providers',
54
+ 'telematics',
55
+ 'devices',
56
+ 'sensors',
57
+ 'events',
58
+ 'service_rates',
59
+ 'order_configs',
60
+ ];
61
+
62
+ public function search(Request $request): JsonResponse
63
+ {
64
+ $query = trim((string) ($request->input('query') ?: $request->input('q')));
65
+ $limit = max(1, min((int) $request->input('limit', 12), 24));
66
+
67
+ if ($query === '') {
68
+ return response()->json(['results' => []]);
69
+ }
70
+
71
+ $types = $this->requestedTypes($request);
72
+ $perTypeLimit = max(1, (int) ceil($limit / max(count($types), 1)));
73
+ $results = collect();
74
+
75
+ foreach ($types as $type) {
76
+ if (!$this->canSearchType($type)) {
77
+ continue;
78
+ }
79
+
80
+ $results = $results->merge($this->searchType($type, $query, $perTypeLimit));
81
+ }
82
+
83
+ return response()->json([
84
+ 'results' => $results->take($limit)->values(),
85
+ ]);
86
+ }
87
+
88
+ private function requestedTypes(Request $request): array
89
+ {
90
+ $types = $request->input('types', self::SEARCH_TYPES);
91
+
92
+ if (is_string($types)) {
93
+ $types = array_filter(array_map('trim', explode(',', $types)));
94
+ }
95
+
96
+ if (!is_array($types)) {
97
+ return self::SEARCH_TYPES;
98
+ }
99
+
100
+ $types = array_values(array_intersect($types, self::SEARCH_TYPES));
101
+
102
+ return empty($types) ? self::SEARCH_TYPES : $types;
103
+ }
104
+
105
+ private function canSearchType(string $type): bool
106
+ {
107
+ $permissions = [
108
+ 'orders' => 'fleet-ops see order',
109
+ 'drivers' => 'fleet-ops see driver',
110
+ 'vehicles' => 'fleet-ops see vehicle',
111
+ 'fleets' => 'fleet-ops see fleet',
112
+ 'vendors' => 'fleet-ops see vendor',
113
+ 'contacts' => 'fleet-ops see contact',
114
+ 'places' => 'fleet-ops see place',
115
+ 'issues' => 'fleet-ops see issue',
116
+ 'fuel_reports' => 'fleet-ops see fuel-report',
117
+ 'fuel_transactions' => 'fleet-ops see fuel-report',
118
+ 'maintenance_schedules' => 'fleet-ops see maintenance-schedule',
119
+ 'work_orders' => 'fleet-ops see work-order',
120
+ 'maintenances' => 'fleet-ops see maintenance',
121
+ 'equipment' => 'fleet-ops see equipment',
122
+ 'parts' => 'fleet-ops see part',
123
+ 'fuel_providers' => 'fleet-ops see fuel-report',
124
+ 'telematics' => 'fleet-ops see telematic',
125
+ 'devices' => 'fleet-ops see device',
126
+ 'sensors' => 'fleet-ops see sensor',
127
+ 'events' => 'fleet-ops see device-event',
128
+ 'service_rates' => 'fleet-ops see service-rate',
129
+ 'order_configs' => 'fleet-ops see order-config',
130
+ ];
131
+
132
+ $user = Auth::getUserFromSession();
133
+
134
+ if ($user?->isAdmin()) {
135
+ return true;
136
+ }
137
+
138
+ return Auth::can($permissions[$type]);
139
+ }
140
+
141
+ private function searchType(string $type, string $query, int $limit): Collection
142
+ {
143
+ return match ($type) {
144
+ 'orders' => $this->searchOrders($query, $limit),
145
+ 'drivers' => $this->searchDrivers($query, $limit),
146
+ 'vehicles' => $this->searchVehicles($query, $limit),
147
+ 'fleets' => $this->searchFleets($query, $limit),
148
+ 'vendors' => $this->searchVendors($query, $limit),
149
+ 'contacts' => $this->searchContacts($query, $limit),
150
+ 'places' => $this->searchPlaces($query, $limit),
151
+ 'issues' => $this->searchIssues($query, $limit),
152
+ 'fuel_reports' => $this->searchFuelReports($query, $limit),
153
+ 'fuel_transactions' => $this->searchFuelTransactions($query, $limit),
154
+ 'maintenance_schedules' => $this->searchMaintenanceSchedules($query, $limit),
155
+ 'work_orders' => $this->searchWorkOrders($query, $limit),
156
+ 'maintenances' => $this->searchMaintenances($query, $limit),
157
+ 'equipment' => $this->searchEquipment($query, $limit),
158
+ 'parts' => $this->searchParts($query, $limit),
159
+ 'fuel_providers' => $this->searchFuelProviders($query, $limit),
160
+ 'telematics' => $this->searchTelematics($query, $limit),
161
+ 'devices' => $this->searchDevices($query, $limit),
162
+ 'sensors' => $this->searchSensors($query, $limit),
163
+ 'events' => $this->searchEvents($query, $limit),
164
+ 'service_rates' => $this->searchServiceRates($query, $limit),
165
+ 'order_configs' => $this->searchOrderConfigs($query, $limit),
166
+ default => collect(),
167
+ };
168
+ }
169
+
170
+ private function searchOrders(string $query, int $limit): Collection
171
+ {
172
+ return Order::with('trackingNumber')
173
+ ->where('company_uuid', session('company'))
174
+ ->where(function (Builder $builder) use ($query) {
175
+ $this->whereLike($builder, ['public_id', 'internal_id', 'uuid', 'status', 'type'], $query);
176
+ $builder->orWhereHas('trackingNumber', function (Builder $trackingNumberBuilder) use ($query) {
177
+ $this->whereLike($trackingNumberBuilder, ['tracking_number', 'barcode'], $query);
178
+ });
179
+ })
180
+ ->limit($limit)
181
+ ->get()
182
+ ->map(fn (Order $order) => [
183
+ 'label' => $order->tracking ?: $order->public_id ?: $order->internal_id,
184
+ 'description' => $this->description($order->status, $order->type, $order->public_id, $order->internal_id),
185
+ 'icon' => 'map-location-dot',
186
+ 'type' => 'Order',
187
+ 'route' => 'console.fleet-ops.operations.orders.index.details',
188
+ 'models' => [$this->routeModel($order)],
189
+ 'breadcrumb' => 'Fleet-Ops > Operations > Orders',
190
+ ]);
191
+ }
192
+
193
+ private function searchDrivers(string $query, int $limit): Collection
194
+ {
195
+ return Driver::with('user')
196
+ ->where('company_uuid', session('company'))
197
+ ->where(function (Builder $builder) use ($query) {
198
+ $this->whereLike($builder, ['public_id', 'uuid', 'drivers_license_number', 'status'], $query);
199
+ $builder->orWhereHas('user', function (Builder $userBuilder) use ($query) {
200
+ $this->whereLike($userBuilder, ['name', 'email', 'phone'], $query);
201
+ });
202
+ })
203
+ ->limit($limit)
204
+ ->get()
205
+ ->map(fn (Driver $driver) => [
206
+ 'label' => data_get($driver, 'user.name') ?: $driver->public_id,
207
+ 'description' => $this->description(data_get($driver, 'user.email'), data_get($driver, 'user.phone'), $driver->drivers_license_number),
208
+ 'icon' => 'id-card',
209
+ 'type' => 'Driver',
210
+ 'route' => 'console.fleet-ops.management.drivers.index.details',
211
+ 'models' => [$this->routeModel($driver)],
212
+ 'breadcrumb' => 'Fleet-Ops > Resources > Drivers',
213
+ ]);
214
+ }
215
+
216
+ private function searchVehicles(string $query, int $limit): Collection
217
+ {
218
+ return $this->searchGeneric(Vehicle::class, ['name', 'description', 'make', 'model', 'year', 'internal_id', 'plate_number', 'vin', 'serial_number', 'call_sign', 'public_id', 'uuid'], $query, $limit, fn (Vehicle $vehicle) => [
219
+ 'label' => $vehicle->display_name ?: $vehicle->name ?: $vehicle->public_id,
220
+ 'description' => $this->description($vehicle->plate_number, $vehicle->vin, $vehicle->status),
221
+ 'icon' => 'truck',
222
+ 'type' => 'Vehicle',
223
+ 'route' => 'console.fleet-ops.management.vehicles.index.details',
224
+ 'models' => [$this->routeModel($vehicle)],
225
+ 'breadcrumb' => 'Fleet-Ops > Resources > Vehicles',
226
+ ]);
227
+ }
228
+
229
+ private function searchFleets(string $query, int $limit): Collection
230
+ {
231
+ return $this->searchGeneric(Fleet::class, ['name', 'public_id', 'uuid'], $query, $limit, fn (Fleet $fleet) => [
232
+ 'label' => $fleet->name ?: $fleet->public_id,
233
+ 'description' => $this->description($fleet->public_id),
234
+ 'icon' => 'user-group',
235
+ 'type' => 'Fleet',
236
+ 'route' => 'console.fleet-ops.management.fleets.index.details',
237
+ 'models' => [$this->routeModel($fleet)],
238
+ 'breadcrumb' => 'Fleet-Ops > Resources > Fleets',
239
+ ]);
240
+ }
241
+
242
+ private function searchVendors(string $query, int $limit): Collection
243
+ {
244
+ return $this->searchGeneric(Vendor::class, ['name', 'email', 'phone', 'business_id', 'public_id', 'uuid'], $query, $limit, fn (Vendor $vendor) => [
245
+ 'label' => $vendor->name ?: $vendor->public_id,
246
+ 'description' => $this->description($vendor->email, $vendor->phone, $vendor->business_id),
247
+ 'icon' => 'warehouse',
248
+ 'type' => 'Vendor',
249
+ 'route' => 'console.fleet-ops.management.vendors.index.details',
250
+ 'models' => [$this->routeModel($vendor)],
251
+ 'breadcrumb' => 'Fleet-Ops > Resources > Vendors',
252
+ ]);
253
+ }
254
+
255
+ private function searchContacts(string $query, int $limit): Collection
256
+ {
257
+ return $this->searchGeneric(Contact::class, ['name', 'email', 'phone', 'public_id', 'uuid'], $query, $limit, fn (Contact $contact) => [
258
+ 'label' => $contact->name ?: $contact->public_id,
259
+ 'description' => $this->description($contact->email, $contact->phone, $contact->type),
260
+ 'icon' => 'address-book',
261
+ 'type' => 'Contact',
262
+ 'route' => 'console.fleet-ops.management.contacts.index.details',
263
+ 'models' => [$this->routeModel($contact)],
264
+ 'breadcrumb' => 'Fleet-Ops > Resources > Contacts',
265
+ ]);
266
+ }
267
+
268
+ private function searchPlaces(string $query, int $limit): Collection
269
+ {
270
+ return $this->searchGeneric(Place::class, ['name', 'street1', 'street2', 'country', 'province', 'district', 'city', 'postal_code', 'phone', 'public_id', 'uuid'], $query, $limit, fn (Place $place) => [
271
+ 'label' => $place->name ?: $place->public_id,
272
+ 'description' => $this->description($place->street1, $place->city, $place->country),
273
+ 'icon' => 'location-dot',
274
+ 'type' => 'Place',
275
+ 'route' => 'console.fleet-ops.management.places.index.details',
276
+ 'models' => [$this->routeModel($place)],
277
+ 'breadcrumb' => 'Fleet-Ops > Resources > Places',
278
+ ]);
279
+ }
280
+
281
+ private function searchIssues(string $query, int $limit): Collection
282
+ {
283
+ return $this->searchGeneric(Issue::class, ['public_id', 'uuid', 'issue_id', 'category', 'type', 'report', 'title', 'priority', 'status'], $query, $limit, fn (Issue $issue) => [
284
+ 'label' => $issue->title ?: $issue->public_id,
285
+ 'description' => $this->description($issue->status, $issue->priority, $issue->type),
286
+ 'icon' => 'triangle-exclamation',
287
+ 'type' => 'Issue',
288
+ 'route' => 'console.fleet-ops.management.issues.index.details',
289
+ 'models' => [$this->routeModel($issue)],
290
+ 'breadcrumb' => 'Fleet-Ops > Resources > Issues',
291
+ ]);
292
+ }
293
+
294
+ private function searchFuelReports(string $query, int $limit): Collection
295
+ {
296
+ return $this->searchGeneric(FuelReport::class, ['public_id', 'uuid', 'report', 'status', 'currency'], $query, $limit, fn (FuelReport $report) => [
297
+ 'label' => $report->public_id ?: $report->report,
298
+ 'description' => $this->description($report->status, $report->currency, $report->report),
299
+ 'icon' => 'gas-pump',
300
+ 'type' => 'Fuel Report',
301
+ 'route' => 'console.fleet-ops.management.fuel-reports.index.details',
302
+ 'models' => [$this->routeModel($report)],
303
+ 'breadcrumb' => 'Fleet-Ops > Resources > Fuel Reports',
304
+ ]);
305
+ }
306
+
307
+ private function searchFuelTransactions(string $query, int $limit): Collection
308
+ {
309
+ return $this->searchGeneric(FuelProviderTransaction::class, ['public_id', 'provider', 'provider_transaction_id', 'vehicle_card_id', 'internal_number', 'plate_number', 'vin', 'serial_number', 'call_sign', 'station_name', 'trip_number', 'sync_status'], $query, $limit, fn (FuelProviderTransaction $transaction) => [
310
+ 'label' => $transaction->public_id ?: $transaction->provider_transaction_id,
311
+ 'description' => $this->description($transaction->provider, $transaction->station_name, $transaction->sync_status),
312
+ 'icon' => 'credit-card',
313
+ 'type' => 'Fuel Transaction',
314
+ 'route' => 'console.fleet-ops.management.fuel-transactions.index.details',
315
+ 'models' => [$this->routeModel($transaction)],
316
+ 'breadcrumb' => 'Fleet-Ops > Resources > Fuel Transactions',
317
+ ]);
318
+ }
319
+
320
+ private function searchMaintenanceSchedules(string $query, int $limit): Collection
321
+ {
322
+ return $this->searchGeneric(MaintenanceSchedule::class, ['name', 'type', 'status', 'public_id', 'uuid'], $query, $limit, fn (MaintenanceSchedule $schedule) => [
323
+ 'label' => $schedule->name ?: $schedule->public_id,
324
+ 'description' => $this->description($schedule->type, $schedule->status),
325
+ 'icon' => 'calendar-alt',
326
+ 'type' => 'Maintenance Schedule',
327
+ 'route' => 'console.fleet-ops.maintenance.schedules.index.details',
328
+ 'models' => [$this->routeModel($schedule)],
329
+ 'breadcrumb' => 'Fleet-Ops > Maintenance > Schedules',
330
+ ]);
331
+ }
332
+
333
+ private function searchWorkOrders(string $query, int $limit): Collection
334
+ {
335
+ return $this->searchGeneric(WorkOrder::class, ['code', 'subject', 'instructions', 'status', 'priority', 'public_id', 'uuid'], $query, $limit, fn (WorkOrder $workOrder) => [
336
+ 'label' => $workOrder->code ?: $workOrder->subject,
337
+ 'description' => $this->description($workOrder->status, $workOrder->priority, $workOrder->subject),
338
+ 'icon' => 'clipboard-list',
339
+ 'type' => 'Work Order',
340
+ 'route' => 'console.fleet-ops.maintenance.work-orders.index.details',
341
+ 'models' => [$this->routeModel($workOrder)],
342
+ 'breadcrumb' => 'Fleet-Ops > Maintenance > Work Orders',
343
+ ]);
344
+ }
345
+
346
+ private function searchMaintenances(string $query, int $limit): Collection
347
+ {
348
+ return $this->searchGeneric(Maintenance::class, ['summary', 'notes', 'type', 'status', 'priority', 'public_id', 'uuid'], $query, $limit, fn (Maintenance $maintenance) => [
349
+ 'label' => $maintenance->summary ?: $maintenance->public_id,
350
+ 'description' => $this->description($maintenance->status, $maintenance->priority, $maintenance->type),
351
+ 'icon' => 'history',
352
+ 'type' => 'Maintenance',
353
+ 'route' => 'console.fleet-ops.maintenance.maintenances.index.details',
354
+ 'models' => [$this->routeModel($maintenance)],
355
+ 'breadcrumb' => 'Fleet-Ops > Maintenance > Maintenances',
356
+ ]);
357
+ }
358
+
359
+ private function searchEquipment(string $query, int $limit): Collection
360
+ {
361
+ return $this->searchGeneric(Equipment::class, ['name', 'code', 'type', 'serial_number', 'manufacturer', 'model', 'public_id', 'uuid'], $query, $limit, fn (Equipment $equipment) => [
362
+ 'label' => $equipment->name ?: $equipment->code,
363
+ 'description' => $this->description($equipment->type, $equipment->manufacturer, $equipment->model),
364
+ 'icon' => 'trailer',
365
+ 'type' => 'Equipment',
366
+ 'route' => 'console.fleet-ops.maintenance.equipment.index.details',
367
+ 'models' => [$this->routeModel($equipment)],
368
+ 'breadcrumb' => 'Fleet-Ops > Maintenance > Equipment',
369
+ ]);
370
+ }
371
+
372
+ private function searchParts(string $query, int $limit): Collection
373
+ {
374
+ return $this->searchGeneric(Part::class, ['sku', 'name', 'manufacturer', 'model', 'serial_number', 'barcode', 'public_id', 'uuid'], $query, $limit, fn (Part $part) => [
375
+ 'label' => $part->name ?: $part->sku,
376
+ 'description' => $this->description($part->sku, $part->manufacturer, $part->model),
377
+ 'icon' => 'cog',
378
+ 'type' => 'Part',
379
+ 'route' => 'console.fleet-ops.maintenance.parts.index.details',
380
+ 'models' => [$this->routeModel($part)],
381
+ 'breadcrumb' => 'Fleet-Ops > Maintenance > Parts',
382
+ ]);
383
+ }
384
+
385
+ private function searchFuelProviders(string $query, int $limit): Collection
386
+ {
387
+ return $this->searchGeneric(FuelProviderConnection::class, ['public_id', 'name', 'provider', 'status', 'environment', 'uuid'], $query, $limit, fn (FuelProviderConnection $connection) => [
388
+ 'label' => $connection->name ?: $connection->provider,
389
+ 'description' => $this->description($connection->provider, $connection->status, $connection->environment),
390
+ 'icon' => 'gas-pump',
391
+ 'type' => 'Fuel Integration',
392
+ 'route' => 'console.fleet-ops.connectivity.fuel-providers.details',
393
+ 'models' => [$this->routeModel($connection)],
394
+ 'breadcrumb' => 'Fleet-Ops > Connectivity > Fuel Integrations',
395
+ ]);
396
+ }
397
+
398
+ private function searchTelematics(string $query, int $limit): Collection
399
+ {
400
+ return $this->searchGeneric(Telematic::class, ['name', 'provider', 'model', 'serial_number', 'imei', 'public_id', 'uuid'], $query, $limit, fn (Telematic $telematic) => [
401
+ 'label' => $telematic->name ?: $telematic->provider,
402
+ 'description' => $this->description($telematic->provider, $telematic->model, $telematic->serial_number),
403
+ 'icon' => 'satellite-dish',
404
+ 'type' => 'Telematic Provider',
405
+ 'route' => 'console.fleet-ops.connectivity.telematics.details',
406
+ 'models' => [$this->routeModel($telematic)],
407
+ 'breadcrumb' => 'Fleet-Ops > Connectivity > Telematics',
408
+ ]);
409
+ }
410
+
411
+ private function searchDevices(string $query, int $limit): Collection
412
+ {
413
+ return $this->searchGeneric(Device::class, ['name', 'model', 'serial_number', 'manufacturer', 'device_id', 'internal_id', 'imei', 'public_id', 'uuid'], $query, $limit, fn (Device $device) => [
414
+ 'label' => $device->name ?: $device->device_id,
415
+ 'description' => $this->description($device->manufacturer, $device->model, $device->serial_number),
416
+ 'icon' => 'hard-drive',
417
+ 'type' => 'Device',
418
+ 'route' => 'console.fleet-ops.connectivity.devices.index.details',
419
+ 'models' => [$this->routeModel($device)],
420
+ 'breadcrumb' => 'Fleet-Ops > Connectivity > Devices',
421
+ ]);
422
+ }
423
+
424
+ private function searchSensors(string $query, int $limit): Collection
425
+ {
426
+ return $this->searchGeneric(Sensor::class, ['name', 'type', 'internal_id', 'unit', 'public_id', 'uuid'], $query, $limit, fn (Sensor $sensor) => [
427
+ 'label' => $sensor->name ?: $sensor->internal_id,
428
+ 'description' => $this->description($sensor->type, $sensor->unit),
429
+ 'icon' => 'temperature-full',
430
+ 'type' => 'Sensor',
431
+ 'route' => 'console.fleet-ops.connectivity.sensors.index.details',
432
+ 'models' => [$this->routeModel($sensor)],
433
+ 'breadcrumb' => 'Fleet-Ops > Connectivity > Sensors',
434
+ ]);
435
+ }
436
+
437
+ private function searchEvents(string $query, int $limit): Collection
438
+ {
439
+ return $this->searchGeneric(DeviceEvent::class, ['event_type', 'message', 'ident', 'code', 'provider', 'severity', 'public_id', 'uuid'], $query, $limit, fn (DeviceEvent $event) => [
440
+ 'label' => $event->event_type ?: $event->public_id,
441
+ 'description' => $this->description($event->severity, $event->provider, $event->message),
442
+ 'icon' => 'stream',
443
+ 'type' => 'Device Event',
444
+ 'route' => 'console.fleet-ops.connectivity.events.details',
445
+ 'models' => [$this->routeModel($event)],
446
+ 'breadcrumb' => 'Fleet-Ops > Connectivity > Events',
447
+ ]);
448
+ }
449
+
450
+ private function searchServiceRates(string $query, int $limit): Collection
451
+ {
452
+ return $this->searchGeneric(ServiceRate::class, ['public_id', 'uuid', 'service_name', 'service_type', 'currency', 'algorithm', 'rate_calculation_method'], $query, $limit, fn (ServiceRate $serviceRate) => [
453
+ 'label' => $serviceRate->service_name ?: $serviceRate->public_id,
454
+ 'description' => $this->description($serviceRate->service_type, $serviceRate->currency, $serviceRate->rate_calculation_method),
455
+ 'icon' => 'file-invoice-dollar',
456
+ 'type' => 'Service Rate',
457
+ 'route' => 'console.fleet-ops.operations.service-rates.index.details',
458
+ 'models' => [$this->routeModel($serviceRate)],
459
+ 'breadcrumb' => 'Fleet-Ops > Operations > Service Rates',
460
+ ]);
461
+ }
462
+
463
+ private function searchOrderConfigs(string $query, int $limit): Collection
464
+ {
465
+ return $this->searchGeneric(OrderConfig::class, ['name', 'description', 'key', 'namespace', 'status', 'public_id', 'uuid'], $query, $limit, fn (OrderConfig $orderConfig) => [
466
+ 'label' => $orderConfig->name ?: $orderConfig->public_id,
467
+ 'description' => $this->description($orderConfig->status, $orderConfig->description, $orderConfig->namespace),
468
+ 'icon' => 'diagram-project',
469
+ 'type' => 'Order Config',
470
+ 'route' => 'console.fleet-ops.operations.order-config',
471
+ 'queryParams' => ['query' => $query],
472
+ 'breadcrumb' => 'Fleet-Ops > Operations > Order Config',
473
+ ]);
474
+ }
475
+
476
+ private function searchGeneric(string $modelClass, array $columns, string $query, int $limit, callable $mapper): Collection
477
+ {
478
+ return $modelClass::where('company_uuid', session('company'))
479
+ ->where(function (Builder $builder) use ($columns, $query) {
480
+ $this->whereLike($builder, $columns, $query);
481
+ })
482
+ ->limit($limit)
483
+ ->get()
484
+ ->map($mapper);
485
+ }
486
+
487
+ private function whereLike(Builder $builder, array $columns, string $query): void
488
+ {
489
+ $like = '%' . Str::replace(['%', '_'], ['\\%', '\\_'], $query) . '%';
490
+
491
+ foreach ($columns as $index => $column) {
492
+ $method = $index === 0 ? 'where' : 'orWhere';
493
+ $builder->{$method}($column, 'like', $like);
494
+ }
495
+ }
496
+
497
+ private function routeModel($model): ?string
498
+ {
499
+ return $model->public_id ?: $model->uuid;
500
+ }
501
+
502
+ private function description(...$parts): string
503
+ {
504
+ return trim(implode(' ', array_filter(array_map(fn ($part) => is_scalar($part) ? (string) $part : null, $parts))));
505
+ }
506
+ }
@@ -110,7 +110,7 @@ class TelematicController extends FleetOpsController
110
110
  $telematic = $this->findTelematic($id);
111
111
 
112
112
  $jobId = $this->telematicService->discoverDevices($telematic, [
113
- 'limit' => $request->input('limit', 100),
113
+ 'limit' => $request->input('limit'),
114
114
  'filters' => $request->input('filters', []),
115
115
  ]);
116
116
 
@@ -16,6 +16,7 @@ use Illuminate\Http\JsonResponse;
16
16
  use Illuminate\Http\Request;
17
17
  use Illuminate\Support\Arr;
18
18
  use Illuminate\Support\Facades\DB;
19
+ use Illuminate\Support\Facades\Log;
19
20
  use Illuminate\Support\Str;
20
21
  use Maatwebsite\Excel\Facades\Excel;
21
22
 
@@ -179,12 +180,31 @@ class VehicleController extends FleetOpsController
179
180
  {
180
181
  $request->validate(['device' => 'required|string']);
181
182
 
182
- $vehicle = $this->findVehicle($id);
183
- $device = $this->findDevice($request->input('device'));
183
+ $deviceId = $request->input('device');
184
+ $vehicle = $this->resolveVehicle($id);
185
+ $device = $this->resolveDevice($deviceId);
184
186
 
185
- $device->attachTo($vehicle);
186
- $device->load(['telematic', 'warranty', 'attachable']);
187
- $vehicle->load(['driver', 'devices']);
187
+ if (!$vehicle) {
188
+ $this->logDeviceAttachmentLookupFailure('attach-device', 'vehicle', $id, $deviceId);
189
+
190
+ return response()->error('Vehicle not found or not available for this organization.', 404);
191
+ }
192
+
193
+ if (!$device) {
194
+ $this->logDeviceAttachmentLookupFailure('attach-device', 'device', $id, $deviceId);
195
+
196
+ return response()->error('Device not found or not available for this organization.', 404);
197
+ }
198
+
199
+ try {
200
+ $device->attachTo($vehicle);
201
+ $device->load(['telematic', 'warranty', 'attachable']);
202
+ $vehicle->load(['driver', 'devices']);
203
+ } catch (\Throwable $e) {
204
+ $this->logDeviceAttachmentFailure('attach-device', $vehicle, $device, $e);
205
+
206
+ return response()->error('Unable to attach device to vehicle. Please try again or contact support.', 500);
207
+ }
188
208
 
189
209
  return response()->json([
190
210
  'status' => 'ok',
@@ -198,16 +218,35 @@ class VehicleController extends FleetOpsController
198
218
  {
199
219
  $request->validate(['device' => 'required|string']);
200
220
 
201
- $vehicle = $this->findVehicle($id);
202
- $device = $this->findDevice($request->input('device'));
221
+ $deviceId = $request->input('device');
222
+ $vehicle = $this->resolveVehicle($id);
223
+ $device = $this->resolveDevice($deviceId);
224
+
225
+ if (!$vehicle) {
226
+ $this->logDeviceAttachmentLookupFailure('detach-device', 'vehicle', $id, $deviceId);
227
+
228
+ return response()->error('Vehicle not found or not available for this organization.', 404);
229
+ }
230
+
231
+ if (!$device) {
232
+ $this->logDeviceAttachmentLookupFailure('detach-device', 'device', $id, $deviceId);
233
+
234
+ return response()->error('Device not found or not available for this organization.', 404);
235
+ }
203
236
 
204
237
  if ($device->attachable_uuid !== $vehicle->uuid) {
205
238
  return response()->error('This device is not attached to the selected vehicle.');
206
239
  }
207
240
 
208
- $device->detach();
209
- $device->load(['telematic', 'warranty', 'attachable']);
210
- $vehicle->load(['driver', 'devices']);
241
+ try {
242
+ $device->detach();
243
+ $device->load(['telematic', 'warranty', 'attachable']);
244
+ $vehicle->load(['driver', 'devices']);
245
+ } catch (\Throwable $e) {
246
+ $this->logDeviceAttachmentFailure('detach-device', $vehicle, $device, $e);
247
+
248
+ return response()->error('Unable to detach device from vehicle. Please try again or contact support.', 500);
249
+ }
211
250
 
212
251
  return response()->json([
213
252
  'status' => 'ok',
@@ -226,6 +265,15 @@ class VehicleController extends FleetOpsController
226
265
  ->firstOrFail();
227
266
  }
228
267
 
268
+ protected function resolveVehicle(string $id): ?Vehicle
269
+ {
270
+ return Vehicle::where(function ($query) use ($id) {
271
+ $query->where('uuid', $id)->orWhere('public_id', $id);
272
+ })
273
+ ->where('company_uuid', session('company'))
274
+ ->first();
275
+ }
276
+
229
277
  protected function findDriver(string $id): Driver
230
278
  {
231
279
  return Driver::where(function ($query) use ($id) {
@@ -235,6 +283,42 @@ class VehicleController extends FleetOpsController
235
283
  ->firstOrFail();
236
284
  }
237
285
 
286
+ protected function resolveDevice(string $id): ?Device
287
+ {
288
+ return Device::where(function ($query) use ($id) {
289
+ $query->where('uuid', $id)->orWhere('public_id', $id);
290
+ })
291
+ ->where('company_uuid', session('company'))
292
+ ->first();
293
+ }
294
+
295
+ protected function logDeviceAttachmentLookupFailure(string $action, string $missingResource, string $vehicleId, ?string $deviceId): void
296
+ {
297
+ Log::warning('Vehicle device attachment lookup failed', [
298
+ 'action' => $action,
299
+ 'missing_resource' => $missingResource,
300
+ 'vehicle_id' => $vehicleId,
301
+ 'device_id' => $deviceId,
302
+ 'company_uuid' => session('company'),
303
+ 'request_id' => request()->headers->get('X-Request-ID') ?? request()->headers->get('X-Correlation-ID'),
304
+ ]);
305
+ }
306
+
307
+ protected function logDeviceAttachmentFailure(string $action, Vehicle $vehicle, Device $device, \Throwable $exception): void
308
+ {
309
+ Log::error('Vehicle device attachment failed', [
310
+ 'action' => $action,
311
+ 'vehicle_uuid' => $vehicle->uuid,
312
+ 'vehicle_id' => $vehicle->public_id,
313
+ 'device_uuid' => $device->uuid,
314
+ 'device_id' => $device->public_id,
315
+ 'company_uuid' => session('company'),
316
+ 'request_id' => request()->headers->get('X-Request-ID') ?? request()->headers->get('X-Correlation-ID'),
317
+ 'exception_class' => get_class($exception),
318
+ 'exception' => $exception->getMessage(),
319
+ ]);
320
+ }
321
+
238
322
  protected function activeOrderUuid(Vehicle $vehicle): ?string
239
323
  {
240
324
  $vehicle->loadMissing('driver.currentOrder');