> */ protected array $contextKeywords = [ 'clinical' => ['reception', 'consult', 'outpatient', 'general', 'doctor', 'nurse', 'triage', 'waiting'], 'pharmacy' => ['pharm', 'dispens', 'drug', 'rx'], 'laboratory' => ['lab', 'sample', 'patholog', 'investig'], 'dentistry' => ['dent', 'dental', 'oral'], 'ophthalmology' => ['eye', 'ophthal', 'optom'], 'physiotherapy' => ['physio', 'rehab', 'therapy'], 'maternity' => ['matern', 'antenatal', 'obstetric'], 'radiology' => ['radio', 'imaging', 'x-ray', 'xray', 'ultrasound', 'ct', 'mri'], ]; public function __construct( protected QueueClient $queue, protected CarePermissions $permissions, protected PlanService $plans, protected OrganizationResolver $resolver, ) {} /** * @return array{ * enabled: bool, * counters: list>, * counter_uuid: ?string, * state: ?array, * stubs: list>, * error: ?string, * context: string * }|null */ public function panel( Request $request, Organization $organization, ?Member $member, string $context = 'clinical', ?string $preferredCounterUuid = null, ): ?array { if (! $this->shouldShow($organization, $member)) { return null; } $owner = (string) $organization->owner_ref; $branchScope = $this->resolver->branchScope($member); $branchName = null; if ($branchScope) { $branchName = Branch::owned($owner)->where('id', $branchScope)->value('name'); } $stubs = $this->stubsForContext($organization, $context, $branchScope); if (! $this->queue->configured()) { return [ 'enabled' => true, 'counters' => [], 'counter_uuid' => null, 'state' => null, 'stubs' => $stubs, 'error' => 'Ladill Queue API is not configured on this Care instance.', 'context' => $context, ]; } try { $counters = $this->queue->counters($owner); } catch (RequestException $e) { if ($e->response?->status() === 403) { try { $this->queue->syncIntegration($owner, true); $counters = $this->queue->counters($owner); } catch (RequestException) { return [ 'enabled' => true, 'counters' => [], 'counter_uuid' => null, 'state' => null, 'stubs' => $stubs, 'error' => 'Queue integration is not linked. Re-save Settings with Queue enabled.', 'context' => $context, ]; } } else { return [ 'enabled' => true, 'counters' => [], 'counter_uuid' => null, 'state' => null, 'stubs' => $stubs, 'error' => 'Could not reach Ladill Queue.', 'context' => $context, ]; } } $stubCounterUuid = collect($stubs) ->map(fn ($s) => (string) ($s['counter_uuid'] ?? '')) ->first(fn ($uuid) => $uuid !== ''); $counters = $this->filterCounters($counters, $branchName, $context, $stubCounterUuid); $counterUuid = $preferredCounterUuid ?: ($stubCounterUuid ?: '') ?: (string) ($counters[0]['uuid'] ?? ''); if ($counterUuid !== '' && ! collect($counters)->contains(fn ($c) => ($c['uuid'] ?? '') === $counterUuid)) { $counterUuid = (string) ($counters[0]['uuid'] ?? ''); } $state = null; if ($counterUuid !== '') { try { $state = $this->queue->console($owner, $counterUuid); } catch (RequestException) { return [ 'enabled' => true, 'counters' => $counters, 'counter_uuid' => $counterUuid, 'state' => null, 'stubs' => $stubs, 'error' => 'Counter console unavailable.', 'context' => $context, ]; } } return [ 'enabled' => true, 'counters' => $counters, 'counter_uuid' => $counterUuid !== '' ? $counterUuid : null, 'state' => $state, 'stubs' => $stubs, 'error' => null, 'context' => $context, ]; } public function shouldShow(Organization $organization, ?Member $member): bool { if (! $member) { return false; } if (! $this->plans->canUseQueueIntegration($organization)) { return false; } if (! data_get($organization->settings, 'queue_integration_enabled')) { return false; } return $this->permissions->can($member, 'service_queues.console'); } /** * @param list> $counters * @return list> */ protected function filterCounters( array $counters, ?string $branchName, string $context, ?string $preferredUuid = null, ): array { $all = collect($counters); $filtered = $all; if ($preferredUuid) { $linked = $all->first(fn ($c) => ($c['uuid'] ?? '') === $preferredUuid); if ($linked) { return [$linked]; } } if ($branchName) { $branchFiltered = $filtered->filter(function ($counter) use ($branchName) { $counterBranch = (string) ($counter['branch'] ?? ''); return $counterBranch === '' || strcasecmp($counterBranch, $branchName) === 0; }); if ($branchFiltered->isNotEmpty()) { $filtered = $branchFiltered; } } $keywords = $this->keywordsFor($context); if ($keywords !== []) { $keywordFiltered = $filtered->filter(function ($counter) use ($keywords) { $queueNames = collect($counter['queues'] ?? [])->map(function ($q) { return is_array($q) ? (string) ($q['name'] ?? '') : (string) $q; })->implode(' '); $haystack = strtolower(implode(' ', [ (string) ($counter['name'] ?? ''), $queueNames, ])); foreach ($keywords as $keyword) { if ($keyword !== '' && str_contains($haystack, $keyword)) { return true; } } return false; }); // Fall back to branch-scoped counters when no keyword match (demo / generic counters). if ($keywordFiltered->isNotEmpty()) { $filtered = $keywordFiltered; } } return $filtered->values()->all(); } /** * @return list */ protected function keywordsFor(string $context): array { if (isset($this->contextKeywords[$context])) { return $this->contextKeywords[$context]; } $module = config("care.specialty_modules.{$context}.queue_keywords"); return is_array($module) ? $module : []; } /** * @return list> */ protected function stubsForContext(Organization $organization, string $context, ?int $branchId): array { if (! array_key_exists($context, config('care.specialty_modules', []))) { return []; } $queues = data_get($organization->settings, "specialty_module_provisioning.{$context}.queues", []); if (! is_array($queues)) { return []; } return array_values(array_filter($queues, function ($queue) use ($branchId) { if (! ($queue['active'] ?? false)) { return false; } if ($branchId !== null && (int) ($queue['branch_id'] ?? 0) !== $branchId) { return false; } return true; })); } }