From 4ad3a8a90146eed5503c3368f5612e070698f996 Mon Sep 17 00:00:00 2001 From: isaacclad Date: Sat, 18 Jul 2026 23:40:04 +0000 Subject: [PATCH] Show payments and paid bills on billing dashboards. Cashiers and other bills.view roles get today's collections breakdown and a recent paid-invoice list under the existing metric cards. Co-authored-by: Cursor --- .../Controllers/Care/DashboardController.php | 11 +++ app/Services/Care/ReportService.php | 64 +++++++++++++ resources/views/care/dashboard.blade.php | 81 ++++++++++++++++ tests/Feature/CareWebTest.php | 92 ++++++++++++++++++- 4 files changed, 247 insertions(+), 1 deletion(-) diff --git a/app/Http/Controllers/Care/DashboardController.php b/app/Http/Controllers/Care/DashboardController.php index e937b8e..9cbfae1 100644 --- a/app/Http/Controllers/Care/DashboardController.php +++ b/app/Http/Controllers/Care/DashboardController.php @@ -80,6 +80,14 @@ class DashboardController extends Controller ? $this->patientQueueForMember($request, $organization->id, $owner, $member, $branchScope) : [collect(), collect(), null]; + $showBillingPanel = $canBills; + $paymentStats = $showBillingPanel + ? $this->reports->todayPaymentStats($owner, $organization->id, $branchScope) + : null; + $recentPaidBills = $showBillingPanel + ? $this->reports->recentPaidBills($owner, $organization->id, $branchScope) + : collect(); + return view('care.dashboard', [ 'organization' => $organization, 'member' => $member, @@ -98,6 +106,9 @@ class DashboardController extends Controller 'inConsultation' => $inConsultation, 'practitionerId' => $practitionerId, 'specialtyModules' => $this->specialtyModules->enabledModulesForMember($organization, $member), + 'showBillingPanel' => $showBillingPanel, + 'paymentStats' => $paymentStats, + 'recentPaidBills' => $recentPaidBills, ]); } diff --git a/app/Services/Care/ReportService.php b/app/Services/Care/ReportService.php index 20f2ca3..28209c9 100644 --- a/app/Services/Care/ReportService.php +++ b/app/Services/Care/ReportService.php @@ -78,6 +78,70 @@ class ReportService ]; } + /** + * Today's payment collections for billing dashboards (cashiers / bills.view). + * + * @return array{ + * collected_minor: int, + * payment_count: int, + * cash_minor: int, + * other_minor: int + * } + */ + public function todayPaymentStats( + string $ownerRef, + int $organizationId, + ?int $branchId = null, + ): array { + $today = now()->startOfDay(); + + $base = Payment::owned($ownerRef) + ->where('status', Payment::STATUS_PAID) + ->whereHas('bill', function (Builder $q) use ($organizationId, $branchId) { + $q->where('organization_id', $organizationId); + if ($branchId) { + $q->where('branch_id', $branchId); + } + }) + ->whereDate('paid_at', $today); + + $collected = (int) (clone $base)->sum('amount_minor'); + $count = (clone $base)->count(); + $cash = (int) (clone $base)->where('method', Payment::METHOD_CASH)->sum('amount_minor'); + + return [ + 'collected_minor' => $collected, + 'payment_count' => $count, + 'cash_minor' => $cash, + 'other_minor' => max(0, $collected - $cash), + ]; + } + + /** + * Recently paid invoices for billing dashboards. + * + * @return Collection + */ + public function recentPaidBills( + string $ownerRef, + int $organizationId, + ?int $branchId = null, + int $limit = 10, + ): Collection { + return Bill::owned($ownerRef) + ->where('organization_id', $organizationId) + ->when($branchId, fn ($q) => $q->where('branch_id', $branchId)) + ->where('status', Bill::STATUS_PAID) + ->with('patient') + ->withMax([ + 'payments as last_paid_at' => fn ($q) => $q->where('status', Payment::STATUS_PAID), + ], 'paid_at') + ->orderByDesc('last_paid_at') + ->orderByDesc('id') + ->limit($limit) + ->get(); + } + /** * @return array */ diff --git a/resources/views/care/dashboard.blade.php b/resources/views/care/dashboard.blade.php index 42a4427..a8afd49 100644 --- a/resources/views/care/dashboard.blade.php +++ b/resources/views/care/dashboard.blade.php @@ -276,4 +276,85 @@ @endforelse @endif + + @if (! empty($showBillingPanel) && $paymentStats) + @php + $money = fn (int $minor) => $currency.' '.number_format($minor / 100, 2); + @endphp +
+
+
+

Payments today

+

Collections at your branch

+
+ Open billing +
+
+
+

Collected

+

{{ $money($paymentStats['collected_minor']) }}

+
+
+

Payments

+

{{ number_format($paymentStats['payment_count']) }}

+
+
+

Cash

+

{{ $money($paymentStats['cash_minor']) }}

+
+
+

Other methods

+

{{ $money($paymentStats['other_minor']) }}

+
+
+
+ +
+
+
+

Recently paid bills

+

Latest settled invoices

+
+ View paid +
+
+ + + + + + + + + + + + @forelse ($recentPaidBills as $bill) + @php + $billUrl = route('care.bills.show', $bill); + $paidAt = $bill->last_paid_at + ? \Illuminate\Support\Carbon::parse($bill->last_paid_at)->format('d M Y, H:i') + : '—'; + @endphp + + + + + + + + @empty + + + + @endforelse + +
InvoicePatientTotalPaid at
+ {{ $bill->invoice_number }} + {{ $bill->patient?->fullName() ?? 'Patient' }}{{ $money((int) $bill->total_minor) }}{{ $paidAt }} + View +
No paid bills yet.
+
+
+ @endif diff --git a/tests/Feature/CareWebTest.php b/tests/Feature/CareWebTest.php index 69ce3cb..eaa61ec 100644 --- a/tests/Feature/CareWebTest.php +++ b/tests/Feature/CareWebTest.php @@ -83,7 +83,9 @@ class CareWebTest extends TestCase ->assertOk() ->assertSee('Test Clinic') ->assertSee('Revenue today') - ->assertSee('Open bills'); + ->assertSee('Open bills') + ->assertSee('Payments today') + ->assertSee('Recently paid bills'); } public function test_doctor_dashboard_hides_finance_metrics(): void @@ -99,10 +101,98 @@ class CareWebTest extends TestCase ->assertSee('No patients waiting right now.') ->assertDontSee('Revenue today') ->assertDontSee('Open bills') + ->assertDontSee('Payments today') + ->assertDontSee('Recently paid bills') ->assertDontSee('Team members') ->assertDontSee('Active branches'); } + public function test_cashier_dashboard_shows_payments_and_paid_bills(): void + { + Member::where('user_ref', $this->user->public_id)->update([ + 'role' => 'cashier', + 'branch_id' => Branch::firstOrFail()->id, + ]); + + $branch = Branch::firstOrFail(); + $patient = \App\Models\Patient::create([ + 'uuid' => (string) \Illuminate\Support\Str::uuid(), + 'owner_ref' => $this->user->public_id, + 'organization_id' => $this->organization->id, + 'branch_id' => $branch->id, + 'patient_number' => 'LC-2026-00099', + 'first_name' => 'Ama', + 'last_name' => 'Mensah', + ]); + + $visit = \App\Models\Visit::create([ + 'uuid' => (string) \Illuminate\Support\Str::uuid(), + 'owner_ref' => $this->user->public_id, + 'organization_id' => $this->organization->id, + 'branch_id' => $branch->id, + 'patient_id' => $patient->id, + 'status' => \App\Models\Visit::STATUS_COMPLETED, + 'checked_in_at' => now()->subHour(), + 'completed_at' => now(), + ]); + + $bill = \App\Models\Bill::create([ + 'uuid' => (string) \Illuminate\Support\Str::uuid(), + 'owner_ref' => $this->user->public_id, + 'organization_id' => $this->organization->id, + 'branch_id' => $branch->id, + 'visit_id' => $visit->id, + 'patient_id' => $patient->id, + 'invoice_number' => 'INV-DASH-001', + 'status' => \App\Models\Bill::STATUS_PAID, + 'subtotal_minor' => 4500, + 'total_minor' => 4500, + 'amount_paid_minor' => 4500, + 'balance_minor' => 0, + ]); + + \App\Models\Payment::create([ + 'uuid' => (string) \Illuminate\Support\Str::uuid(), + 'owner_ref' => $this->user->public_id, + 'bill_id' => $bill->id, + 'amount_minor' => 3000, + 'method' => \App\Models\Payment::METHOD_CASH, + 'status' => \App\Models\Payment::STATUS_PAID, + 'paid_at' => now(), + 'recorded_by' => $this->user->public_id, + ]); + + \App\Models\Payment::create([ + 'uuid' => (string) \Illuminate\Support\Str::uuid(), + 'owner_ref' => $this->user->public_id, + 'bill_id' => $bill->id, + 'amount_minor' => 1500, + 'method' => \App\Models\Payment::METHOD_MOMO, + 'status' => \App\Models\Payment::STATUS_PAID, + 'paid_at' => now(), + 'recorded_by' => $this->user->public_id, + ]); + + $this->actingAs($this->user) + ->get(route('care.dashboard')) + ->assertOk() + ->assertSee('Patients today') + ->assertSee('Open bills') + ->assertSee('Payments today') + ->assertSee('Collected') + ->assertSee('GHS 45.00') + ->assertSee('Cash') + ->assertSee('GHS 30.00') + ->assertSee('Other methods') + ->assertSee('GHS 15.00') + ->assertSee('Recently paid bills') + ->assertSee('INV-DASH-001') + ->assertSee('Ama Mensah') + ->assertSee(route('care.bills.show', $bill), false) + ->assertDontSee('Revenue today') + ->assertDontSee('Patients in queue'); + } + public function test_onboarding_creates_organization(): void { Organization::query()->delete();