diff --git a/app/Http/Controllers/Care/BillController.php b/app/Http/Controllers/Care/BillController.php index 460ef39..0defcc5 100644 --- a/app/Http/Controllers/Care/BillController.php +++ b/app/Http/Controllers/Care/BillController.php @@ -55,10 +55,17 @@ class BillController extends Controller ]); } + $filters = $request->only(['status', 'patient_id']); + // Cashiers land on collectible balances; walk-up unpaid list is the primary path + // (booth Call next is secondary when financial gates are off). + if (! $request->has('status') && $member?->role === 'cashier') { + $filters['status'] = 'outstanding'; + } + $bills = $this->bills->list( $this->ownerRef($request), $organization->id, - $request->only(['status', 'patient_id']), + $filters, $branchId > 0 ? $branchId : null, ); @@ -66,6 +73,7 @@ class BillController extends Controller 'organization' => $organization, 'bills' => $bills, 'statuses' => config('care.bill_statuses'), + 'statusFilter' => $filters['status'] ?? '', 'branches' => $branches, 'branchId' => $branchId, 'canSwitchBranch' => $this->branchContext->canSwitch($member), @@ -91,7 +99,7 @@ class BillController extends Controller if (! $ticket) { return back()->with( 'info', - 'No patients waiting at the billing booth. Open an unpaid invoice below to record a walk-up payment.', + 'No patients waiting at the billing booth. Open an unpaid invoice above to record a walk-up payment.', ); } diff --git a/app/Services/Care/BillService.php b/app/Services/Care/BillService.php index 81e325e..5ba6d01 100644 --- a/app/Services/Care/BillService.php +++ b/app/Services/Care/BillService.php @@ -46,7 +46,11 @@ class BillService $query->where('branch_id', $branchId); } - if ($status = $filters['status'] ?? null) { + $status = $filters['status'] ?? null; + if ($status === 'outstanding') { + $query->whereIn('status', [Bill::STATUS_OPEN, Bill::STATUS_PARTIAL]) + ->where('balance_minor', '>', 0); + } elseif ($status) { $query->where('status', $status); } diff --git a/app/Services/Care/DemoTenantSeeder.php b/app/Services/Care/DemoTenantSeeder.php index d0b7495..0d4b213 100644 --- a/app/Services/Care/DemoTenantSeeder.php +++ b/app/Services/Care/DemoTenantSeeder.php @@ -692,7 +692,10 @@ class DemoTenantSeeder $branch = $hq; if ($role !== 'hospital_admin' && $branches !== []) { - $branch = $branches[$index % count($branches)]; + // Cashiers always staff HQ so demo unpaid invoices on branch[0] are collectible. + $branch = $role === 'cashier' + ? $hq + : $branches[$index % count($branches)]; } $email = strtolower((string) $staff['email']); @@ -1750,10 +1753,27 @@ class DemoTenantSeeder ); } + $branchCount = max(1, count($branches)); for ($i = 0; $i < $volumes['bills']; $i++) { $patient = $patients[$i % count($patients)]; - $branch = $branches[$i % count($branches)]; + $branch = $branches[$i % $branchCount]; $fee = 5000 + (($i % 5) * 1000); + // Payment mix is per-branch (not i%3 with branchCount=3), otherwise HQ + // only ever gets paid invoices and the Ridge cashier has nothing to collect. + $slot = intdiv($i, $branchCount) % 3; + if ($slot === 0) { + $status = Bill::STATUS_PAID; + $amountPaid = $fee; + $balance = 0; + } elseif ($slot === 1) { + $status = Bill::STATUS_PARTIAL; + $amountPaid = (int) floor($fee / 2); + $balance = $fee - $amountPaid; + } else { + $status = Bill::STATUS_OPEN; + $amountPaid = 0; + $balance = $fee; + } $visit = Visit::withTrashed()->updateOrCreate( ['uuid' => $this->demoUuid("bill-visit|{$ownerRef}|{$i}")], @@ -1779,13 +1799,13 @@ class DemoTenantSeeder 'visit_id' => $visit->id, 'patient_id' => $patient->id, 'invoice_number' => sprintf('DEMO-INV-%s-%04d', Str::upper(Str::substr(md5($ownerRef), 0, 4)), $i + 1), - 'status' => $i % 3 === 0 ? Bill::STATUS_PAID : Bill::STATUS_OPEN, + 'status' => $status, 'subtotal_minor' => $fee, 'discount_minor' => 0, 'tax_minor' => 0, 'total_minor' => $fee, - 'amount_paid_minor' => $i % 3 === 0 ? $fee : 0, - 'balance_minor' => $i % 3 === 0 ? 0 : $fee, + 'amount_paid_minor' => $amountPaid, + 'balance_minor' => $balance, 'created_by' => $ownerRef, 'finalized_at' => now()->subDays($i % 12), 'deleted_at' => null, @@ -1806,13 +1826,14 @@ class DemoTenantSeeder ], ); - if ($bill->status === Bill::STATUS_PAID) { + $paymentUuid = $this->demoUuid("pay|{$ownerRef}|{$i}"); + if ($amountPaid > 0) { Payment::query()->updateOrCreate( - ['uuid' => $this->demoUuid("pay|{$ownerRef}|{$i}")], + ['uuid' => $paymentUuid], [ 'owner_ref' => $ownerRef, 'bill_id' => $bill->id, - 'amount_minor' => $fee, + 'amount_minor' => $amountPaid, 'method' => Payment::METHOD_CASH, 'status' => Payment::STATUS_PAID, 'reference' => 'DEMO-PAY-'.($i + 1), @@ -1820,6 +1841,8 @@ class DemoTenantSeeder 'recorded_by' => $ownerRef, ], ); + } else { + Payment::query()->where('uuid', $paymentUuid)->delete(); } } } diff --git a/resources/views/care/bills/index.blade.php b/resources/views/care/bills/index.blade.php index 8f434b7..6449192 100644 --- a/resources/views/care/bills/index.blade.php +++ b/resources/views/care/bills/index.blade.php @@ -3,7 +3,7 @@
Encounter billing and outstanding balances
+Outstanding balances ready for walk-up payment
| No bills yet. | ||||||
| + @if (($statusFilter ?? '') === 'outstanding') + No outstanding invoices at this branch. Choose “All statuses” to browse paid bills. + @else + No bills yet. + @endif + | +
Booth queue (optional)
+ @include('care.partials.queue-ops', [ + 'queueIntegration' => $queueIntegration, + 'queueCallNextRoute' => 'care.bills.call-next', + 'queueCallNextParams' => [], + 'queueCallNextDescription' => 'Only when a patient is waiting at billing. Otherwise open an unpaid invoice above.', + 'branchId' => $branchId ?? null, + 'variant' => 'muted', + ]) +