authorizeAbility($request, 'devices.view'); $organization = $this->organization($request); $branchScope = app(OrganizationResolver::class)->branchScope($this->member($request)); $query = Device::owned($this->ownerRef($request)) ->where('organization_id', $organization->id) ->with('branch') ->when($branchScope, fn ($q) => $q->where('branch_id', $branchScope)) ->orderBy('name'); $devices = (clone $query)->paginate(25); $heroStats = [ 'total' => (clone $query)->count(), 'online' => (clone $query) ->where('status', Device::STATUS_ACTIVE) ->where('last_seen_at', '>=', now()->subMinutes(10)) ->count(), 'agent' => (clone $query)->where('connection_mode', Device::MODE_AGENT)->count(), ]; return view('care.devices.index', [ 'organization' => $organization, 'devices' => $devices, 'deviceTypes' => config('care.device_types'), 'connectionModes' => config('care.device_connection_modes'), 'heroStats' => $heroStats, 'canManage' => app(\App\Services\Care\CarePermissions::class) ->can($this->member($request), 'devices.manage'), 'hasAgentDevices' => $this->plans->hasPaidPlan($organization), ]); } public function create(Request $request): View { $this->authorizeAbility($request, 'devices.manage'); $organization = $this->organization($request); return view('care.devices.create', [ 'organization' => $organization, 'deviceTypes' => config('care.device_types'), 'connectionModes' => config('care.device_connection_modes'), 'branches' => $this->branches($request, $organization->id), 'hasAgentDevices' => $this->plans->hasPaidPlan($organization), 'agentTypes' => config('care.device_agent_types'), 'defaultModes' => collect(config('care.device_types')) ->keys() ->mapWithKeys(fn ($type) => [$type => $this->devices->defaultConnectionMode($type)]) ->all(), ]); } public function store(Request $request): RedirectResponse { $this->authorizeAbility($request, 'devices.manage'); $organization = $this->organization($request); $validated = $this->validatedDevice($request, $organization); if (! $this->devices->canRegisterType($organization, $validated['type'], $this->plans)) { return back()->withInput()->with( 'error', 'Agent-connected clinical devices require Care Pro or Enterprise. USB barcode/QR scanners work on Free.', ); } $mode = $validated['connection_mode'] ?? $this->devices->defaultConnectionMode($validated['type']); $device = Device::create([ 'owner_ref' => $this->ownerRef($request), 'organization_id' => $organization->id, 'branch_id' => $validated['branch_id'] ?? null, 'name' => $validated['name'], 'type' => $validated['type'], 'connection_mode' => $mode, 'status' => Device::STATUS_OFFLINE, 'metadata' => [], ]); $plainToken = null; if ($mode === Device::MODE_AGENT || in_array($validated['type'], config('care.device_agent_types', []), true)) { $plainToken = $this->devices->issueToken($device); } $redirect = redirect()->route('care.devices.edit', $device) ->with('success', 'Device registered.'); if ($plainToken) { $redirect->with('device_token', $plainToken); } return $redirect; } public function edit(Request $request, Device $device): View { $this->authorizeAbility($request, 'devices.manage'); $this->authorizeDevice($request, $device); return view('care.devices.edit', [ 'device' => $device, 'organization' => $this->organization($request), 'deviceTypes' => config('care.device_types'), 'connectionModes' => config('care.device_connection_modes'), 'deviceStatuses' => config('care.device_statuses'), 'branches' => $this->branches($request, $device->organization_id), 'hasAgentDevices' => $this->plans->hasPaidPlan($this->organization($request)), 'agentTypes' => config('care.device_agent_types'), 'connectionHint' => $this->devices->connectionHint($device->type, $device->connection_mode), 'plainToken' => session('device_token'), ]); } public function update(Request $request, Device $device): RedirectResponse { $this->authorizeAbility($request, 'devices.manage'); $this->authorizeDevice($request, $device); $organization = $this->organization($request); $validated = $this->validatedDevice($request, $organization, updating: true); if ( isset($validated['type']) && $validated['type'] !== $device->type && ! $this->devices->canRegisterType($organization, $validated['type'], $this->plans) ) { return back()->withInput()->with( 'error', 'Agent-connected clinical devices require Care Pro or Enterprise.', ); } $device->update([ 'name' => $validated['name'], 'type' => $validated['type'], 'branch_id' => $validated['branch_id'] ?? null, 'connection_mode' => $validated['connection_mode'] ?? $device->connection_mode ?? $this->devices->defaultConnectionMode($validated['type']), 'status' => $validated['status'] ?? $device->status, ]); return redirect()->route('care.devices.index')->with('success', 'Device updated.'); } public function destroy(Request $request, Device $device): RedirectResponse { $this->authorizeAbility($request, 'devices.manage'); $this->authorizeDevice($request, $device); $device->delete(); return redirect()->route('care.devices.index')->with('success', 'Device removed.'); } public function regenerateToken(Request $request, Device $device): RedirectResponse { $this->authorizeAbility($request, 'devices.manage'); $this->authorizeDevice($request, $device); $plain = $this->devices->issueToken($device); return back() ->with('success', 'Device token regenerated. Copy it now — it will not be shown again.') ->with('device_token', $plain); } public function revokeToken(Request $request, Device $device): RedirectResponse { $this->authorizeAbility($request, 'devices.manage'); $this->authorizeDevice($request, $device); $this->devices->revokeToken($device); return back()->with('success', 'Device token revoked.'); } protected function authorizeDevice(Request $request, Device $device): void { $this->authorizeOwner($request, $device); abort_unless($device->organization_id === $this->organization($request)->id, 404); $branchId = app(OrganizationResolver::class)->branchScope($this->member($request)); if ($branchId !== null && $device->branch_id !== null && $device->branch_id !== $branchId) { abort(404); } } /** * @return array */ protected function validatedDevice(Request $request, $organization, bool $updating = false): array { $rules = [ 'name' => ['required', 'string', 'max:255'], 'type' => ['required', 'string', 'in:'.implode(',', array_keys(config('care.device_types')))], 'branch_id' => ['nullable', 'integer', 'exists:care_branches,id'], 'connection_mode' => ['nullable', 'string', 'in:'.implode(',', array_keys(config('care.device_connection_modes')))], ]; if ($updating) { $rules['status'] = ['nullable', 'string', 'in:'.implode(',', array_keys(config('care.device_statuses')))]; } $validated = $request->validate($rules); if (! empty($validated['branch_id'])) { $ok = Branch::owned($this->ownerRef($request)) ->where('organization_id', $organization->id) ->where('id', $validated['branch_id']) ->exists(); abort_unless($ok, 422); } return $validated; } /** @return \Illuminate\Database\Eloquent\Collection */ protected function branches(Request $request, int $organizationId) { $branchScope = app(OrganizationResolver::class)->branchScope($this->member($request)); return Branch::owned($this->ownerRef($request)) ->where('organization_id', $organizationId) ->where('is_active', true) ->when($branchScope, fn ($q) => $q->where('id', $branchScope)) ->orderBy('name') ->get(); } }