Make cashier billing invoices actionable for walk-up payment.
Deploy Ladill Care / deploy (push) Successful in 1m1s

Clarify booth Call next vs unpaid invoice list, and expose Pay/View row actions so cashiers can open bills when the booth queue is empty.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
isaacclad
2026-07-18 23:16:49 +00:00
co-authored by Cursor
parent ff316f15c6
commit b84c7e31ba
4 changed files with 75 additions and 18 deletions
+4 -1
View File
@@ -89,7 +89,10 @@ class BillController extends Controller
$result = $this->queueBridge->callNext($organization, CareQueueContexts::BILLING, $branchId, $member); $result = $this->queueBridge->callNext($organization, CareQueueContexts::BILLING, $branchId, $member);
$ticket = $result['ticket'] ?? null; $ticket = $result['ticket'] ?? null;
if (! $ticket) { if (! $ticket) {
return back()->with('info', 'No bills waiting at your billing service point.'); return back()->with(
'info',
'No patients waiting at the billing booth. Open an unpaid invoice below to record a walk-up payment.',
);
} }
return back()->with('success', 'Called '.$ticket['ticket_number'].'.'); return back()->with('success', 'Called '.$ticket['ticket_number'].'.');
+34 -16
View File
@@ -29,17 +29,20 @@
<button type="submit" class="btn-primary">Filter</button> <button type="submit" class="btn-primary">Filter</button>
</form> </form>
<div class="mt-4"> @if (! empty($canManageQueue) && ! empty($queueIntegration['enabled']))
@include('care.partials.queue-ops', [ <div class="mt-4">
'queueIntegration' => $queueIntegration ?? null, @include('care.partials.queue-ops', [
'queueCallNextRoute' => 'care.bills.call-next', 'queueIntegration' => $queueIntegration,
'queueCallNextParams' => [], 'queueCallNextRoute' => 'care.bills.call-next',
'branchId' => $branchId ?? null, 'queueCallNextParams' => [],
'variant' => 'bar', 'queueCallNextDescription' => 'Call the next patient waiting at the billing booth. Unpaid invoices below stay open for walk-up payment.',
]) 'branchId' => $branchId ?? null,
</div> 'variant' => 'bar',
])
</div>
@endif
<div class="mt-4 overflow-hidden rounded-2xl border border-slate-200 bg-white"> <div class="mt-4 overflow-x-auto rounded-2xl border border-slate-200 bg-white">
<table class="min-w-full text-sm"> <table class="min-w-full text-sm">
<thead class="bg-slate-50 text-left text-xs uppercase text-slate-500"> <thead class="bg-slate-50 text-left text-xs uppercase text-slate-500">
<tr> <tr>
@@ -49,12 +52,25 @@
<th class="px-4 py-3">Total</th> <th class="px-4 py-3">Total</th>
<th class="px-4 py-3">Balance</th> <th class="px-4 py-3">Balance</th>
<th class="px-4 py-3">Status</th> <th class="px-4 py-3">Status</th>
<th></th> <th class="px-4 py-3 text-right">Actions</th>
</tr> </tr>
</thead> </thead>
<tbody class="divide-y divide-slate-50"> <tbody class="divide-y divide-slate-50">
@forelse ($bills as $bill) @forelse ($bills as $bill)
<tr> @php
$billUrl = route('care.bills.show', $bill);
$canServe = ! empty($queueIntegration['enabled'])
&& $bill->queue_ticket_uuid
&& ! in_array($bill->queue_ticket_status ?? '', ['serving', 'completed', 'cancelled'], true)
&& $bill->balance_minor > 0;
$actionLabel = $bill->balance_minor > 0 && $bill->status !== \App\Models\Bill::STATUS_VOID
? 'Pay'
: 'View';
@endphp
<tr
class="cursor-pointer hover:bg-slate-50"
onclick="window.location='{{ $billUrl }}'"
>
<td class="px-4 py-3"> <td class="px-4 py-3">
@if (! empty($queueIntegration['enabled']) && $bill->queue_ticket_number) @if (! empty($queueIntegration['enabled']) && $bill->queue_ticket_number)
@include('care.partials.queue-ticket', [ @include('care.partials.queue-ticket', [
@@ -65,20 +81,22 @@
<span class="text-slate-400"></span> <span class="text-slate-400"></span>
@endif @endif
</td> </td>
<td class="px-4 py-3 font-mono text-xs">{{ $bill->invoice_number }}</td> <td class="px-4 py-3 font-mono text-xs">
<a href="{{ $billUrl }}" class="font-medium text-sky-600 hover:text-sky-700" onclick="event.stopPropagation()">{{ $bill->invoice_number }}</a>
</td>
<td class="px-4 py-3">{{ $bill->patient->fullName() }}</td> <td class="px-4 py-3">{{ $bill->patient->fullName() }}</td>
<td class="px-4 py-3">{{ $money($bill->total_minor) }}</td> <td class="px-4 py-3">{{ $money($bill->total_minor) }}</td>
<td class="px-4 py-3 {{ $bill->balance_minor > 0 ? 'text-amber-700' : '' }}">{{ $money($bill->balance_minor) }}</td> <td class="px-4 py-3 {{ $bill->balance_minor > 0 ? 'text-amber-700' : '' }}">{{ $money($bill->balance_minor) }}</td>
<td class="px-4 py-3">{{ $statuses[$bill->status] ?? $bill->status }}</td> <td class="px-4 py-3">{{ $statuses[$bill->status] ?? $bill->status }}</td>
<td class="px-4 py-3 text-right"> <td class="px-4 py-3 text-right" onclick="event.stopPropagation()">
<div class="flex items-center justify-end gap-2"> <div class="flex items-center justify-end gap-2">
@if (! empty($queueIntegration['enabled']) && $bill->queue_ticket_uuid && ! in_array($bill->queue_ticket_status ?? '', ['serving', 'completed', 'cancelled'], true) && $bill->balance_minor > 0) @if ($canServe)
<form method="POST" action="{{ route('care.bills.serve', $bill) }}"> <form method="POST" action="{{ route('care.bills.serve', $bill) }}">
@csrf @csrf
<button type="submit" class="rounded-lg bg-sky-600 px-2.5 py-1 text-xs font-medium text-white hover:bg-sky-700">Serve</button> <button type="submit" class="rounded-lg bg-sky-600 px-2.5 py-1 text-xs font-medium text-white hover:bg-sky-700">Serve</button>
</form> </form>
@endif @endif
<a href="{{ route('care.bills.show', $bill) }}" class="text-sky-600">View</a> <a href="{{ $billUrl }}" class="rounded-lg border border-slate-200 bg-white px-2.5 py-1 text-xs font-medium text-slate-700 hover:border-slate-300 hover:bg-slate-50">{{ $actionLabel }}</a>
</div> </div>
</td> </td>
</tr> </tr>
@@ -3,6 +3,7 @@
$qi = $queueIntegration ?? null; $qi = $queueIntegration ?? null;
$callNextRoute = $queueCallNextRoute ?? null; $callNextRoute = $queueCallNextRoute ?? null;
$callNextParams = $queueCallNextParams ?? []; $callNextParams = $queueCallNextParams ?? [];
$callNextDescription = $queueCallNextDescription ?? 'Bring the next waiting patient to your desk';
$currentAppointmentId = $currentAppointmentId ?? null; $currentAppointmentId = $currentAppointmentId ?? null;
$variant = $variant ?? 'bar'; // bar | button $variant = $variant ?? 'bar'; // bar | button
@endphp @endphp
@@ -24,7 +25,7 @@
<button type="submit" class="group flex w-full items-center justify-between gap-3 rounded-xl bg-indigo-600 px-4 py-3 text-left shadow-sm transition hover:bg-indigo-700"> <button type="submit" class="group flex w-full items-center justify-between gap-3 rounded-xl bg-indigo-600 px-4 py-3 text-left shadow-sm transition hover:bg-indigo-700">
<span class="min-w-0"> <span class="min-w-0">
<span class="block text-sm font-semibold text-white">Call next</span> <span class="block text-sm font-semibold text-white">Call next</span>
<span class="mt-0.5 block truncate text-xs text-indigo-100">Bring the next waiting patient to your desk</span> <span class="mt-0.5 block text-xs text-indigo-100">{{ $callNextDescription }}</span>
</span> </span>
<span class="flex h-9 w-9 shrink-0 items-center justify-center rounded-lg bg-white/15 text-white transition group-hover:bg-white/25"> <span class="flex h-9 w-9 shrink-0 items-center justify-center rounded-lg bg-white/15 text-white transition group-hover:bg-white/25">
<svg class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" aria-hidden="true"> <svg class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" aria-hidden="true">
+35
View File
@@ -158,4 +158,39 @@ class CareBillTest extends TestCase
->assertOk() ->assertOk()
->assertSee('INV-'); ->assertSee('INV-');
} }
public function test_cashier_bills_index_links_each_invoice_for_payment(): void
{
$this->actingAs($this->user)
->post(route('care.bills.generate', $this->visit));
$bill = Bill::firstOrFail();
$this->actingAs($this->user)
->get(route('care.bills.index'))
->assertOk()
->assertSee($bill->invoice_number)
->assertSee(route('care.bills.show', $bill), false)
->assertSee('Pay')
->assertSee('Actions');
$this->actingAs($this->user)
->get(route('care.bills.show', $bill))
->assertOk()
->assertSee('Record payment');
}
public function test_call_next_empty_booth_explains_walk_up_invoices(): void
{
\Illuminate\Support\Facades\Http::fake();
$settings = $this->organization->settings ?? [];
$settings['queue_integration_enabled'] = true;
$this->organization->update(['settings' => $settings]);
$this->actingAs($this->user)
->post(route('care.bills.call-next'), ['branch_id' => Branch::firstOrFail()->id])
->assertRedirect()
->assertSessionHas('info', 'No patients waiting at the billing booth. Open an unpaid invoice below to record a walk-up payment.');
}
} }