@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.
- package/addon/components/layout/fleet-ops-sidebar/operations-monitor.hbs +201 -0
- package/addon/components/layout/fleet-ops-sidebar/operations-monitor.js +589 -0
- package/addon/components/layout/fleet-ops-sidebar.hbs +19 -233
- package/addon/components/layout/fleet-ops-sidebar.js +287 -394
- package/addon/components/maintenance-schedule/form.js +1 -0
- package/addon/components/order/form/payload.js +3 -1
- package/addon/controllers/connectivity/devices/index.js +4 -1
- package/addon/controllers/maintenance/index.js +100 -0
- package/addon/controllers/management/fuel-transactions/index.js +1 -1
- package/addon/controllers/management/index.js +88 -0
- package/addon/controllers/settings/index.js +142 -0
- package/addon/extension.js +21 -0
- package/addon/routes/analytics/index.js +3 -0
- package/addon/routes/connectivity/devices/index.js +3 -0
- package/addon/routes/maintenance/index.js +3 -0
- package/addon/routes/maintenance/work-orders/index.js +2 -0
- package/addon/routes/settings/index.js +3 -0
- package/addon/styles/fleetops-engine.css +1801 -2
- package/addon/templates/analytics/index.hbs +14 -0
- package/addon/templates/analytics/reports/index.hbs +4 -4
- package/addon/templates/maintenance/index.hbs +113 -0
- package/addon/templates/management/fuel-transactions/index/details.hbs +1 -1
- package/addon/templates/management/index.hbs +109 -73
- package/addon/templates/operations/orders/index.hbs +1 -1
- package/addon/templates/operations/service-rates/index.hbs +1 -1
- package/addon/templates/settings/index.hbs +83 -0
- package/addon/templates/settings/routing.hbs +2 -2
- package/app/controllers/maintenance/index.js +1 -0
- package/app/controllers/management/index.js +1 -0
- package/app/controllers/settings/index.js +1 -0
- package/app/routes/analytics/index.js +1 -0
- package/app/routes/maintenance/index.js +1 -0
- package/app/routes/settings/index.js +1 -0
- package/app/templates/analytics/index.js +1 -0
- package/app/templates/maintenance/index.js +1 -0
- package/app/templates/management/index.js +1 -0
- package/app/templates/settings/index.js +1 -0
- package/composer.json +1 -1
- package/extension.json +1 -1
- package/package.json +3 -3
- package/server/migrations/2026_06_15_000001_normalize_device_attachable_vehicle_morph_types.php +30 -0
- package/server/src/Console/Commands/FixInvalidPolymorphicRelationTypeNamespaces.php +1 -0
- package/server/src/Http/Controllers/Internal/v1/DeviceController.php +75 -11
- package/server/src/Http/Controllers/Internal/v1/HubController.php +300 -0
- package/server/src/Http/Controllers/Internal/v1/SearchController.php +506 -0
- package/server/src/Http/Controllers/Internal/v1/TelematicController.php +1 -1
- package/server/src/Http/Controllers/Internal/v1/VehicleController.php +94 -10
- package/server/src/Http/Filter/DeviceFilter.php +74 -0
- package/server/src/Http/Filter/DriverFilter.php +6 -0
- package/server/src/Http/Filter/VehicleFilter.php +6 -0
- package/server/src/Jobs/SyncTelematicDevicesJob.php +82 -24
- package/server/src/Models/Device.php +1 -1
- package/server/src/Models/Driver.php +1 -1
- package/server/src/Providers/FleetOpsServiceProvider.php +10 -0
- package/server/src/Support/Telematics/Providers/AfaqyProvider.php +51 -13
- package/server/src/routes.php +9 -0
- package/server/tests/HubRoutesTest.php +62 -0
- package/server/tests/TelematicsHardeningTest.php +94 -3
- package/translations/ar-ae.yml +1 -1
- 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'
|
|
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
|
-
$
|
|
183
|
-
$
|
|
183
|
+
$deviceId = $request->input('device');
|
|
184
|
+
$vehicle = $this->resolveVehicle($id);
|
|
185
|
+
$device = $this->resolveDevice($deviceId);
|
|
184
186
|
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
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
|
-
$
|
|
202
|
-
$
|
|
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
|
-
|
|
209
|
-
|
|
210
|
-
|
|
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');
|