Issue Queue tickets for every waiting appointment when integration is on.
Deploy Ladill Care / deploy (push) Successful in 51s
Deploy Ladill Care / deploy (push) Successful in 51s
Patient queue syncs consultation and specialty waiters; specialty contexts now provision desks so check-in/onboarding no longer leaves half the list on local positions only. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -39,8 +39,8 @@ class QueueController extends Controller
|
|||||||
|
|
||||||
$queueIntegration = ['enabled' => false];
|
$queueIntegration = ['enabled' => false];
|
||||||
if ($branchId && $this->queueBridge->isEnabled($organization)) {
|
if ($branchId && $this->queueBridge->isEnabled($organization)) {
|
||||||
// Catch up tickets for waiters created before Queue was linked (demo / pre-enable).
|
// Catch up tickets for every waiting appointment (consultation + specialties).
|
||||||
$this->queueBridge->syncMissingTickets($organization, CareQueueContexts::CONSULTATION, $branchId, 50);
|
$this->queueBridge->syncMissingAppointmentTickets($organization, $branchId, 100);
|
||||||
$queueIntegration = $this->queueBridge->listMeta($organization, CareQueueContexts::CONSULTATION, $branchId);
|
$queueIntegration = $this->queueBridge->listMeta($organization, CareQueueContexts::CONSULTATION, $branchId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -214,8 +214,17 @@ class SettingsController extends Controller
|
|||||||
->where('is_active', true)
|
->where('is_active', true)
|
||||||
->pluck('id');
|
->pluck('id');
|
||||||
foreach ($branches as $branchId) {
|
foreach ($branches as $branchId) {
|
||||||
|
try {
|
||||||
|
$bridge->syncMissingAppointmentTickets($organization, (int) $branchId, 50);
|
||||||
|
} catch (\Throwable) {
|
||||||
|
// Continue other branches.
|
||||||
|
}
|
||||||
foreach (array_keys(CareQueueContexts::departments()) as $context) {
|
foreach (array_keys(CareQueueContexts::departments()) as $context) {
|
||||||
if ($context === CareQueueContexts::RECEPTION) {
|
if (in_array($context, [
|
||||||
|
CareQueueContexts::RECEPTION,
|
||||||
|
CareQueueContexts::CONSULTATION,
|
||||||
|
CareQueueContexts::TRIAGE,
|
||||||
|
], true)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -66,15 +66,16 @@ class CareQueueBridge
|
|||||||
return $appointment;
|
return $appointment;
|
||||||
}
|
}
|
||||||
|
|
||||||
$point = $this->resolveConsultationPoint($organization, $appointment);
|
$point = $this->resolveAppointmentPoint($organization, $appointment);
|
||||||
|
|
||||||
if (CareQueueContexts::usesAssignedRouting($context) && ! $point) {
|
if (CareQueueContexts::usesAssignedRouting($context) && empty($point['counter_uuid'] ?? null)) {
|
||||||
$appointment->forceFill([
|
$appointment->forceFill([
|
||||||
'queue_routing_status' => CareQueueContexts::ROUTING_UNRESOLVED,
|
'queue_routing_status' => CareQueueContexts::ROUTING_UNRESOLVED,
|
||||||
])->save();
|
])->save();
|
||||||
Log::info('care.queue_issue_unresolved_assignment', [
|
Log::info('care.queue_issue_unresolved_assignment', [
|
||||||
'appointment_id' => $appointment->id,
|
'appointment_id' => $appointment->id,
|
||||||
'practitioner_id' => $appointment->practitioner_id,
|
'practitioner_id' => $appointment->practitioner_id,
|
||||||
|
'context' => $context,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return $appointment->fresh();
|
return $appointment->fresh();
|
||||||
@@ -280,6 +281,44 @@ class CareQueueBridge
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Issue Queue tickets for every waiting appointment at a branch (consultation + specialties).
|
||||||
|
*/
|
||||||
|
public function syncMissingAppointmentTickets(Organization $organization, int $branchId, int $limit = 100): int
|
||||||
|
{
|
||||||
|
if (! $this->isEnabled($organization) || $branchId < 1 || $limit < 1) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
$owner = (string) $organization->owner_ref;
|
||||||
|
$appointments = Appointment::owned($owner)
|
||||||
|
->where('organization_id', $organization->id)
|
||||||
|
->where('branch_id', $branchId)
|
||||||
|
->whereIn('status', [Appointment::STATUS_WAITING, Appointment::STATUS_CHECKED_IN])
|
||||||
|
->where(function ($q) {
|
||||||
|
$q->whereNull('queue_ticket_uuid')->orWhere('queue_ticket_uuid', '');
|
||||||
|
})
|
||||||
|
->with(['patient', 'organization', 'practitioner', 'visit'])
|
||||||
|
->orderBy('queue_position')
|
||||||
|
->orderBy('waiting_at')
|
||||||
|
->orderBy('checked_in_at')
|
||||||
|
->orderBy('id')
|
||||||
|
->limit($limit)
|
||||||
|
->get();
|
||||||
|
|
||||||
|
$issued = 0;
|
||||||
|
foreach ($appointments as $appointment) {
|
||||||
|
$updated = $this->issueForAppointment($organization, $appointment->fresh([
|
||||||
|
'patient', 'organization', 'practitioner', 'visit',
|
||||||
|
]));
|
||||||
|
if ($updated && filled($updated->queue_ticket_uuid)) {
|
||||||
|
$issued++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $issued;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return array{ticket: ?array<string, mixed>, entity: ?Model, resources: ?array<string, mixed>}
|
* @return array{ticket: ?array<string, mixed>, entity: ?Model, resources: ?array<string, mixed>}
|
||||||
*/
|
*/
|
||||||
@@ -405,43 +444,41 @@ class CareQueueBridge
|
|||||||
/**
|
/**
|
||||||
* @return array<string, mixed>|null
|
* @return array<string, mixed>|null
|
||||||
*/
|
*/
|
||||||
protected function resolveConsultationPoint(Organization $organization, Appointment $appointment): ?array
|
protected function resolveAppointmentPoint(Organization $organization, Appointment $appointment): ?array
|
||||||
{
|
{
|
||||||
$branchId = (int) $appointment->branch_id;
|
$branchId = (int) $appointment->branch_id;
|
||||||
|
$context = $this->contextForAppointment($organization, $appointment);
|
||||||
|
|
||||||
// Assigned-only consultation requires a doctor — leave Unassigned until one is chosen.
|
$this->provisioner->ensure($organization, $context, $branchId);
|
||||||
if (! $appointment->practitioner_id) {
|
$organization = $organization->fresh() ?? $organization;
|
||||||
return null;
|
|
||||||
|
if ($appointment->practitioner_id) {
|
||||||
|
$point = $this->provisioner->pointForPractitioner(
|
||||||
|
$organization,
|
||||||
|
$branchId,
|
||||||
|
(int) $appointment->practitioner_id,
|
||||||
|
$context,
|
||||||
|
);
|
||||||
|
if ($point && ! empty($point['counter_uuid'])) {
|
||||||
|
return $point;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Refresh points for this context only, then retry the assigned desk.
|
||||||
|
$this->provisioner->ensure($organization, $context, $branchId);
|
||||||
|
$organization = $organization->fresh() ?? $organization;
|
||||||
|
$point = $this->provisioner->pointForPractitioner(
|
||||||
|
$organization,
|
||||||
|
$branchId,
|
||||||
|
(int) $appointment->practitioner_id,
|
||||||
|
$context,
|
||||||
|
);
|
||||||
|
if ($point && ! empty($point['counter_uuid'])) {
|
||||||
|
return $point;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$point = $this->provisioner->pointForPractitioner(
|
// Prefer a desk ticket over leaving waiters without a Queue number.
|
||||||
$organization,
|
return $this->provisioner->defaultPoint($organization, $context, $branchId);
|
||||||
$branchId,
|
|
||||||
(int) $appointment->practitioner_id,
|
|
||||||
);
|
|
||||||
if ($point) {
|
|
||||||
return $point;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Practitioner assigned but point not provisioned yet — refresh this branch only.
|
|
||||||
$this->provisioner->ensure($organization, CareQueueContexts::CONSULTATION, $branchId);
|
|
||||||
|
|
||||||
$point = $this->provisioner->pointForPractitioner(
|
|
||||||
$organization->fresh(),
|
|
||||||
$branchId,
|
|
||||||
(int) $appointment->practitioner_id,
|
|
||||||
);
|
|
||||||
if ($point) {
|
|
||||||
return $point;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Prefer a desk ticket over leaving waiters Unassigned while counters catch up
|
|
||||||
// after demo seed / deferred settings provision.
|
|
||||||
return $this->provisioner->defaultPoint(
|
|
||||||
$organization->fresh(),
|
|
||||||
CareQueueContexts::CONSULTATION,
|
|
||||||
$branchId,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -458,7 +495,7 @@ class CareQueueBridge
|
|||||||
): ?array {
|
): ?array {
|
||||||
if ($context === CareQueueContexts::CONSULTATION || CareQueueContexts::isSpecialty($context)) {
|
if ($context === CareQueueContexts::CONSULTATION || CareQueueContexts::isSpecialty($context)) {
|
||||||
if ($practitionerId) {
|
if ($practitionerId) {
|
||||||
return $this->provisioner->pointForPractitioner($organization, $branchId, $practitionerId)
|
return $this->provisioner->pointForPractitioner($organization, $branchId, $practitionerId, $context)
|
||||||
?? collect($resources['points'] ?? [])->first(
|
?? collect($resources['points'] ?? [])->first(
|
||||||
fn ($p) => ($p['kind'] ?? '') === 'practitioner' && (int) ($p['ref_id'] ?? 0) === $practitionerId
|
fn ($p) => ($p['kind'] ?? '') === 'practitioner' && (int) ($p['ref_id'] ?? 0) === $practitionerId
|
||||||
);
|
);
|
||||||
@@ -473,7 +510,7 @@ class CareQueueBridge
|
|||||||
})
|
})
|
||||||
->first();
|
->first();
|
||||||
if ($prac) {
|
if ($prac) {
|
||||||
return $this->provisioner->pointForPractitioner($organization, $branchId, $prac->id);
|
return $this->provisioner->pointForPractitioner($organization, $branchId, $prac->id, $context);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -198,9 +198,21 @@ class CareQueueProvisioner
|
|||||||
$owner = (string) $organization->owner_ref;
|
$owner = (string) $organization->owner_ref;
|
||||||
$priorByKey = collect($stub['points'] ?? [])->keyBy(fn ($p) => (string) ($p['external_key'] ?? ''));
|
$priorByKey = collect($stub['points'] ?? [])->keyBy(fn ($p) => (string) ($p['external_key'] ?? ''));
|
||||||
|
|
||||||
$points = match ($context) {
|
$points = match (true) {
|
||||||
CareQueueContexts::CONSULTATION => $this->practitionerPoints($organization, $branch, $priorByKey),
|
$context === CareQueueContexts::CONSULTATION => $this->practitionerPoints(
|
||||||
CareQueueContexts::TRIAGE => $this->memberRolePoints(
|
$organization,
|
||||||
|
$branch,
|
||||||
|
CareQueueContexts::CONSULTATION,
|
||||||
|
$priorByKey,
|
||||||
|
),
|
||||||
|
CareQueueContexts::isSpecialty($context) => $this->practitionerPoints(
|
||||||
|
$organization,
|
||||||
|
$branch,
|
||||||
|
$context,
|
||||||
|
$priorByKey,
|
||||||
|
data_get($organization->settings, "specialty_module_provisioning.{$context}.department_ids", []),
|
||||||
|
),
|
||||||
|
$context === CareQueueContexts::TRIAGE => $this->memberRolePoints(
|
||||||
$organization,
|
$organization,
|
||||||
$branch,
|
$branch,
|
||||||
['nurse'],
|
['nurse'],
|
||||||
@@ -208,7 +220,7 @@ class CareQueueProvisioner
|
|||||||
'Nurse station',
|
'Nurse station',
|
||||||
$priorByKey,
|
$priorByKey,
|
||||||
),
|
),
|
||||||
CareQueueContexts::PHARMACY => $this->memberRolePoints(
|
$context === CareQueueContexts::PHARMACY => $this->memberRolePoints(
|
||||||
$organization,
|
$organization,
|
||||||
$branch,
|
$branch,
|
||||||
['pharmacist'],
|
['pharmacist'],
|
||||||
@@ -216,7 +228,7 @@ class CareQueueProvisioner
|
|||||||
'Pharmacy counter',
|
'Pharmacy counter',
|
||||||
$priorByKey,
|
$priorByKey,
|
||||||
),
|
),
|
||||||
CareQueueContexts::LABORATORY, CareQueueContexts::IMAGING => $this->memberRolePoints(
|
in_array($context, [CareQueueContexts::LABORATORY, CareQueueContexts::IMAGING], true) => $this->memberRolePoints(
|
||||||
$organization,
|
$organization,
|
||||||
$branch,
|
$branch,
|
||||||
['lab_technician'],
|
['lab_technician'],
|
||||||
@@ -224,7 +236,7 @@ class CareQueueProvisioner
|
|||||||
$context === CareQueueContexts::IMAGING ? 'Imaging room' : 'Lab desk',
|
$context === CareQueueContexts::IMAGING ? 'Imaging room' : 'Lab desk',
|
||||||
$priorByKey,
|
$priorByKey,
|
||||||
),
|
),
|
||||||
CareQueueContexts::BILLING => $this->memberRolePoints(
|
$context === CareQueueContexts::BILLING => $this->memberRolePoints(
|
||||||
$organization,
|
$organization,
|
||||||
$branch,
|
$branch,
|
||||||
['cashier'],
|
['cashier'],
|
||||||
@@ -232,7 +244,7 @@ class CareQueueProvisioner
|
|||||||
'Cashier desk',
|
'Cashier desk',
|
||||||
$priorByKey,
|
$priorByKey,
|
||||||
),
|
),
|
||||||
CareQueueContexts::RECEPTION => $this->memberRolePoints(
|
$context === CareQueueContexts::RECEPTION => $this->memberRolePoints(
|
||||||
$organization,
|
$organization,
|
||||||
$branch,
|
$branch,
|
||||||
['receptionist'],
|
['receptionist'],
|
||||||
@@ -264,23 +276,37 @@ class CareQueueProvisioner
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* @param list<int|string>|null $departmentIds
|
||||||
* @param \Illuminate\Support\Collection<string, array<string, mixed>> $priorByKey
|
* @param \Illuminate\Support\Collection<string, array<string, mixed>> $priorByKey
|
||||||
* @return list<array<string, mixed>>
|
* @return list<array<string, mixed>>
|
||||||
*/
|
*/
|
||||||
protected function practitionerPoints(Organization $organization, Branch $branch, $priorByKey): array
|
protected function practitionerPoints(
|
||||||
{
|
Organization $organization,
|
||||||
|
Branch $branch,
|
||||||
|
string $context,
|
||||||
|
$priorByKey,
|
||||||
|
?array $departmentIds = null,
|
||||||
|
): array {
|
||||||
$practitioners = Practitioner::owned($organization->owner_ref)
|
$practitioners = Practitioner::owned($organization->owner_ref)
|
||||||
->where('organization_id', $organization->id)
|
->where('organization_id', $organization->id)
|
||||||
->where('branch_id', $branch->id)
|
->where('branch_id', $branch->id)
|
||||||
->where('is_active', true)
|
->where('is_active', true)
|
||||||
|
->when(
|
||||||
|
is_array($departmentIds) && $departmentIds !== [],
|
||||||
|
fn ($q) => $q->whereIn('department_id', array_map('intval', $departmentIds)),
|
||||||
|
)
|
||||||
->orderBy('name')
|
->orderBy('name')
|
||||||
->get();
|
->get();
|
||||||
|
|
||||||
$points = [];
|
$points = [];
|
||||||
$roomIndex = 1;
|
$roomIndex = 1;
|
||||||
|
$roomPrefix = CareQueueContexts::isSpecialty($context)
|
||||||
|
? (string) (config("care.specialty_modules.{$context}.label") ?? 'Room')
|
||||||
|
: 'Consultation Room';
|
||||||
|
|
||||||
foreach ($practitioners as $practitioner) {
|
foreach ($practitioners as $practitioner) {
|
||||||
$key = CareQueueContexts::pointExternalKey(
|
$key = CareQueueContexts::pointExternalKey(
|
||||||
CareQueueContexts::CONSULTATION,
|
$context,
|
||||||
$branch->id,
|
$branch->id,
|
||||||
'practitioner',
|
'practitioner',
|
||||||
$practitioner->id,
|
$practitioner->id,
|
||||||
@@ -288,7 +314,7 @@ class CareQueueProvisioner
|
|||||||
$prior = $priorByKey->get($key, []);
|
$prior = $priorByKey->get($key, []);
|
||||||
$room = trim((string) ($practitioner->room ?? ''));
|
$room = trim((string) ($practitioner->room ?? ''));
|
||||||
if ($room === '') {
|
if ($room === '') {
|
||||||
$room = 'Consultation Room '.$roomIndex;
|
$room = $roomPrefix.' '.$roomIndex;
|
||||||
}
|
}
|
||||||
$roomIndex++;
|
$roomIndex++;
|
||||||
|
|
||||||
@@ -407,13 +433,17 @@ class CareQueueProvisioner
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Resolve a service point for a practitioner (consultation) or member staff_ref.
|
* Resolve a service point for a practitioner on a queue context.
|
||||||
*
|
*
|
||||||
* @return array<string, mixed>|null
|
* @return array<string, mixed>|null
|
||||||
*/
|
*/
|
||||||
public function pointForPractitioner(Organization $organization, int $branchId, int $practitionerId): ?array
|
public function pointForPractitioner(
|
||||||
{
|
Organization $organization,
|
||||||
$resources = $this->resourcesFor($organization, CareQueueContexts::CONSULTATION, $branchId);
|
int $branchId,
|
||||||
|
int $practitionerId,
|
||||||
|
string $context = CareQueueContexts::CONSULTATION,
|
||||||
|
): ?array {
|
||||||
|
$resources = $this->resourcesFor($organization, $context, $branchId);
|
||||||
if (! $resources) {
|
if (! $resources) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -495,7 +525,11 @@ class CareQueueProvisioner
|
|||||||
if ($existing && ! empty($existing['queue_uuid']) && (
|
if ($existing && ! empty($existing['queue_uuid']) && (
|
||||||
! empty($existing['points']) || ! empty($existing['counter_uuid'])
|
! empty($existing['points']) || ! empty($existing['counter_uuid'])
|
||||||
) && $modeMatches) {
|
) && $modeMatches) {
|
||||||
return $existing;
|
// Specialty module stubs often only store a legacy counter — still provision
|
||||||
|
// practitioner points so check-in / sync can assign real desks.
|
||||||
|
if (! CareQueueContexts::isSpecialty($context) || ! empty($existing['points'])) {
|
||||||
|
return $existing;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (! $this->shouldProvision($organization)) {
|
if (! $this->shouldProvision($organization)) {
|
||||||
@@ -503,7 +537,8 @@ class CareQueueProvisioner
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (CareQueueContexts::isSpecialty($context)) {
|
if (CareQueueContexts::isSpecialty($context)) {
|
||||||
return $existing;
|
return $this->provisionSpecialtyContextBranch($organization->fresh(), $context, $branchId)
|
||||||
|
?? $existing;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (! CareQueueContexts::isDepartment($context)) {
|
if (! CareQueueContexts::isDepartment($context)) {
|
||||||
@@ -515,6 +550,69 @@ class CareQueueProvisioner
|
|||||||
return $this->resourcesFor($organization->fresh(), $context, $branchId);
|
return $this->resourcesFor($organization->fresh(), $context, $branchId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provision a single specialty queue for one branch (idempotent).
|
||||||
|
*
|
||||||
|
* @return array<string, mixed>|null
|
||||||
|
*/
|
||||||
|
public function provisionSpecialtyContextBranch(Organization $organization, string $context, int $branchId): ?array
|
||||||
|
{
|
||||||
|
if (! CareQueueContexts::isSpecialty($context) || ! $this->shouldProvision($organization)) {
|
||||||
|
return $this->resourcesFor($organization, $context, $branchId);
|
||||||
|
}
|
||||||
|
|
||||||
|
$definition = config("care.specialty_modules.{$context}");
|
||||||
|
if (! is_array($definition)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$branch = Branch::owned((string) $organization->owner_ref)
|
||||||
|
->where('organization_id', $organization->id)
|
||||||
|
->whereKey($branchId)
|
||||||
|
->first();
|
||||||
|
if (! $branch) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$meta = [
|
||||||
|
'name' => (string) ($definition['queue_name'] ?? $definition['label'] ?? $context),
|
||||||
|
'prefix' => (string) ($definition['queue_prefix'] ?? strtoupper(substr($context, 0, 3))),
|
||||||
|
'routing_mode' => 'assigned_only',
|
||||||
|
'type' => 'consultation',
|
||||||
|
];
|
||||||
|
|
||||||
|
$provisioning = (array) data_get($organization->settings, 'specialty_module_provisioning', []);
|
||||||
|
$record = is_array($provisioning[$context] ?? null) ? $provisioning[$context] : [];
|
||||||
|
$queues = is_array($record['queues'] ?? null) ? $record['queues'] : [];
|
||||||
|
$byBranch = collect($queues)->keyBy(fn ($q) => (string) ($q['branch_id'] ?? ''));
|
||||||
|
$prior = $byBranch->get((string) $branch->id, []);
|
||||||
|
if (! is_array($prior)) {
|
||||||
|
$prior = [];
|
||||||
|
}
|
||||||
|
$prior['module'] = $context;
|
||||||
|
|
||||||
|
$stub = $this->provisionBranchDepartment(
|
||||||
|
$organization,
|
||||||
|
(string) $organization->owner_ref,
|
||||||
|
$context,
|
||||||
|
$meta,
|
||||||
|
$branch,
|
||||||
|
$prior,
|
||||||
|
);
|
||||||
|
$byBranch->put((string) $branch->id, $stub);
|
||||||
|
|
||||||
|
$provisioning[$context] = array_merge($record, [
|
||||||
|
'active' => true,
|
||||||
|
'queues' => $byBranch->values()->all(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
$settings = $organization->settings ?? [];
|
||||||
|
$settings['specialty_module_provisioning'] = $provisioning;
|
||||||
|
$organization->update(['settings' => $settings]);
|
||||||
|
|
||||||
|
return $stub;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Provision a single department queue for one branch (idempotent).
|
* Provision a single department queue for one branch (idempotent).
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -186,17 +186,20 @@ class DemoTenantSeeder
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Scoped per branch/context (not full-org) so demos finish quickly while
|
// Scoped per branch (not full-org) so demos finish quickly while
|
||||||
// still leaving waiters with real tickets instead of "Unassigned".
|
// still leaving waiters with real tickets instead of local positions only.
|
||||||
$contexts = [
|
|
||||||
CareQueueContexts::CONSULTATION,
|
|
||||||
CareQueueContexts::PHARMACY,
|
|
||||||
CareQueueContexts::LABORATORY,
|
|
||||||
CareQueueContexts::BILLING,
|
|
||||||
];
|
|
||||||
|
|
||||||
foreach ($branches as $branch) {
|
foreach ($branches as $branch) {
|
||||||
foreach ($contexts as $context) {
|
try {
|
||||||
|
$bridge->syncMissingAppointmentTickets($organization, (int) $branch->id, 100);
|
||||||
|
} catch (\Throwable) {
|
||||||
|
// Continue — page load will retry.
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ([
|
||||||
|
CareQueueContexts::PHARMACY,
|
||||||
|
CareQueueContexts::LABORATORY,
|
||||||
|
CareQueueContexts::BILLING,
|
||||||
|
] as $context) {
|
||||||
try {
|
try {
|
||||||
$bridge->syncMissingTickets($organization, $context, (int) $branch->id, 50);
|
$bridge->syncMissingTickets($organization, $context, (int) $branch->id, 50);
|
||||||
} catch (\Throwable) {
|
} catch (\Throwable) {
|
||||||
|
|||||||
@@ -368,5 +368,104 @@ class CareQueueBridgeTest extends TestCase
|
|||||||
->assertSee('Call again')
|
->assertSee('Call again')
|
||||||
->assertSee('C055');
|
->assertSee('C055');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function test_patient_queue_sync_issues_specialty_tickets(): void
|
||||||
|
{
|
||||||
|
$department = \App\Models\Department::create([
|
||||||
|
'owner_ref' => $this->owner->public_id,
|
||||||
|
'branch_id' => $this->branch->id,
|
||||||
|
'name' => 'Dentistry',
|
||||||
|
'type' => 'dental',
|
||||||
|
'is_active' => true,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$practitioner = \App\Models\Practitioner::create([
|
||||||
|
'owner_ref' => $this->owner->public_id,
|
||||||
|
'organization_id' => $this->organization->id,
|
||||||
|
'branch_id' => $this->branch->id,
|
||||||
|
'department_id' => $department->id,
|
||||||
|
'name' => 'Dentistry Clinic',
|
||||||
|
'room' => 'Dental Bay 1',
|
||||||
|
'is_active' => true,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$settings = $this->organization->settings;
|
||||||
|
$settings['specialty_modules'] = ['dentistry' => true];
|
||||||
|
$settings['specialty_module_provisioning'] = [
|
||||||
|
'dentistry' => [
|
||||||
|
'active' => true,
|
||||||
|
'department_ids' => [$department->id],
|
||||||
|
'queues' => [[
|
||||||
|
'module' => 'dentistry',
|
||||||
|
'branch_id' => $this->branch->id,
|
||||||
|
'branch_name' => 'Main',
|
||||||
|
'name' => 'Dentistry',
|
||||||
|
'prefix' => 'DEN',
|
||||||
|
'active' => true,
|
||||||
|
'synced' => true,
|
||||||
|
'routing_mode' => 'assigned_only',
|
||||||
|
'queue_uuid' => 'dddddddd-dddd-dddd-dddd-dddddddddddd',
|
||||||
|
'counter_uuid' => 'eeeeeeee-eeee-eeee-eeee-eeeeeeeeeeee',
|
||||||
|
'queue_external_key' => CareQueueContexts::queueExternalKey('dentistry', $this->branch->id),
|
||||||
|
'counter_external_key' => CareQueueContexts::counterExternalKey('dentistry', $this->branch->id),
|
||||||
|
'points' => [[
|
||||||
|
'kind' => 'practitioner',
|
||||||
|
'ref_id' => $practitioner->id,
|
||||||
|
'destination' => 'Dental Bay 1',
|
||||||
|
'staff_display_name' => 'Dentistry Clinic',
|
||||||
|
'counter_uuid' => 'eeeeeeee-eeee-eeee-eeee-eeeeeeeeeeee',
|
||||||
|
'synced' => true,
|
||||||
|
]],
|
||||||
|
]],
|
||||||
|
],
|
||||||
|
];
|
||||||
|
$this->organization->update(['settings' => $settings]);
|
||||||
|
|
||||||
|
Appointment::create([
|
||||||
|
'owner_ref' => $this->owner->public_id,
|
||||||
|
'organization_id' => $this->organization->id,
|
||||||
|
'branch_id' => $this->branch->id,
|
||||||
|
'patient_id' => $this->patient->id,
|
||||||
|
'practitioner_id' => $practitioner->id,
|
||||||
|
'department_id' => $department->id,
|
||||||
|
'type' => Appointment::TYPE_WALK_IN,
|
||||||
|
'status' => Appointment::STATUS_WAITING,
|
||||||
|
'scheduled_at' => now(),
|
||||||
|
'waiting_at' => now(),
|
||||||
|
'queue_position' => 1,
|
||||||
|
'reason' => 'Toothache',
|
||||||
|
]);
|
||||||
|
|
||||||
|
Http::fake(function (\Illuminate\Http\Client\Request $request) {
|
||||||
|
if (str_contains($request->url(), '/tickets') && $request->method() === 'POST') {
|
||||||
|
$this->assertSame('dddddddd-dddd-dddd-dddd-dddddddddddd', $request['service_queue_id'] ?? null);
|
||||||
|
$this->assertSame('eeeeeeee-eeee-eeee-eeee-eeeeeeeeeeee', $request['assigned_counter_id'] ?? null);
|
||||||
|
|
||||||
|
return Http::response([
|
||||||
|
'data' => [
|
||||||
|
'uuid' => 'ffffffff-ffff-ffff-ffff-ffffffffffff',
|
||||||
|
'ticket_number' => 'DEN001',
|
||||||
|
'status' => 'waiting',
|
||||||
|
'assigned_counter' => ['uuid' => 'eeeeeeee-eeee-eeee-eeee-eeeeeeeeeeee'],
|
||||||
|
'destination' => 'Dental Bay 1',
|
||||||
|
'staff_display_name' => 'Dentistry Clinic',
|
||||||
|
],
|
||||||
|
], 201);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Http::response(['data' => []], 200);
|
||||||
|
});
|
||||||
|
|
||||||
|
$this->actingAs($this->owner)
|
||||||
|
->get(route('care.queue.index', ['branch_id' => $this->branch->id]))
|
||||||
|
->assertOk()
|
||||||
|
->assertSee('DEN001')
|
||||||
|
->assertSee('Toothache');
|
||||||
|
|
||||||
|
$this->assertDatabaseHas('care_appointments', [
|
||||||
|
'patient_id' => $this->patient->id,
|
||||||
|
'queue_ticket_number' => 'DEN001',
|
||||||
|
]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user