Wire Care lists into Ladill Queue workflows instead of reception panels.
Deploy Ladill Care / deploy (push) Successful in 1m45s
Deploy Ladill Care / deploy (push) Successful in 1m45s
When Queue integration is on, role pages issue tickets into their own department queues and drive Call next → Serve → Complete on the native lists. Integration off leaves existing UI unchanged. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,386 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Care;
|
||||
|
||||
use App\Models\Appointment;
|
||||
use App\Models\Bill;
|
||||
use App\Models\InvestigationRequest;
|
||||
use App\Models\Organization;
|
||||
use App\Models\Patient;
|
||||
use App\Models\Prescription;
|
||||
use App\Services\Queue\QueueClient;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Http\Client\RequestException;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
/**
|
||||
* Bridges Care workflow entities to Ladill Queue tickets for the matching department queue.
|
||||
*/
|
||||
class CareQueueBridge
|
||||
{
|
||||
public function __construct(
|
||||
protected QueueClient $queue,
|
||||
protected CareQueueProvisioner $provisioner,
|
||||
protected PlanService $plans,
|
||||
protected SpecialtyModuleService $specialties,
|
||||
) {}
|
||||
|
||||
public function isEnabled(Organization $organization): bool
|
||||
{
|
||||
return $this->plans->canUseQueueIntegration($organization)
|
||||
&& (bool) data_get($organization->settings, 'queue_integration_enabled')
|
||||
&& $this->queue->configured();
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve which queue context an appointment should use (specialty if applicable).
|
||||
*/
|
||||
public function contextForAppointment(Organization $organization, Appointment $appointment): string
|
||||
{
|
||||
$departmentId = $appointment->department_id;
|
||||
if (! $departmentId) {
|
||||
return CareQueueContexts::CONSULTATION;
|
||||
}
|
||||
|
||||
foreach ($this->specialties->enabledKeys($organization) as $key) {
|
||||
$deptIds = data_get($organization->settings, "specialty_module_provisioning.{$key}.department_ids", []);
|
||||
if (is_array($deptIds) && in_array($departmentId, array_map('intval', $deptIds), true)) {
|
||||
return $key;
|
||||
}
|
||||
}
|
||||
|
||||
return CareQueueContexts::CONSULTATION;
|
||||
}
|
||||
|
||||
public function issueForAppointment(Organization $organization, Appointment $appointment): ?Appointment
|
||||
{
|
||||
if (! $this->isEnabled($organization) || filled($appointment->queue_ticket_uuid)) {
|
||||
return $appointment;
|
||||
}
|
||||
|
||||
$context = $this->contextForAppointment($organization, $appointment);
|
||||
$ticket = $this->issue(
|
||||
$organization,
|
||||
$context,
|
||||
(int) $appointment->branch_id,
|
||||
$appointment->patient,
|
||||
[
|
||||
'care_entity' => 'appointment',
|
||||
'care_entity_uuid' => $appointment->uuid,
|
||||
'care_entity_id' => $appointment->id,
|
||||
],
|
||||
$appointment->type === Appointment::TYPE_WALK_IN ? 'walk_in' : 'appointment',
|
||||
'appointment',
|
||||
);
|
||||
|
||||
if (! $ticket) {
|
||||
return $appointment;
|
||||
}
|
||||
|
||||
$this->applyTicket($appointment, $ticket);
|
||||
|
||||
return $appointment->fresh();
|
||||
}
|
||||
|
||||
public function issueForPrescription(Organization $organization, Prescription $prescription): ?Prescription
|
||||
{
|
||||
if (! $this->isEnabled($organization) || filled($prescription->queue_ticket_uuid)) {
|
||||
return $prescription;
|
||||
}
|
||||
|
||||
$prescription->loadMissing(['patient', 'visit']);
|
||||
$branchId = (int) ($prescription->visit?->branch_id ?? 0);
|
||||
if ($branchId < 1) {
|
||||
return $prescription;
|
||||
}
|
||||
|
||||
$ticket = $this->issue(
|
||||
$organization,
|
||||
CareQueueContexts::PHARMACY,
|
||||
$branchId,
|
||||
$prescription->patient,
|
||||
[
|
||||
'care_entity' => 'prescription',
|
||||
'care_entity_uuid' => $prescription->uuid,
|
||||
'care_entity_id' => $prescription->id,
|
||||
],
|
||||
);
|
||||
|
||||
if (! $ticket) {
|
||||
return $prescription;
|
||||
}
|
||||
|
||||
$this->applyTicket($prescription, $ticket);
|
||||
|
||||
return $prescription->fresh();
|
||||
}
|
||||
|
||||
public function issueForInvestigation(Organization $organization, InvestigationRequest $request): ?InvestigationRequest
|
||||
{
|
||||
if (! $this->isEnabled($organization) || filled($request->queue_ticket_uuid)) {
|
||||
return $request;
|
||||
}
|
||||
|
||||
$request->loadMissing('patient');
|
||||
$ticket = $this->issue(
|
||||
$organization,
|
||||
CareQueueContexts::LABORATORY,
|
||||
(int) $request->branch_id,
|
||||
$request->patient,
|
||||
[
|
||||
'care_entity' => 'investigation_request',
|
||||
'care_entity_uuid' => $request->uuid,
|
||||
'care_entity_id' => $request->id,
|
||||
],
|
||||
);
|
||||
|
||||
if (! $ticket) {
|
||||
return $request;
|
||||
}
|
||||
|
||||
$this->applyTicket($request, $ticket);
|
||||
|
||||
return $request->fresh();
|
||||
}
|
||||
|
||||
public function issueForBill(Organization $organization, Bill $bill): ?Bill
|
||||
{
|
||||
if (! $this->isEnabled($organization) || filled($bill->queue_ticket_uuid)) {
|
||||
return $bill;
|
||||
}
|
||||
|
||||
if (in_array($bill->status, [Bill::STATUS_PAID, Bill::STATUS_VOID], true)) {
|
||||
return $bill;
|
||||
}
|
||||
|
||||
$bill->loadMissing('patient');
|
||||
$ticket = $this->issue(
|
||||
$organization,
|
||||
CareQueueContexts::BILLING,
|
||||
(int) $bill->branch_id,
|
||||
$bill->patient,
|
||||
[
|
||||
'care_entity' => 'bill',
|
||||
'care_entity_uuid' => $bill->uuid,
|
||||
'care_entity_id' => $bill->id,
|
||||
],
|
||||
);
|
||||
|
||||
if (! $ticket) {
|
||||
return $bill;
|
||||
}
|
||||
|
||||
$this->applyTicket($bill, $ticket);
|
||||
|
||||
return $bill->fresh();
|
||||
}
|
||||
|
||||
/**
|
||||
* Call the next waiting ticket for a Care page context + branch.
|
||||
*
|
||||
* @return array{ticket: ?array<string, mixed>, entity: ?Model, resources: ?array<string, mixed>}
|
||||
*/
|
||||
public function callNext(Organization $organization, string $context, int $branchId): array
|
||||
{
|
||||
$resources = $this->provisioner->ensure($organization, $context, $branchId);
|
||||
if (! $resources || empty($resources['queue_uuid']) || empty($resources['counter_uuid'])) {
|
||||
return ['ticket' => null, 'entity' => null, 'resources' => $resources];
|
||||
}
|
||||
|
||||
$owner = (string) $organization->owner_ref;
|
||||
|
||||
try {
|
||||
$ticket = $this->queue->callNext(
|
||||
$owner,
|
||||
(string) $resources['queue_uuid'],
|
||||
(string) $resources['counter_uuid'],
|
||||
);
|
||||
} catch (RequestException $e) {
|
||||
Log::warning('care.queue_call_next_failed', [
|
||||
'context' => $context,
|
||||
'branch_id' => $branchId,
|
||||
'message' => $e->getMessage(),
|
||||
]);
|
||||
|
||||
return ['ticket' => null, 'entity' => null, 'resources' => $resources];
|
||||
}
|
||||
|
||||
if (! $ticket) {
|
||||
return ['ticket' => null, 'entity' => null, 'resources' => $resources];
|
||||
}
|
||||
|
||||
$entity = $this->findEntityByTicket($organization, $context, (string) ($ticket['uuid'] ?? ''));
|
||||
if ($entity) {
|
||||
$this->applyTicket($entity, $ticket);
|
||||
}
|
||||
|
||||
return ['ticket' => $ticket, 'entity' => $entity?->fresh(), 'resources' => $resources];
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark ticket as serving (Queue "start").
|
||||
*
|
||||
* @return array<string, mixed>|null
|
||||
*/
|
||||
public function serve(Organization $organization, Model $entity): ?array
|
||||
{
|
||||
return $this->ticketAction($organization, $entity, 'start');
|
||||
}
|
||||
|
||||
/**
|
||||
* Complete the Queue ticket linked to a Care entity.
|
||||
*
|
||||
* @return array<string, mixed>|null
|
||||
*/
|
||||
public function complete(Organization $organization, Model $entity): ?array
|
||||
{
|
||||
return $this->ticketAction($organization, $entity, 'complete');
|
||||
}
|
||||
|
||||
/**
|
||||
* Recall the Queue ticket linked to a Care entity.
|
||||
*
|
||||
* @return array<string, mixed>|null
|
||||
*/
|
||||
public function recall(Organization $organization, Model $entity): ?array
|
||||
{
|
||||
return $this->ticketAction($organization, $entity, 'recall');
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{enabled: bool, context: string, queue_uuid: ?string, counter_uuid: ?string}
|
||||
*/
|
||||
public function listMeta(Organization $organization, string $context, int $branchId): array
|
||||
{
|
||||
if (! $this->isEnabled($organization)) {
|
||||
return [
|
||||
'enabled' => false,
|
||||
'context' => $context,
|
||||
'queue_uuid' => null,
|
||||
'counter_uuid' => null,
|
||||
];
|
||||
}
|
||||
|
||||
$resources = $this->provisioner->ensure($organization->fresh(), $context, $branchId);
|
||||
|
||||
return [
|
||||
'enabled' => true,
|
||||
'context' => $context,
|
||||
'queue_uuid' => $resources['queue_uuid'] ?? null,
|
||||
'counter_uuid' => $resources['counter_uuid'] ?? null,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $metadata
|
||||
* @return array<string, mixed>|null
|
||||
*/
|
||||
protected function issue(
|
||||
Organization $organization,
|
||||
string $context,
|
||||
int $branchId,
|
||||
?Patient $patient,
|
||||
array $metadata = [],
|
||||
string $priority = 'walk_in',
|
||||
string $source = 'api',
|
||||
): ?array {
|
||||
$resources = $this->provisioner->ensure($organization, $context, $branchId);
|
||||
if (! $resources || empty($resources['queue_uuid'])) {
|
||||
Log::info('care.queue_issue_skipped_no_queue', [
|
||||
'context' => $context,
|
||||
'branch_id' => $branchId,
|
||||
]);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
$owner = (string) $organization->owner_ref;
|
||||
|
||||
try {
|
||||
return $this->queue->issueTicket($owner, [
|
||||
'service_queue_id' => $resources['queue_uuid'],
|
||||
'customer_name' => $patient?->fullName(),
|
||||
'customer_phone' => $patient?->phone,
|
||||
'priority' => $priority,
|
||||
'source' => $source,
|
||||
'metadata' => $metadata,
|
||||
]);
|
||||
} catch (\Throwable $e) {
|
||||
Log::warning('care.queue_issue_failed', [
|
||||
'context' => $context,
|
||||
'branch_id' => $branchId,
|
||||
'message' => $e->getMessage(),
|
||||
]);
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $ticket
|
||||
*/
|
||||
protected function applyTicket(Model $entity, array $ticket): void
|
||||
{
|
||||
$entity->forceFill([
|
||||
'queue_ticket_uuid' => $ticket['uuid'] ?? $entity->getAttribute('queue_ticket_uuid'),
|
||||
'queue_ticket_number' => $ticket['ticket_number'] ?? $entity->getAttribute('queue_ticket_number'),
|
||||
'queue_ticket_status' => $ticket['status'] ?? $entity->getAttribute('queue_ticket_status'),
|
||||
])->save();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>|null
|
||||
*/
|
||||
protected function ticketAction(Organization $organization, Model $entity, string $action): ?array
|
||||
{
|
||||
$uuid = (string) $entity->getAttribute('queue_ticket_uuid');
|
||||
if ($uuid === '' || ! $this->isEnabled($organization)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
$ticket = $this->queue->ticketAction((string) $organization->owner_ref, $uuid, [
|
||||
'action' => $action,
|
||||
]);
|
||||
$this->applyTicket($entity, $ticket);
|
||||
|
||||
return $ticket;
|
||||
} catch (\Throwable $e) {
|
||||
Log::warning('care.queue_ticket_action_failed', [
|
||||
'action' => $action,
|
||||
'ticket_uuid' => $uuid,
|
||||
'message' => $e->getMessage(),
|
||||
]);
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
protected function findEntityByTicket(Organization $organization, string $context, string $ticketUuid): ?Model
|
||||
{
|
||||
if ($ticketUuid === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
$owner = (string) $organization->owner_ref;
|
||||
|
||||
return match (true) {
|
||||
$context === CareQueueContexts::PHARMACY => Prescription::owned($owner)
|
||||
->where('organization_id', $organization->id)
|
||||
->where('queue_ticket_uuid', $ticketUuid)
|
||||
->first(),
|
||||
$context === CareQueueContexts::LABORATORY => InvestigationRequest::owned($owner)
|
||||
->where('organization_id', $organization->id)
|
||||
->where('queue_ticket_uuid', $ticketUuid)
|
||||
->first(),
|
||||
$context === CareQueueContexts::BILLING => Bill::owned($owner)
|
||||
->where('organization_id', $organization->id)
|
||||
->where('queue_ticket_uuid', $ticketUuid)
|
||||
->first(),
|
||||
default => Appointment::owned($owner)
|
||||
->where('organization_id', $organization->id)
|
||||
->where('queue_ticket_uuid', $ticketUuid)
|
||||
->first(),
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user