Deploy Ladill Care / deploy (push) Successful in 53s
Remove the standalone Service Queues nav so call-next/now-serving lives on clinical queue, appointments, pharmacy, lab, and specialty surfaces; Pro orgs can activate dentistry and related modules with branch departments and queue stubs. Co-authored-by: Cursor <cursoragent@cursor.com>
250 lines
8.1 KiB
PHP
250 lines
8.1 KiB
PHP
<?php
|
|
|
|
namespace App\Services\Care;
|
|
|
|
use App\Models\Branch;
|
|
use App\Models\Member;
|
|
use App\Models\Organization;
|
|
use App\Services\Queue\QueueClient;
|
|
use Illuminate\Http\Client\RequestException;
|
|
use Illuminate\Http\Request;
|
|
|
|
/**
|
|
* Loads Ladill Queue console state for embedding on role-native pages
|
|
* (clinical queue, pharmacy, lab, specialty modules) instead of a standalone Service Queues nav.
|
|
*/
|
|
class ServiceQueuePresenter
|
|
{
|
|
/** @var array<string, list<string>> */
|
|
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<array<string, mixed>>,
|
|
* counter_uuid: ?string,
|
|
* state: ?array<string, mixed>,
|
|
* stubs: list<array<string, mixed>>,
|
|
* 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,
|
|
];
|
|
}
|
|
}
|
|
|
|
$counters = $this->filterCounters($counters, $branchName, $context);
|
|
$counterUuid = $preferredCounterUuid
|
|
?: (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<array<string, mixed>> $counters
|
|
* @return list<array<string, mixed>>
|
|
*/
|
|
protected function filterCounters(array $counters, ?string $branchName, string $context): array
|
|
{
|
|
$keywords = $this->keywordsFor($context);
|
|
|
|
$filtered = collect($counters);
|
|
|
|
if ($branchName) {
|
|
$branchFiltered = $filtered->filter(function ($counter) use ($branchName) {
|
|
$counterBranch = (string) ($counter['branch'] ?? '');
|
|
|
|
return $counterBranch === '' || strcasecmp($counterBranch, $branchName) === 0;
|
|
});
|
|
if ($branchFiltered->isNotEmpty()) {
|
|
$filtered = $branchFiltered;
|
|
}
|
|
}
|
|
|
|
if ($keywords !== []) {
|
|
$keywordFiltered = $filtered->filter(function ($counter) use ($keywords) {
|
|
$haystack = strtolower(implode(' ', [
|
|
(string) ($counter['name'] ?? ''),
|
|
collect($counter['queues'] ?? [])->pluck('name')->implode(' '),
|
|
]));
|
|
|
|
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<string>
|
|
*/
|
|
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<array<string, mixed>>
|
|
*/
|
|
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;
|
|
}));
|
|
}
|
|
}
|