From 7ffa44b12829e01182aae07d9803f981640d173c Mon Sep 17 00:00:00 2001 From: isaacclad Date: Mon, 8 Jun 2026 21:47:33 +0000 Subject: [PATCH] Add mobile-only payment search for Ladill Mini. Wire the mobile search tab to a payments search page and remove desktop header search shortcuts. Co-authored-by: Cursor --- .../Controllers/Mini/PaymentsController.php | 16 ++++++- app/Http/Controllers/SearchController.php | 44 +++++++++++++------ resources/views/layouts/user.blade.php | 13 +----- .../partials/mobile-bottom-nav.blade.php | 3 +- resources/views/search/index.blade.php | 10 ++--- routes/web.php | 2 + tests/Feature/SearchTest.php | 34 +++++++++----- 7 files changed, 80 insertions(+), 42 deletions(-) diff --git a/app/Http/Controllers/Mini/PaymentsController.php b/app/Http/Controllers/Mini/PaymentsController.php index 88cc954..c60277c 100644 --- a/app/Http/Controllers/Mini/PaymentsController.php +++ b/app/Http/Controllers/Mini/PaymentsController.php @@ -13,6 +13,7 @@ class PaymentsController extends Controller public function index(Request $request): View { $account = ladill_account(); + $search = trim((string) $request->query('q', '')); $qrIds = $account->qrCodes() ->where('type', QrCode::TYPE_PAYMENT) @@ -20,12 +21,25 @@ class PaymentsController extends Controller $payments = MiniPayment::query() ->whereIn('qr_code_id', $qrIds) + ->when($search !== '', function ($query) use ($search) { + $like = '%'.$search.'%'; + $query->where(function ($inner) use ($like) { + $inner->where('payer_name', 'like', $like) + ->orWhere('payer_email', 'like', $like) + ->orWhere('payer_note', 'like', $like) + ->orWhere('reference', 'like', $like) + ->orWhere('payment_reference', 'like', $like) + ->orWhereHas('qrCode', fn ($qr) => $qr->where('label', 'like', $like)); + }); + }) ->with('qrCode') ->latest('created_at') - ->paginate(25); + ->paginate(25) + ->withQueryString(); return view('mini.payments', [ 'payments' => $payments, + 'search' => $search, ]); } } diff --git a/app/Http/Controllers/SearchController.php b/app/Http/Controllers/SearchController.php index 4d8bd9d..9cc2bae 100644 --- a/app/Http/Controllers/SearchController.php +++ b/app/Http/Controllers/SearchController.php @@ -2,8 +2,8 @@ namespace App\Http\Controllers; +use App\Models\MiniPayment; use App\Models\QrCode; -use App\Support\Qr\QrTypeCatalog; use Illuminate\Http\JsonResponse; use Illuminate\Http\Request; use Illuminate\View\View; @@ -28,26 +28,42 @@ class SearchController extends Controller /** @return list */ private function results(string $q): array { + $account = ladill_account(); $like = '%'.$q.'%'; - return ladill_account()->qrCodes() - ->whereIn('type', QrTypeCatalog::eventsTypes()) - ->where(function ($query) use ($like): void { - $query->where('label', 'like', $like) - ->orWhere('short_code', 'like', $like) - ->orWhere('destination_url', 'like', $like); + $qrIds = $account->qrCodes() + ->where('type', QrCode::TYPE_PAYMENT) + ->pluck('id'); + + if ($qrIds->isEmpty()) { + return []; + } + + return MiniPayment::query() + ->whereIn('qr_code_id', $qrIds) + ->with('qrCode') + ->where(function ($query) use ($like) { + $query->where('payer_name', 'like', $like) + ->orWhere('payer_email', 'like', $like) + ->orWhere('payer_note', 'like', $like) + ->orWhere('reference', 'like', $like) + ->orWhere('payment_reference', 'like', $like) + ->orWhereHas('qrCode', fn ($qr) => $qr->where('label', 'like', $like)); }) - ->orderByDesc('updated_at') + ->latest('created_at') ->limit(15) ->get() - ->map(function (QrCode $code): array { - $content = $code->content(); + ->map(function (MiniPayment $payment): array { + $amount = number_format( + ($payment->status === MiniPayment::STATUS_PAID ? $payment->merchant_amount_minor : $payment->amount_minor) / 100, + 2, + ); return [ - 'type' => $code->type === QrCode::TYPE_EVENT ? 'event' : 'programme', - 'title' => $content['name'] ?? $code->label, - 'subtitle' => $code->typeLabel().' · '.$code->publicUrl(), - 'url' => route('events.show', $code), + 'type' => 'payment', + 'title' => $payment->payer_name ?: 'Walk-in customer', + 'subtitle' => 'GHS '.$amount.' · '.($payment->qrCode?->label ?? 'Payment QR').' · '.ucfirst($payment->status), + 'url' => route('mini.payments.index', ['q' => $payment->reference]), ]; }) ->all(); diff --git a/resources/views/layouts/user.blade.php b/resources/views/layouts/user.blade.php index b4ca329..91082b0 100644 --- a/resources/views/layouts/user.blade.php +++ b/resources/views/layouts/user.blade.php @@ -65,8 +65,8 @@ @include('partials.mobile-bottom-nav', [ 'homeUrl' => route('mini.dashboard'), 'homeActive' => request()->routeIs('mini.dashboard'), - 'searchUrl' => route('mini.payment-qrs.index'), - 'searchActive' => request()->routeIs('mini.payment-qrs.*'), + 'searchUrl' => route('mini.search'), + 'searchActive' => request()->routeIs('mini.search'), 'notificationsUrl' => route('notifications.index'), 'notificationsActive' => request()->routeIs('notifications.*'), 'unreadUrl' => route('notifications.unread'), @@ -257,15 +257,6 @@ document.addEventListener('alpine:init', () => { }, })); }); - -// Global "/" shortcut to focus search -document.addEventListener('keydown', (e) => { - if (e.key === '/' && !['INPUT','TEXTAREA','SELECT'].includes(document.activeElement.tagName) && !document.activeElement.isContentEditable) { - e.preventDefault(); - const input = document.querySelector('[x-data*="topbarSearch"] input'); - if (input) input.focus(); - } -}); @include('partials.afia') @include('partials.sso-keepalive') diff --git a/resources/views/partials/mobile-bottom-nav.blade.php b/resources/views/partials/mobile-bottom-nav.blade.php index 98d9b82..69cda93 100644 --- a/resources/views/partials/mobile-bottom-nav.blade.php +++ b/resources/views/partials/mobile-bottom-nav.blade.php @@ -11,6 +11,7 @@ if ($profileMenuItems === [] && $profileUrl !== '#') { $profileMenuItems = [['type' => 'link', 'label' => 'Profile', 'href' => $profileUrl]]; } + $searchLabel = $searchLabel ?? 'Search'; $navActive = fn (bool $active) => $active ? 'text-indigo-600' : 'text-slate-600'; @endphp
@@ -26,7 +27,7 @@ - Search + {{ $searchLabel }} @if (!empty($centerCompose)) diff --git a/resources/views/search/index.blade.php b/resources/views/search/index.blade.php index b9503b7..48b5c11 100644 --- a/resources/views/search/index.blade.php +++ b/resources/views/search/index.blade.php @@ -4,10 +4,10 @@ @include('partials.search-screen', [ 'query' => $query, 'results' => $results, - 'backUrl' => route('events.dashboard'), - 'searchUrl' => route('events.search'), - 'heading' => 'Find events and programmes', - 'placeholder' => 'Search events, programmes, short codes…', - 'emptyHint' => 'Use at least 2 characters to search event names, programme titles, or short codes.', + 'backUrl' => route('mini.dashboard'), + 'searchUrl' => route('mini.search'), + 'heading' => 'Find payments', + 'placeholder' => 'Search payer, note, reference, QR…', + 'emptyHint' => 'Use at least 2 characters to search payer names, notes, references, or QR labels.', ]) diff --git a/routes/web.php b/routes/web.php index 0fcac6e..be71fee 100644 --- a/routes/web.php +++ b/routes/web.php @@ -11,6 +11,7 @@ use App\Http\Controllers\Public\QrScanController; use App\Http\Controllers\Qr\AccountController; use App\Http\Controllers\Qr\AfiaController; use App\Http\Controllers\Qr\TeamController; +use App\Http\Controllers\SearchController; use Illuminate\Support\Facades\Route; Route::get('/', fn () => auth()->check() @@ -36,6 +37,7 @@ Route::middleware(['auth'])->group(function () { Route::post('/notifications/mark-all-read', [NotificationController::class, 'markAllAsRead'])->name('notifications.mark-all-read'); Route::get('/dashboard', [OverviewController::class, 'index'])->name('mini.dashboard'); + Route::get('/search', SearchController::class)->name('mini.search'); Route::post('/afia/chat', [AfiaController::class, 'chat'])->name('mini.afia.chat'); Route::get('/payment-qrs', [PaymentQrController::class, 'index'])->name('mini.payment-qrs.index'); diff --git a/tests/Feature/SearchTest.php b/tests/Feature/SearchTest.php index 5b180a7..7c87c64 100644 --- a/tests/Feature/SearchTest.php +++ b/tests/Feature/SearchTest.php @@ -2,6 +2,7 @@ namespace Tests\Feature; +use App\Models\MiniPayment; use App\Models\QrCode; use App\Models\User; use Illuminate\Foundation\Testing\RefreshDatabase; @@ -17,25 +18,38 @@ class SearchTest extends TestCase return User::create(['public_id' => (string) Str::uuid(), 'name' => 'A', 'email' => 'a+'.uniqid().'@x.com']); } - public function test_user_can_search_owned_event_by_label(): void + public function test_user_can_search_payments_by_payer_name(): void { $user = $this->user(); - QrCode::query()->create([ + $qr = QrCode::query()->create([ 'user_id' => $user->id, - 'short_code' => 'summit', - 'type' => QrCode::TYPE_EVENT, - 'label' => 'NextGen Summit', + 'short_code' => 'till01', + 'type' => QrCode::TYPE_PAYMENT, + 'label' => 'Main till', 'destination_url' => 'https://example.com', 'is_active' => true, ]); + MiniPayment::query()->create([ + 'qr_code_id' => $qr->id, + 'user_id' => $user->id, + 'reference' => 'pay-'.Str::uuid(), + 'amount_minor' => 5000, + 'currency' => 'GHS', + 'platform_fee_minor' => 250, + 'merchant_amount_minor' => 4750, + 'payer_name' => 'Ama Mensah', + 'status' => MiniPayment::STATUS_PAID, + 'paid_at' => now(), + ]); + $response = $this->actingAs($user) - ->getJson(route('events.search', ['q' => 'NextGen'])); + ->getJson(route('mini.search', ['q' => 'Ama'])); $response->assertOk(); - $response->assertJsonPath('results.0.type', 'event'); - $response->assertJsonPath('results.0.title', 'NextGen Summit'); + $response->assertJsonPath('results.0.type', 'payment'); + $response->assertJsonPath('results.0.title', 'Ama Mensah'); } public function test_search_page_renders_for_authenticated_user(): void @@ -43,8 +57,8 @@ class SearchTest extends TestCase $user = $this->user(); $this->actingAs($user) - ->get(route('events.search')) + ->get(route('mini.search')) ->assertOk() - ->assertSee('Find events and programmes', false); + ->assertSee('Find payments', false); } }