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); $all = (clone $query)->get(); $heroStats = [ 'total' => $all->count(), 'online' => $all->filter(fn (Device $d) => $d->isOnline())->count(), 'kiosks' => $all->where('type', 'kiosk')->count(), 'displays' => $all->where('type', 'display')->count(), 'agent' => $all->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(CarePermissions::class) ->can($this->member($request), 'devices.manage'), 'hasAgentDevices' => $this->plans->hasPaidPlan($organization), 'hasQueueDevices' => $this->plans->hasPaidPlan($organization), ]); } public function create(Request $request): View { $this->authorizeAbility($request, 'devices.manage'); $organization = $this->organization($request); $owner = $this->ownerRef($request); $paid = $this->plans->hasPaidPlan($organization); return view('care.devices.create', [ 'organization' => $organization, 'deviceTypes' => config('care.device_types'), 'connectionModes' => config('care.device_connection_modes'), 'branches' => $this->branches($request, $organization->id), 'queues' => CareServiceQueue::owned($owner) ->where('organization_id', $organization->id) ->where('is_active', true) ->orderBy('name') ->get(), 'layouts' => config('care.display_layouts'), 'hasAgentDevices' => $paid, 'hasQueueDevices' => $paid, 'agentTypes' => config('care.device_agent_types'), 'queueDeviceTypes' => config('care.queue_device_types', []), 'defaultModes' => collect(config('care.device_types')) ->keys() ->mapWithKeys(fn ($type) => [$type => $this->devices->defaultConnectionMode($type)]) ->all(), 'prefillType' => $request->query('type'), ]); } public function store(Request $request): RedirectResponse { $this->authorizeAbility($request, 'devices.manage'); $organization = $this->organization($request); $owner = $this->ownerRef($request); $validated = $this->validatedDevice($request, $organization); if (! $this->devices->canRegisterType($organization, $validated['type'], $this->plans)) { $message = $this->devices->isQueueDeviceType($validated['type']) ? 'Queue kiosks and displays require Care Pro or Enterprise.' : 'Agent-connected clinical devices require Care Pro or Enterprise. USB barcode/QR scanners work on Free.'; return back()->withInput()->with('error', $message); } $mode = $validated['connection_mode'] ?? $this->devices->defaultConnectionMode($validated['type']); $metadata = []; if ($validated['type'] === 'kiosk') { $allowed = CareServiceQueue::owned($owner) ->where('organization_id', $organization->id) ->pluck('id') ->all(); $metadata = $this->kiosk->buildMetadata($validated, $allowed); } if ($validated['type'] === 'display') { if (empty($validated['branch_id'])) { return back()->withInput()->with('error', 'Queue displays require a branch.'); } $queueIds = array_map('intval', $validated['queue_ids'] ?? []); $allowed = CareServiceQueue::owned($owner) ->where('organization_id', $organization->id) ->whereIn('id', $queueIds) ->pluck('id') ->all(); $screen = CareDisplayScreen::create([ 'owner_ref' => $owner, 'organization_id' => $organization->id, 'branch_id' => $validated['branch_id'], 'name' => $validated['name'], 'layout' => $validated['layout'] ?? 'standard', 'service_queue_ids' => array_values($allowed), 'is_active' => true, ]); $metadata = [ 'display_screen_id' => $screen->id, 'display_screen_uuid' => $screen->uuid, 'service_queue_ids' => array_values($allowed), ]; } $device = Device::create([ 'owner_ref' => $owner, 'organization_id' => $organization->id, 'branch_id' => $validated['branch_id'] ?? null, 'name' => $validated['name'], 'type' => $validated['type'], 'connection_mode' => $mode, 'status' => Device::STATUS_OFFLINE, 'metadata' => $metadata, ]); $plainToken = null; $needsToken = $this->devices->isQueueDeviceType($validated['type']) || $mode === Device::MODE_AGENT || in_array($validated['type'], config('care.device_agent_types', []), true); if ($needsToken) { $plainToken = $this->devices->issueToken($device->fresh()); } $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); $organization = $this->organization($request); $owner = $this->ownerRef($request); $displayScreen = null; $displayPublicUrl = null; if ($device->type === 'display' && ! empty($device->metadata['display_screen_id'])) { $displayScreen = CareDisplayScreen::query()->find($device->metadata['display_screen_id']); if ($displayScreen) { $displayPublicUrl = route('care.display.public', $displayScreen->access_token); } } $kioskUrl = null; if ($device->type === 'kiosk') { $token = $this->devices->publicAccessToken($device) ?? session('device_token'); if ($token) { $kioskUrl = route('care.kiosk.device', $token); } } return view('care.devices.edit', [ 'device' => $device, 'organization' => $organization, 'deviceTypes' => config('care.device_types'), 'connectionModes' => config('care.device_connection_modes'), 'deviceStatuses' => config('care.device_statuses'), 'branches' => $this->branches($request, $device->organization_id), 'queues' => CareServiceQueue::owned($owner) ->where('organization_id', $organization->id) ->where('is_active', true) ->orderBy('name') ->get(), 'layouts' => config('care.display_layouts'), 'hasAgentDevices' => $this->plans->hasPaidPlan($organization), 'hasQueueDevices' => $this->plans->hasPaidPlan($organization), 'agentTypes' => config('care.device_agent_types'), 'queueDeviceTypes' => config('care.queue_device_types', []), 'connectionHint' => $this->devices->connectionHint($device->type, $device->connection_mode), 'plainToken' => session('device_token'), 'kioskUrl' => $kioskUrl, 'displayScreen' => $displayScreen, 'displayPublicUrl' => $displayPublicUrl, 'kioskSettings' => $device->type === 'kiosk' ? $this->kiosk->settings($device) : null, ]); } public function update(Request $request, Device $device): RedirectResponse { $this->authorizeAbility($request, 'devices.manage'); $this->authorizeDevice($request, $device); $organization = $this->organization($request); $owner = $this->ownerRef($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', 'That device type requires Care Pro or Enterprise.', ); } $metadata = $device->metadata ?? []; if (($validated['type'] ?? $device->type) === 'kiosk') { $allowed = CareServiceQueue::owned($owner) ->where('organization_id', $organization->id) ->pluck('id') ->all(); $metadata = array_merge( $metadata, $this->kiosk->buildMetadata($validated, $allowed), ); if ($access = $this->devices->publicAccessToken($device)) { $metadata['access_token'] = $access; } } if (($validated['type'] ?? $device->type) === 'display') { $queueIds = array_map('intval', $validated['queue_ids'] ?? ($metadata['service_queue_ids'] ?? [])); $allowed = CareServiceQueue::owned($owner) ->where('organization_id', $organization->id) ->whereIn('id', $queueIds) ->pluck('id') ->all(); $metadata['service_queue_ids'] = array_values($allowed); if (! empty($metadata['display_screen_id'])) { CareDisplayScreen::query()->where('id', $metadata['display_screen_id'])->update([ 'name' => $validated['name'], 'branch_id' => $validated['branch_id'] ?? null, 'layout' => $validated['layout'] ?? 'standard', 'service_queue_ids' => array_values($allowed), ]); } } $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, 'metadata' => $metadata, ]); 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')))], 'queue_ids' => ['nullable', 'array'], 'queue_ids.*' => ['integer', 'exists:care_service_queues,id'], 'collect_name' => ['nullable', 'boolean'], 'collect_phone' => ['nullable', 'boolean'], 'reset_seconds' => ['nullable', 'integer', 'min:5', 'max:120'], 'welcome_message' => ['nullable', 'string', 'max:255'], 'layout' => ['nullable', 'string', 'in:'.implode(',', array_keys(config('care.display_layouts', ['standard' => 'Standard'])))], ]; 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); } $validated['collect_name'] = $request->boolean('collect_name'); $validated['collect_phone'] = $request->boolean('collect_phone'); 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(); } }