@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
@@ -7,6 +7,7 @@ use Fleetbase\FleetOps\Models\Device;
7
7
  use Fleetbase\FleetOps\Models\Vehicle;
8
8
  use Illuminate\Http\JsonResponse;
9
9
  use Illuminate\Http\Request;
10
+ use Illuminate\Support\Facades\Log;
10
11
 
11
12
  class DeviceController extends FleetOpsController
12
13
  {
@@ -64,11 +65,30 @@ class DeviceController extends FleetOpsController
64
65
  'vehicle' => 'required_without:attachable_uuid|nullable|string',
65
66
  ]);
66
67
 
67
- $device = $this->findDevice($id);
68
- $vehicle = $this->findVehicle($request->input('vehicle') ?? $request->input('attachable_uuid'));
68
+ $vehicleId = $request->input('vehicle') ?? $request->input('attachable_uuid');
69
+ $device = $this->resolveDevice($id);
70
+ $vehicle = $this->resolveVehicle($vehicleId);
69
71
 
70
- $device->attachTo($vehicle);
71
- $device->load(['telematic', 'warranty', 'attachable']);
72
+ if (!$device) {
73
+ $this->logDeviceAttachmentLookupFailure('attach', 'device', $id, $vehicleId);
74
+
75
+ return response()->error('Device not found or not available for this organization.', 404);
76
+ }
77
+
78
+ if (!$vehicle) {
79
+ $this->logDeviceAttachmentLookupFailure('attach', 'vehicle', $id, $vehicleId);
80
+
81
+ return response()->error('Vehicle not found or not available for this organization.', 404);
82
+ }
83
+
84
+ try {
85
+ $device->attachTo($vehicle);
86
+ $device->load(['telematic', 'warranty', 'attachable']);
87
+ } catch (\Throwable $e) {
88
+ $this->logDeviceAttachmentFailure('attach', $device, $vehicle, $e);
89
+
90
+ return response()->error('Unable to attach device to vehicle. Please try again or contact support.', 500);
91
+ }
72
92
 
73
93
  return response()->json([
74
94
  'status' => 'ok',
@@ -81,9 +101,22 @@ class DeviceController extends FleetOpsController
81
101
  */
82
102
  public function detach(string $id): JsonResponse
83
103
  {
84
- $device = $this->findDevice($id);
85
- $device->detach();
86
- $device->load(['telematic', 'warranty', 'attachable']);
104
+ $device = $this->resolveDevice($id);
105
+
106
+ if (!$device) {
107
+ $this->logDeviceAttachmentLookupFailure('detach', 'device', $id, null);
108
+
109
+ return response()->error('Device not found or not available for this organization.', 404);
110
+ }
111
+
112
+ try {
113
+ $device->detach();
114
+ $device->load(['telematic', 'warranty', 'attachable']);
115
+ } catch (\Throwable $e) {
116
+ $this->logDeviceAttachmentFailure('detach', $device, null, $e);
117
+
118
+ return response()->error('Unable to detach device from vehicle. Please try again or contact support.', 500);
119
+ }
87
120
 
88
121
  return response()->json([
89
122
  'status' => 'ok',
@@ -91,21 +124,52 @@ class DeviceController extends FleetOpsController
91
124
  ]);
92
125
  }
93
126
 
94
- protected function findDevice(string $id): Device
127
+ protected function resolveDevice(string $id): ?Device
95
128
  {
96
129
  return Device::where(function ($query) use ($id) {
97
130
  $query->where('uuid', $id)->orWhere('public_id', $id);
98
131
  })
99
132
  ->where('company_uuid', session('company'))
100
- ->firstOrFail();
133
+ ->first();
101
134
  }
102
135
 
103
- protected function findVehicle(?string $id): Vehicle
136
+ protected function resolveVehicle(?string $id): ?Vehicle
104
137
  {
138
+ if (!$id) {
139
+ return null;
140
+ }
141
+
105
142
  return Vehicle::where(function ($query) use ($id) {
106
143
  $query->where('uuid', $id)->orWhere('public_id', $id);
107
144
  })
108
145
  ->where('company_uuid', session('company'))
109
- ->firstOrFail();
146
+ ->first();
147
+ }
148
+
149
+ protected function logDeviceAttachmentLookupFailure(string $action, string $missingResource, string $deviceId, ?string $vehicleId): void
150
+ {
151
+ Log::warning('Device attachment lookup failed', [
152
+ 'action' => $action,
153
+ 'missing_resource' => $missingResource,
154
+ 'device_id' => $deviceId,
155
+ 'vehicle_id' => $vehicleId,
156
+ 'company_uuid' => session('company'),
157
+ 'request_id' => request()->headers->get('X-Request-ID') ?? request()->headers->get('X-Correlation-ID'),
158
+ ]);
159
+ }
160
+
161
+ protected function logDeviceAttachmentFailure(string $action, Device $device, ?Vehicle $vehicle, \Throwable $exception): void
162
+ {
163
+ Log::error('Device attachment failed', [
164
+ 'action' => $action,
165
+ 'device_uuid' => $device->uuid,
166
+ 'device_id' => $device->public_id,
167
+ 'vehicle_uuid' => $vehicle?->uuid,
168
+ 'vehicle_id' => $vehicle?->public_id,
169
+ 'company_uuid' => session('company'),
170
+ 'request_id' => request()->headers->get('X-Request-ID') ?? request()->headers->get('X-Correlation-ID'),
171
+ 'exception_class' => get_class($exception),
172
+ 'exception' => $exception->getMessage(),
173
+ ]);
110
174
  }
111
175
  }
@@ -0,0 +1,300 @@
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\Driver;
8
+ use Fleetbase\FleetOps\Models\Equipment;
9
+ use Fleetbase\FleetOps\Models\Fleet;
10
+ use Fleetbase\FleetOps\Models\FuelProviderTransaction;
11
+ use Fleetbase\FleetOps\Models\FuelReport;
12
+ use Fleetbase\FleetOps\Models\Issue;
13
+ use Fleetbase\FleetOps\Models\Maintenance;
14
+ use Fleetbase\FleetOps\Models\MaintenanceSchedule;
15
+ use Fleetbase\FleetOps\Models\Part;
16
+ use Fleetbase\FleetOps\Models\Place;
17
+ use Fleetbase\FleetOps\Models\Vehicle;
18
+ use Fleetbase\FleetOps\Models\Vendor;
19
+ use Fleetbase\FleetOps\Models\WorkOrder;
20
+ use Fleetbase\Http\Controllers\Controller;
21
+ use Illuminate\Database\Eloquent\Builder;
22
+ use Illuminate\Http\Request;
23
+ use Illuminate\Support\Carbon;
24
+
25
+ class HubController extends Controller
26
+ {
27
+ public function resources(Request $request)
28
+ {
29
+ $company = $this->companyUuid($request);
30
+
31
+ $counts = [
32
+ 'drivers' => $this->count(Driver::query(), $company),
33
+ 'vehicles' => $this->count(Vehicle::query(), $company),
34
+ 'fleets' => $this->count(Fleet::query()->whereNull('parent_fleet_uuid'), $company),
35
+ 'vendors' => $this->count(Vendor::query(), $company),
36
+ 'contacts' => $this->count(Contact::query(), $company),
37
+ 'places' => $this->count(Place::query(), $company),
38
+ 'issues' => $this->count(Issue::query()->whereNotIn('status', ['resolved', 'closed']), $company),
39
+ 'drivers_without_vehicles' => $this->count(Driver::query()->whereNull('vehicle_uuid'), $company),
40
+ 'vehicles_without_drivers' => $this->count(Vehicle::query()->whereDoesntHave('driver'), $company),
41
+ 'vehicles_without_devices' => $this->count(Vehicle::query()->whereDoesntHave('devices'), $company),
42
+ 'unattached_devices' => $this->count(Device::query()->whereNull('attachable_uuid'), $company),
43
+ 'resource_issues' => $this->count(Issue::query()->whereNotIn('status', ['resolved', 'closed'])->where(function ($query) {
44
+ $query->whereNotNull('vehicle_uuid')->orWhereNotNull('driver_uuid');
45
+ }), $company),
46
+ 'overdue_vehicle_schedules' => $this->count(MaintenanceSchedule::query()->where('subject_type', Vehicle::class)->where('status', 'active')->whereNotNull('next_due_date')->where('next_due_date', '<', Carbon::now()), $company),
47
+ 'upcoming_vehicle_schedules' => $this->count(MaintenanceSchedule::query()->where('subject_type', Vehicle::class)->where('status', 'active')->whereBetween('next_due_date', [Carbon::now(), Carbon::now()->addDays(7)]), $company),
48
+ 'open_resource_work_orders' => $this->count(WorkOrder::query()->whereIn('target_type', [Vehicle::class, Equipment::class])->whereIn('status', ['open', 'in_progress']), $company),
49
+ 'overdue_resource_work_orders' => $this->count(WorkOrder::query()->whereIn('target_type', [Vehicle::class, Equipment::class])->whereNotIn('status', ['closed', 'canceled'])->whereNotNull('due_at')->where('due_at', '<', Carbon::now()), $company),
50
+ 'low_stock_parts' => $this->count(Part::query()->where('quantity_on_hand', '>', 0)->where('quantity_on_hand', '<=', 5), $company),
51
+ 'unmatched_fuel_transactions' => $this->count(FuelProviderTransaction::query()->where('sync_status', 'unmatched'), $company),
52
+ 'fuel_reports' => $this->count(FuelReport::query(), $company),
53
+ 'fuel_transactions' => $this->count(FuelProviderTransaction::query(), $company),
54
+ ];
55
+
56
+ $fuelRecords = $counts['fuel_reports'] + $counts['fuel_transactions'];
57
+
58
+ return response()->json([
59
+ 'kpis' => [
60
+ $this->kpi('drivers', 'Drivers', $counts['drivers'], $counts['drivers'] > 0 ? 'Driver profiles available for dispatch.' : 'Add driver profiles before dispatching work.', 'blue', 'id-card', 'management.drivers'),
61
+ $this->kpi('vehicles', 'Vehicles', $counts['vehicles'], $counts['vehicles'] > 0 ? 'Vehicles ready for assignment and tracking.' : 'Add vehicles so dispatch has assignable assets.', 'green', 'truck', 'management.vehicles'),
62
+ $this->kpi('issues', 'Open Issues', $counts['issues'], $counts['issues'] > 0 ? 'Issues need review before they affect service quality.' : 'No open issues are visible right now.', $counts['issues'] > 0 ? 'rose' : 'green', 'triangle-exclamation', 'management.issues'),
63
+ $this->kpi('fuel_records', 'Fuel Records', $fuelRecords, $fuelRecords > 0 ? 'Fuel activity is available for review.' : 'Fuel records will appear as reports or transactions arrive.', 'amber', 'gas-pump', $counts['fuel_transactions'] > $counts['fuel_reports'] ? 'management.fuel-transactions' : 'management.fuel-reports'),
64
+ ],
65
+ 'actions' => $this->resourceActions($counts, $fuelRecords),
66
+ 'sections' => [
67
+ [
68
+ 'key' => 'people_assets',
69
+ 'title' => 'People And Assets',
70
+ 'description' => 'Keep dispatchable drivers, vehicles, and fleet groupings ready for live work.',
71
+ 'links' => [
72
+ $this->link('Drivers', 'management.drivers', 'id-card', $counts['drivers'], 'Manage driver profiles, assignments, and schedules.'),
73
+ $this->link('Vehicles', 'management.vehicles', 'truck', $counts['vehicles'], 'Track vehicles, device links, equipment, and service context.'),
74
+ $this->link('Fleets', 'management.fleets', 'user-group', $counts['fleets'], 'Group drivers and vehicles by team, region, or service type.'),
75
+ ],
76
+ ],
77
+ [
78
+ 'key' => 'network',
79
+ 'title' => 'Operating Network',
80
+ 'description' => 'Maintain the partners, contacts, and places used while creating and dispatching orders.',
81
+ 'links' => [
82
+ $this->link('Vendors', 'management.vendors', 'warehouse', $counts['vendors'], 'Service partners connected to operational work.'),
83
+ $this->link('Contacts', 'management.contacts', 'address-book', $counts['contacts'], 'Customer and contact records for order workflows.'),
84
+ $this->link('Places', 'management.places', 'location-dot', $counts['places'], 'Reusable locations, zones, and address context.'),
85
+ ],
86
+ ],
87
+ [
88
+ 'key' => 'exceptions',
89
+ 'title' => 'Fuel And Exceptions',
90
+ 'description' => 'Review fuel activity and operational issues before they affect service quality.',
91
+ 'links' => [
92
+ $this->link('Fuel Reports', 'management.fuel-reports', 'gas-pump', $counts['fuel_reports'], 'Review reported fuel activity and costs.'),
93
+ $this->link('Fuel Transactions', 'management.fuel-transactions', 'credit-card', $counts['fuel_transactions'], 'Inspect transaction-level fueling data and matching context.'),
94
+ $this->link('Issues', 'management.issues', 'triangle-exclamation', $counts['issues'], 'Track incidents, service issues, and follow-up work.'),
95
+ ],
96
+ ],
97
+ ],
98
+ 'docs' => [
99
+ $this->doc('Drivers', 'id-card', 'fleet-ops/resources/drivers/overview', 'Drivers guide', 'Create driver profiles, app access, assignment context, and operating status.'),
100
+ $this->doc('Vehicles', 'truck', 'fleet-ops/resources/vehicles/overview', 'Vehicles guide', 'Track fleet assets, assignment readiness, and vehicle operating details.'),
101
+ $this->doc('Fleets', 'user-group', 'fleet-ops/resources/fleets/overview', 'Fleets guide', 'Group drivers and vehicles by team, region, or service coverage.'),
102
+ $this->doc('Contacts', 'address-book', 'fleet-ops/resources/contacts/overview', 'Contacts guide', 'Manage people and organizations used across orders and operational records.'),
103
+ $this->doc('Places', 'location-dot', 'fleet-ops/resources/places/overview', 'Places guide', 'Manage reusable pickup, dropoff, hub, and facility locations.'),
104
+ $this->doc('Issues', 'triangle-exclamation', 'fleet-ops/resources/issues/overview', 'Issues guide', 'Track incidents, service exceptions, and operational follow-up.'),
105
+ ],
106
+ ]);
107
+ }
108
+
109
+ public function maintenance(Request $request)
110
+ {
111
+ $company = $this->companyUuid($request);
112
+ $now = Carbon::now();
113
+ $next7 = Carbon::now()->addDays(7);
114
+
115
+ $overdueSchedules = $this->count(MaintenanceSchedule::query()->where('status', 'active')->whereNotNull('next_due_date')->where('next_due_date', '<', $now), $company);
116
+ $upcomingSchedules = $this->count(MaintenanceSchedule::query()->where('status', 'active')->whereBetween('next_due_date', [$now, $next7]), $company);
117
+ $openWorkOrders = $this->count(WorkOrder::query()->whereIn('status', ['open', 'in_progress']), $company);
118
+ $overdueWorkOrders = $this->count(WorkOrder::query()->whereNotIn('status', ['closed', 'canceled'])->whereNotNull('due_at')->where('due_at', '<', $now), $company);
119
+ $openMaintenance = $this->count(Maintenance::query()->whereNotIn('status', ['completed', 'canceled']), $company);
120
+ $highPriorityMaintenance = $this->count(Maintenance::query()->whereNotIn('status', ['completed', 'canceled'])->whereIn('priority', ['high', 'urgent', 'critical']), $company);
121
+ $lowStockParts = $this->count(Part::query()->where('quantity_on_hand', '>', 0)->where('quantity_on_hand', '<=', 5), $company);
122
+ $equipment = $this->count(Equipment::query(), $company);
123
+
124
+ return response()->json([
125
+ 'kpis' => [
126
+ $this->kpi('overdue_schedules', 'Overdue Schedules', $overdueSchedules, $overdueSchedules > 0 ? 'Recurring service needs attention.' : 'No recurring service is overdue.', $overdueSchedules > 0 ? 'rose' : 'green', 'calendar-xmark', 'maintenance.schedules'),
127
+ $this->kpi('upcoming_schedules', 'Due This Week', $upcomingSchedules, 'Maintenance schedules due in the next 7 days.', 'blue', 'calendar-day', 'maintenance.schedules'),
128
+ $this->kpi('open_work_orders', 'Open Work Orders', $openWorkOrders, $openWorkOrders > 0 ? 'Active work needs assignment or closure.' : 'No open work orders right now.', $openWorkOrders > 0 ? 'amber' : 'green', 'clipboard-list', 'maintenance.work-orders'),
129
+ $this->kpi('low_stock_parts', 'Low Stock Parts', $lowStockParts, $lowStockParts > 0 ? 'Parts inventory may need replenishment.' : 'No low-stock parts detected.', $lowStockParts > 0 ? 'amber' : 'green', 'cog', 'maintenance.parts'),
130
+ ],
131
+ 'actions' => $this->maintenanceActions($overdueSchedules, $upcomingSchedules, $openWorkOrders, $overdueWorkOrders, $highPriorityMaintenance, $lowStockParts, $equipment),
132
+ 'sections' => [
133
+ [
134
+ 'key' => 'planning',
135
+ 'title' => 'Planning',
136
+ 'description' => 'Schedule recurring service before work turns urgent.',
137
+ 'links' => [
138
+ $this->link('Schedules', 'maintenance.schedules', 'calendar-alt', $upcomingSchedules + $overdueSchedules, 'Recurring maintenance intervals and service windows.'),
139
+ $this->link('Work Orders', 'maintenance.work-orders', 'clipboard-list', $openWorkOrders, 'Repair tasks, assignments, vendor work, and closure.'),
140
+ ],
141
+ ],
142
+ [
143
+ 'key' => 'records',
144
+ 'title' => 'Records',
145
+ 'description' => 'Keep the maintenance history and serviceable asset catalog current.',
146
+ 'links' => [
147
+ $this->link('Maintenances', 'maintenance.maintenances', 'history', $openMaintenance, 'Maintenance records and service history by asset.'),
148
+ $this->link('Equipment', 'maintenance.equipment', 'trailer', $equipment, 'Assigned equipment and serviceable operational assets.'),
149
+ $this->link('Parts', 'maintenance.parts', 'cog', $lowStockParts, 'Parts inventory visibility for repair planning.'),
150
+ ],
151
+ ],
152
+ ],
153
+ 'docs' => [
154
+ $this->doc('Schedules', 'calendar-alt', 'fleet-ops/maintenance/schedules/overview', 'Maintenance schedules guide', 'Plan recurring service windows and convert due schedules into work orders.'),
155
+ $this->doc('Work Orders', 'clipboard-list', 'fleet-ops/maintenance/work-orders/overview', 'Work orders guide', 'Coordinate assigned maintenance work, vendors, due dates, and completion.'),
156
+ $this->doc('Equipment', 'trailer', 'fleet-ops/maintenance/equipment/overview', 'Equipment guide', 'Track serviceable equipment that participates in maintenance operations.'),
157
+ $this->doc('Parts', 'cog', 'fleet-ops/maintenance/parts/overview', 'Parts guide', 'Manage parts inventory and restocking signals used by maintenance teams.'),
158
+ ],
159
+ ]);
160
+ }
161
+
162
+ protected function resourceActions(array $counts, int $fuelRecords): array
163
+ {
164
+ $actions = [];
165
+
166
+ if ($counts['drivers'] === 0) {
167
+ $actions[] = $this->action('add_drivers', 'Add driver profiles', 'Drivers are required before dispatch can assign live orders.', 'warning', 'id-card', 'management.drivers');
168
+ }
169
+
170
+ if ($counts['vehicles'] === 0) {
171
+ $actions[] = $this->action('add_vehicles', 'Add vehicles', 'Vehicles make assignment, tracking, and service context usable.', 'warning', 'truck', 'management.vehicles');
172
+ }
173
+
174
+ if ($counts['drivers_without_vehicles'] > 0 && $counts['vehicles'] > 0) {
175
+ $actions[] = $this->action('assign_vehicles_to_drivers', 'Assign vehicles to drivers', "{$counts['drivers_without_vehicles']} driver" . ($counts['drivers_without_vehicles'] === 1 ? ' does' : 's do') . ' not have an assigned vehicle.', 'warning', 'id-card', 'management.drivers', ['vehicle' => 'unassigned']);
176
+ }
177
+
178
+ if ($counts['vehicles_without_drivers'] > 0 && $counts['drivers'] > 0) {
179
+ $actions[] = $this->action('assign_drivers_to_vehicles', 'Assign drivers to vehicles', "{$counts['vehicles_without_drivers']} vehicle" . ($counts['vehicles_without_drivers'] === 1 ? ' does' : 's do') . ' not have an assigned driver.', 'warning', 'truck', 'management.vehicles', ['driver' => 'unassigned']);
180
+ }
181
+
182
+ if ($counts['vehicles_without_devices'] > 0 && $counts['unattached_devices'] > 0) {
183
+ $actions[] = $this->action('attach_devices_to_vehicles', 'Attach devices to vehicles', "{$counts['unattached_devices']} device" . ($counts['unattached_devices'] === 1 ? ' is' : 's are') . ' ready to attach to vehicles without telemetry hardware.', 'info', 'satellite-dish', 'connectivity.devices', ['attachment_state' => 'unattached']);
184
+ }
185
+
186
+ if ($counts['resource_issues'] > 0) {
187
+ $actions[] = $this->action('review_resource_issues', 'Review vehicle and driver issues', "{$counts['resource_issues']} issue" . ($counts['resource_issues'] === 1 ? ' is' : 's are') . ' linked to drivers or vehicles.', 'warning', 'triangle-exclamation', 'management.issues', ['status' => 'open']);
188
+ } elseif ($counts['issues'] > 0) {
189
+ $actions[] = $this->action('review_issues', 'Review open issues', "{$counts['issues']} issue" . ($counts['issues'] === 1 ? ' needs' : 's need') . ' follow-up.', 'warning', 'triangle-exclamation', 'management.issues', ['status' => 'open']);
190
+ }
191
+
192
+ if (($counts['overdue_vehicle_schedules'] + $counts['upcoming_vehicle_schedules']) > 0) {
193
+ $serviceCount = $counts['overdue_vehicle_schedules'] + $counts['upcoming_vehicle_schedules'];
194
+ $actions[] = $this->action('prepare_vehicle_maintenance', 'Prepare upcoming maintenance', "{$serviceCount} vehicle schedule" . ($serviceCount === 1 ? ' needs' : 's need') . ' maintenance planning.', $counts['overdue_vehicle_schedules'] > 0 ? 'warning' : 'info', 'calendar-day', 'maintenance.schedules', ['status' => 'active']);
195
+ }
196
+
197
+ if ($counts['overdue_resource_work_orders'] > 0) {
198
+ $actions[] = $this->action('close_overdue_work_orders', 'Close overdue work orders', "{$counts['overdue_resource_work_orders']} resource work order" . ($counts['overdue_resource_work_orders'] === 1 ? ' is' : 's are') . ' past due.', 'warning', 'clipboard-list', 'maintenance.work-orders', ['status' => 'open']);
199
+ } elseif ($counts['open_resource_work_orders'] > 0) {
200
+ $actions[] = $this->action('review_open_work_orders', 'Review open work orders', "{$counts['open_resource_work_orders']} resource work order" . ($counts['open_resource_work_orders'] === 1 ? ' is' : 's are') . ' open for vehicles or equipment.', 'info', 'clipboard-list', 'maintenance.work-orders', ['status' => 'open']);
201
+ }
202
+
203
+ if ($counts['low_stock_parts'] > 0) {
204
+ $actions[] = $this->action('replenish_low_stock_parts', 'Replenish low-stock parts', "{$counts['low_stock_parts']} stocked part" . ($counts['low_stock_parts'] === 1 ? ' is' : 's are') . ' at or below the default threshold.', 'warning', 'cog', 'maintenance.parts', ['status' => 'in_stock']);
205
+ }
206
+
207
+ if ($counts['unmatched_fuel_transactions'] > 0) {
208
+ $actions[] = $this->action('review_unmatched_fuel_transactions', 'Review unmatched fuel transactions', "{$counts['unmatched_fuel_transactions']} fuel transaction" . ($counts['unmatched_fuel_transactions'] === 1 ? ' needs' : 's need') . ' resource matching.', 'warning', 'gas-pump', 'management.fuel-transactions', ['sync_status' => 'unmatched']);
209
+ }
210
+
211
+ if ($counts['fleets'] === 0 && $counts['drivers'] > 0 && $counts['vehicles'] > 0) {
212
+ $actions[] = $this->action('create_fleets', 'Create fleet groups', 'Group active drivers and vehicles by team, region, or service type.', 'info', 'user-group', 'management.fleets');
213
+ }
214
+
215
+ if ($counts['places'] === 0) {
216
+ $actions[] = $this->action('add_operating_places', 'Add operating places', 'Places keep recurring pickup, dropoff, facility, and service locations ready.', 'info', 'location-dot', 'management.places');
217
+ }
218
+
219
+ if ($counts['vendors'] === 0) {
220
+ $actions[] = $this->action('add_service_vendors', 'Add vendors for service work', 'Vendors connect maintenance, service work, and operating partners to resources.', 'info', 'warehouse', 'management.vendors');
221
+ }
222
+
223
+ if ($counts['contacts'] === 0) {
224
+ $actions[] = $this->action('add_contacts', 'Add operating contacts', 'Contacts connect customers, partners, and service people to order workflows.', 'info', 'address-book', 'management.contacts');
225
+ }
226
+
227
+ if ($fuelRecords === 0) {
228
+ $actions[] = $this->action('review_fuel', 'Open fuel records', 'Fuel activity will appear here once reports or transactions are captured.', 'info', 'gas-pump', 'management.fuel-reports');
229
+ }
230
+
231
+ return $actions ? array_slice($actions, 0, 6) : [$this->action('ready', 'Core resources look ready', 'Drivers, vehicles, issues, and supporting records are in a healthy operating posture.', 'success', 'check-circle', null)];
232
+ }
233
+
234
+ protected function maintenanceActions(int $overdueSchedules, int $upcomingSchedules, int $openWorkOrders, int $overdueWorkOrders, int $highPriorityMaintenance, int $lowStockParts, int $equipment): array
235
+ {
236
+ $actions = [];
237
+
238
+ if ($overdueSchedules > 0) {
239
+ $actions[] = $this->action('overdue_schedules', 'Review overdue schedules', "{$overdueSchedules} recurring service schedule" . ($overdueSchedules === 1 ? ' is' : 's are') . ' overdue.', 'warning', 'calendar-xmark', 'maintenance.schedules');
240
+ }
241
+
242
+ if ($overdueWorkOrders > 0) {
243
+ $actions[] = $this->action('overdue_work_orders', 'Close overdue work orders', "{$overdueWorkOrders} work order" . ($overdueWorkOrders === 1 ? ' is' : 's are') . ' past due.', 'warning', 'clipboard-list', 'maintenance.work-orders');
244
+ }
245
+
246
+ if ($highPriorityMaintenance > 0) {
247
+ $actions[] = $this->action('high_priority_maintenance', 'Prioritize critical maintenance', "{$highPriorityMaintenance} high-priority maintenance record" . ($highPriorityMaintenance === 1 ? ' needs' : 's need') . ' review.', 'warning', 'wrench', 'maintenance.maintenances');
248
+ }
249
+
250
+ if ($upcomingSchedules > 0) {
251
+ $actions[] = $this->action('upcoming_service', 'Prepare upcoming service', "{$upcomingSchedules} schedule" . ($upcomingSchedules === 1 ? ' is' : 's are') . ' due in the next 7 days.', 'info', 'calendar-day', 'maintenance.schedules');
252
+ }
253
+
254
+ if ($openWorkOrders === 0) {
255
+ $actions[] = $this->action('no_open_work_orders', 'No open work orders', 'Create work orders when scheduled service or asset issues need assignment.', 'success', 'check-circle', 'maintenance.work-orders');
256
+ }
257
+
258
+ if ($lowStockParts > 0) {
259
+ $actions[] = $this->action('low_stock_parts', 'Replenish low-stock parts', "{$lowStockParts} stocked part" . ($lowStockParts === 1 ? ' is' : 's are') . ' at or below the default threshold.', 'warning', 'cog', 'maintenance.parts');
260
+ }
261
+
262
+ if ($equipment === 0) {
263
+ $actions[] = $this->action('add_equipment', 'Add serviceable equipment', 'Equipment records make maintenance planning more complete.', 'info', 'trailer', 'maintenance.equipment');
264
+ }
265
+
266
+ return array_slice($actions, 0, 5);
267
+ }
268
+
269
+ protected function count(Builder $query, ?string $companyUuid): int
270
+ {
271
+ return (int) $query->when($companyUuid, fn ($query) => $query->where('company_uuid', $companyUuid))->count();
272
+ }
273
+
274
+ protected function companyUuid(Request $request): ?string
275
+ {
276
+ return session('company') ?? $request->user()?->company_uuid ?? $request->user()?->company?->uuid;
277
+ }
278
+
279
+ protected function kpi(string $key, string $label, int $value, string $caption, string $tone, string $icon, string $route): array
280
+ {
281
+ return compact('key', 'label', 'value', 'caption', 'tone', 'icon', 'route');
282
+ }
283
+
284
+ protected function action(string $key, string $label, string $description, string $tone, string $icon, ?string $route, array $query = []): array
285
+ {
286
+ $query = (object) $query;
287
+
288
+ return compact('key', 'label', 'description', 'tone', 'icon', 'route', 'query');
289
+ }
290
+
291
+ protected function link(string $label, string $route, string $icon, int $count, string $description): array
292
+ {
293
+ return compact('label', 'route', 'icon', 'count', 'description');
294
+ }
295
+
296
+ protected function doc(string $label, string $icon, string $slug, string $title, string $description = ''): array
297
+ {
298
+ return compact('label', 'icon', 'slug', 'title', 'description');
299
+ }
300
+ }