Deploy Ladill Care / deploy (push) Successful in 1m28s
Issue, call-next, serve, and complete tickets locally so Care Pro no longer needs QUEUE_API_* when CARE_QUEUE_DRIVER=native (default). Remote Ladill Queue remains available for cutover; PHPUnit keeps the remote driver for existing fakes. Co-authored-by: Cursor <cursoragent@cursor.com>
328 lines
11 KiB
PHP
328 lines
11 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 (config('care.queue.driver', 'native') === 'native') {
|
|
$counters = $this->nativeCountersFromStubs($stubs, $context);
|
|
$stubCounterUuid = collect($stubs)
|
|
->map(fn ($s) => (string) ($s['counter_uuid'] ?? ''))
|
|
->first(fn ($uuid) => $uuid !== '');
|
|
$counterUuid = $preferredCounterUuid
|
|
?: ($stubCounterUuid ?: '')
|
|
?: (string) ($counters[0]['uuid'] ?? '');
|
|
|
|
return [
|
|
'enabled' => true,
|
|
'counters' => $counters,
|
|
'counter_uuid' => $counterUuid !== '' ? $counterUuid : null,
|
|
'state' => null,
|
|
'stubs' => $stubs,
|
|
'error' => null,
|
|
'context' => $context,
|
|
];
|
|
}
|
|
|
|
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<array<string, mixed>> $counters
|
|
* @return list<array<string, mixed>>
|
|
*/
|
|
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();
|
|
}
|
|
|
|
/**
|
|
* @param list<array<string, mixed>> $stubs
|
|
* @return list<array<string, mixed>>
|
|
*/
|
|
protected function nativeCountersFromStubs(array $stubs, string $context): array
|
|
{
|
|
$counters = [];
|
|
foreach ($stubs as $stub) {
|
|
$branchName = $stub['branch_name'] ?? null;
|
|
$queueName = $stub['name'] ?? $context;
|
|
$points = is_array($stub['points'] ?? null) ? $stub['points'] : [];
|
|
|
|
foreach ($points as $point) {
|
|
$uuid = (string) ($point['counter_uuid'] ?? '');
|
|
if ($uuid === '') {
|
|
continue;
|
|
}
|
|
$counters[] = [
|
|
'uuid' => $uuid,
|
|
'name' => (string) ($point['name'] ?? $point['destination'] ?? 'Desk'),
|
|
'branch' => $branchName,
|
|
'queues' => [['name' => $queueName]],
|
|
];
|
|
}
|
|
|
|
if ($points === [] && ! empty($stub['counter_uuid'])) {
|
|
$counters[] = [
|
|
'uuid' => (string) $stub['counter_uuid'],
|
|
'name' => (string) $queueName,
|
|
'branch' => $branchName,
|
|
'queues' => [['name' => $queueName]],
|
|
];
|
|
}
|
|
}
|
|
|
|
return $counters;
|
|
}
|
|
|
|
/**
|
|
* @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;
|
|
}));
|
|
}
|
|
}
|