Fix patient queue showing position 1 for every waiter.
Deploy Ladill Care / deploy (push) Successful in 1m44s
Deploy Ladill Care / deploy (push) Successful in 1m44s
Specialty demo seed reused slot+1 per module; repair duplicates on queue load and renumber after demo seed so waiting lists show 1..n. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -73,6 +73,8 @@ class AppointmentService
|
||||
*/
|
||||
public function queue(string $ownerRef, int $branchId, ?int $practitionerId = null): Collection
|
||||
{
|
||||
$this->repairDuplicateQueuePositions($ownerRef, $branchId);
|
||||
|
||||
$query = Appointment::owned($ownerRef)
|
||||
->where('branch_id', $branchId)
|
||||
->whereIn('status', [Appointment::STATUS_WAITING, Appointment::STATUS_CHECKED_IN])
|
||||
@@ -381,6 +383,37 @@ class AppointmentService
|
||||
return ($max ?? 0) + 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Demo specialty seed used to set every module's waiter to position 1.
|
||||
* Repair duplicates in place so the patient queue shows 1..n.
|
||||
*/
|
||||
protected function repairDuplicateQueuePositions(string $ownerRef, int $branchId): void
|
||||
{
|
||||
$waiters = Appointment::owned($ownerRef)
|
||||
->where('branch_id', $branchId)
|
||||
->whereIn('status', [Appointment::STATUS_WAITING, Appointment::STATUS_CHECKED_IN])
|
||||
->orderByRaw('COALESCE(waiting_at, checked_in_at, scheduled_at) asc')
|
||||
->orderBy('id')
|
||||
->get(['id', 'queue_position']);
|
||||
|
||||
if ($waiters->count() < 2) {
|
||||
return;
|
||||
}
|
||||
|
||||
$positions = $waiters->pluck('queue_position');
|
||||
if ($positions->filter(fn ($p) => $p !== null)->unique()->count() === $waiters->count()) {
|
||||
return;
|
||||
}
|
||||
|
||||
foreach ($waiters as $index => $appointment) {
|
||||
$position = $index + 1;
|
||||
if ((int) $appointment->queue_position === $position) {
|
||||
continue;
|
||||
}
|
||||
Appointment::whereKey($appointment->id)->update(['queue_position' => $position]);
|
||||
}
|
||||
}
|
||||
|
||||
protected function assertTransition(Appointment $appointment, string ...$allowedFrom): void
|
||||
{
|
||||
if (! in_array($appointment->status, $allowedFrom, true)) {
|
||||
|
||||
@@ -125,7 +125,6 @@ class DemoTenantSeeder
|
||||
$ownerRef,
|
||||
$volumes,
|
||||
);
|
||||
$this->assignAllDemoWaiters($organization, $ownerRef, $branches, $practitioners);
|
||||
if (in_array($plan, ['pro', 'enterprise'], true)) {
|
||||
$appointmentCount += $this->seedSpecialtyDemoData(
|
||||
$organization->fresh(),
|
||||
@@ -135,6 +134,8 @@ class DemoTenantSeeder
|
||||
$ownerRef,
|
||||
);
|
||||
}
|
||||
$this->assignAllDemoWaiters($organization, $ownerRef, $branches, $practitioners);
|
||||
$this->renumberWaitingQueuePositions($organization, $ownerRef, $branches);
|
||||
|
||||
if ($volumes['paid_modules']) {
|
||||
$this->seedPaidModules(
|
||||
@@ -297,6 +298,39 @@ class DemoTenantSeeder
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Give each waiting/checked-in appointment a unique 1..n position per branch.
|
||||
*
|
||||
* @param list<Branch> $branches
|
||||
*/
|
||||
private function renumberWaitingQueuePositions(
|
||||
Organization $organization,
|
||||
string $ownerRef,
|
||||
array $branches,
|
||||
): void {
|
||||
foreach ($branches as $branch) {
|
||||
$waiters = Appointment::withTrashed()
|
||||
->owned($ownerRef)
|
||||
->where('organization_id', $organization->id)
|
||||
->where('branch_id', $branch->id)
|
||||
->whereIn('status', [Appointment::STATUS_WAITING, Appointment::STATUS_CHECKED_IN])
|
||||
->orderByRaw('COALESCE(waiting_at, checked_in_at, scheduled_at) asc')
|
||||
->orderBy('id')
|
||||
->get();
|
||||
|
||||
foreach ($waiters as $index => $appointment) {
|
||||
$position = $index + 1;
|
||||
if ((int) $appointment->queue_position === $position) {
|
||||
continue;
|
||||
}
|
||||
$appointment->forceFill([
|
||||
'queue_position' => $position,
|
||||
'deleted_at' => null,
|
||||
])->save();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure the demo tenant has an onboarded organization + owner member.
|
||||
* Safe to call on the SSO request path before redirecting.
|
||||
@@ -751,6 +785,8 @@ class DemoTenantSeeder
|
||||
|
||||
$count = 0;
|
||||
$catalog = config('care.specialty_modules', []);
|
||||
/** @var array<int, int> $nextPositionByBranch */
|
||||
$nextPositionByBranch = [];
|
||||
foreach ($catalog as $key => $definition) {
|
||||
$type = (string) ($definition['department_type'] ?? '');
|
||||
if ($type === '') {
|
||||
@@ -814,6 +850,13 @@ class DemoTenantSeeder
|
||||
);
|
||||
}
|
||||
|
||||
$inQueue = in_array($status, [Appointment::STATUS_WAITING, Appointment::STATUS_CHECKED_IN], true);
|
||||
$queuePosition = null;
|
||||
if ($inQueue) {
|
||||
$nextPositionByBranch[$branch->id] = ($nextPositionByBranch[$branch->id] ?? 0) + 1;
|
||||
$queuePosition = $nextPositionByBranch[$branch->id];
|
||||
}
|
||||
|
||||
Appointment::withTrashed()->updateOrCreate(
|
||||
['uuid' => $this->demoUuid("appt|{$ownerRef}|{$token}")],
|
||||
[
|
||||
@@ -828,11 +871,12 @@ class DemoTenantSeeder
|
||||
'status' => $status,
|
||||
'scheduled_at' => $scheduledAt,
|
||||
'checked_in_at' => $visit?->checked_in_at,
|
||||
'waiting_at' => $inQueue ? $scheduledAt->copy()->addMinutes(5) : null,
|
||||
'completed_at' => $status === Appointment::STATUS_COMPLETED
|
||||
? $scheduledAt->copy()->addHour()
|
||||
: null,
|
||||
'cancelled_at' => null,
|
||||
'queue_position' => $slot + 1,
|
||||
'queue_position' => $queuePosition,
|
||||
'reason' => $moduleReasons[$slot % count($moduleReasons)],
|
||||
'created_by' => $ownerRef,
|
||||
'deleted_at' => null,
|
||||
|
||||
@@ -209,6 +209,52 @@ class CareAppointmentTest extends TestCase
|
||||
->assertSee('Patient queue');
|
||||
}
|
||||
|
||||
public function test_queue_repairs_duplicate_positions(): void
|
||||
{
|
||||
$second = Patient::create([
|
||||
'uuid' => (string) \Illuminate\Support\Str::uuid(),
|
||||
'owner_ref' => $this->user->public_id,
|
||||
'organization_id' => $this->organization->id,
|
||||
'branch_id' => $this->branch->id,
|
||||
'patient_number' => 'LC-2026-00002',
|
||||
'first_name' => 'Kofi',
|
||||
'last_name' => 'Mensah',
|
||||
]);
|
||||
|
||||
foreach ([$this->patient, $second] as $index => $patient) {
|
||||
Appointment::create([
|
||||
'uuid' => (string) \Illuminate\Support\Str::uuid(),
|
||||
'owner_ref' => $this->user->public_id,
|
||||
'organization_id' => $this->organization->id,
|
||||
'branch_id' => $this->branch->id,
|
||||
'patient_id' => $patient->id,
|
||||
'practitioner_id' => $this->practitioner->id,
|
||||
'type' => Appointment::TYPE_WALK_IN,
|
||||
'status' => Appointment::STATUS_WAITING,
|
||||
'scheduled_at' => now()->subMinutes(10 - $index),
|
||||
'waiting_at' => now()->subMinutes(10 - $index),
|
||||
'queue_position' => 1,
|
||||
'reason' => $index === 0 ? 'Toothache' : 'Blurry vision',
|
||||
'created_by' => $this->user->public_id,
|
||||
]);
|
||||
}
|
||||
|
||||
$this->actingAs($this->user)
|
||||
->get(route('care.queue.index', ['branch_id' => $this->branch->id]))
|
||||
->assertOk()
|
||||
->assertSee('Toothache')
|
||||
->assertSee('Blurry vision');
|
||||
|
||||
$positions = Appointment::query()
|
||||
->where('branch_id', $this->branch->id)
|
||||
->where('status', Appointment::STATUS_WAITING)
|
||||
->orderBy('waiting_at')
|
||||
->pluck('queue_position')
|
||||
->all();
|
||||
|
||||
$this->assertSame([1, 2], array_map('intval', $positions));
|
||||
}
|
||||
|
||||
public function test_api_can_book_and_check_in(): void
|
||||
{
|
||||
Sanctum::actingAs($this->user);
|
||||
|
||||
Reference in New Issue
Block a user