From c9bcc58fb0a4b2f609ac5cb6a9ec397ce1269c15 Mon Sep 17 00:00:00 2001 From: isaacclad Date: Tue, 23 Jun 2026 12:25:56 +0000 Subject: [PATCH] Add wallet balance widget to the profile menu (after Billing) Wallet balance peek rendered as a menu row directly under Billing, backed by a /wallet/balance endpoint (BillingClient) and guarded by Route::has. Also opens Afia via a direct window event for reliability. Co-Authored-By: Claude Opus 4.8 --- .../Controllers/WalletBalanceController.php | 40 ++++++++ app/Support/UserProfileMenu.php | 5 + config/billing.php | 2 + resources/js/app.js | 22 +++++ resources/js/ladill-search-shortcut.js | 97 +++++++++++++++++++ resources/views/layouts/user.blade.php | 10 +- .../views/partials/afia-button.blade.php | 2 +- .../views/partials/search-screen.blade.php | 4 +- resources/views/partials/topbar-qr.blade.php | 4 +- resources/views/partials/topbar.blade.php | 4 +- .../partials/user-profile-menu.blade.php | 2 + .../views/partials/wallet-widget.blade.php | 23 +++++ routes/web.php | 2 + 13 files changed, 201 insertions(+), 16 deletions(-) create mode 100644 app/Http/Controllers/WalletBalanceController.php create mode 100644 resources/js/ladill-search-shortcut.js create mode 100644 resources/views/partials/wallet-widget.blade.php diff --git a/app/Http/Controllers/WalletBalanceController.php b/app/Http/Controllers/WalletBalanceController.php new file mode 100644 index 0000000..29d4267 --- /dev/null +++ b/app/Http/Controllers/WalletBalanceController.php @@ -0,0 +1,40 @@ +user()->public_id; + + $minor = Cache::remember("wallet_balance:{$publicId}", now()->addSeconds(30), function () use ($billing, $publicId) { + try { + return $billing->balanceMinor($publicId); + } catch (\Throwable) { + return null; + } + }); + + if ($minor === null) { + return response()->json(['available' => false]); + } + + $currency = (string) config('billing.currency', 'GHS'); + + return response()->json([ + 'available' => true, + 'balance_minor' => $minor, + 'currency' => $currency, + 'formatted' => $currency.' '.number_format($minor / 100, 2), + ]); + } +} diff --git a/app/Support/UserProfileMenu.php b/app/Support/UserProfileMenu.php index 3228d49..2806cdb 100644 --- a/app/Support/UserProfileMenu.php +++ b/app/Support/UserProfileMenu.php @@ -43,6 +43,11 @@ class UserProfileMenu ]; } + + if (Route::has((string) config("billing.wallet_balance_route", "wallet.balance"))) { + $items[] = ["type" => "wallet"]; + } + if (self::userIsAdmin($user)) { $adminHref = self::platformUrl('account', 'admin'); diff --git a/config/billing.php b/config/billing.php index f74369b..ce61575 100644 --- a/config/billing.php +++ b/config/billing.php @@ -4,4 +4,6 @@ return [ 'api_url' => env('BILLING_API_URL', 'https://ladill.com/api/billing'), 'api_key' => env('BILLING_API_KEY_EVENTS'), 'service' => 'events', + 'wallet_balance_route' => 'wallet.balance', + 'currency' => 'GHS', ]; diff --git a/resources/js/app.js b/resources/js/app.js index ad8634c..5f49f46 100644 --- a/resources/js/app.js +++ b/resources/js/app.js @@ -1,8 +1,10 @@ import './bootstrap'; import { registerLadillConfirmStore, registerLadillModalHelpers } from './ladill-modals'; +import { registerLadillSearchShortcut } from './ladill-search-shortcut'; registerLadillModalHelpers(); +registerLadillSearchShortcut(); import Alpine from 'alpinejs'; @@ -195,6 +197,26 @@ Alpine.data('topbarSearch', (config = {}) => ({ }, })); + +// Wallet balance peek for the avatar dropdown. +Alpine.data('walletWidget', (config = {}) => ({ + display: '…', + async load() { + if (! config.url) { + this.display = 'View wallet'; + + return; + } + try { + const res = await fetch(config.url, { headers: { Accept: 'application/json', 'X-Requested-With': 'XMLHttpRequest' } }); + const data = await res.json(); + this.display = data.available ? data.formatted : 'View wallet'; + } catch (e) { + this.display = 'View wallet'; + } + }, +})); + window.Alpine = Alpine; registerLadillConfirmStore(Alpine); diff --git a/resources/js/ladill-search-shortcut.js b/resources/js/ladill-search-shortcut.js new file mode 100644 index 0000000..72b493f --- /dev/null +++ b/resources/js/ladill-search-shortcut.js @@ -0,0 +1,97 @@ +function isEditableTarget(element) { + if (!(element instanceof HTMLElement)) { + return false; + } + + const tag = element.tagName; + + if (tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT') { + return true; + } + + return element.isContentEditable; +} + +function isVisibleInput(input) { + if (!(input instanceof HTMLElement)) { + return false; + } + + const style = window.getComputedStyle(input); + + return style.display !== 'none' + && style.visibility !== 'hidden' + && style.opacity !== '0'; +} + +function findSearchInput() { + const marked = [...document.querySelectorAll('[data-ladill-search-input]')]; + const visibleMarked = marked.find(isVisibleInput); + + if (visibleMarked) { + return visibleMarked; + } + + if (marked.length > 0) { + return marked[0]; + } + + return document.querySelector('[x-data*="topbarSearch"] input'); +} + +function focusSearchInput() { + const input = findSearchInput(); + + if (!input) { + const fallbackUrl = document.body?.dataset?.ladillSearchUrl; + + if (fallbackUrl) { + window.location.href = fallbackUrl; + } + + return false; + } + + if (!isVisibleInput(input)) { + const fallbackUrl = document.body?.dataset?.ladillSearchUrl; + + if (fallbackUrl) { + window.location.href = fallbackUrl; + + return true; + } + } + + input.focus(); + + if (input instanceof HTMLInputElement && (input.type === 'text' || input.type === 'search')) { + input.select(); + } + + return true; +} + +export function registerLadillSearchShortcut() { + if (window.__ladillSearchShortcutRegistered) { + return; + } + + window.__ladillSearchShortcutRegistered = true; + + document.addEventListener('keydown', (event) => { + if (event.key !== '/' && event.code !== 'Slash') { + return; + } + + if (event.ctrlKey || event.metaKey || event.altKey) { + return; + } + + if (isEditableTarget(document.activeElement)) { + return; + } + + event.preventDefault(); + focusSearchInput(); + }); +} diff --git a/resources/views/layouts/user.blade.php b/resources/views/layouts/user.blade.php index 64948ca..aae3fcc 100644 --- a/resources/views/layouts/user.blade.php +++ b/resources/views/layouts/user.blade.php @@ -18,7 +18,7 @@ @vite(['resources/css/app.css', 'resources/js/app.js']) - +
{{-- Mobile sidebar overlay --}}
@@ -254,14 +254,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/afia-button.blade.php b/resources/views/partials/afia-button.blade.php index f8eede3..0588ba6 100644 --- a/resources/views/partials/afia-button.blade.php +++ b/resources/views/partials/afia-button.blade.php @@ -8,7 +8,7 @@ @if ($handler === 'openAfia') @click="openAfia()" @else - @click="$dispatch('afia-open')" + @click="window.dispatchEvent(new CustomEvent('afia-open'))" @endif @class([ 'inline-flex shrink-0 items-center justify-center gap-2 rounded-xl bg-gradient-to-r from-indigo-700 via-blue-600 to-emerald-400 text-sm font-semibold text-white shadow-sm transition hover:opacity-95', diff --git a/resources/views/partials/search-screen.blade.php b/resources/views/partials/search-screen.blade.php index ee04c30..be97ed7 100644 --- a/resources/views/partials/search-screen.blade.php +++ b/resources/views/partials/search-screen.blade.php @@ -14,7 +14,7 @@
- {{ $heading }}
-
-
+ class="absolute right-0 z-50 mt-2 w-64 rounded-xl border border-slate-200 bg-white shadow-lg"> @include('partials.user-profile-menu', [ 'items' => \App\Support\UserProfileMenu::items($user), ]) diff --git a/resources/views/partials/topbar.blade.php b/resources/views/partials/topbar.blade.php index 39fd405..a0ae239 100644 --- a/resources/views/partials/topbar.blade.php +++ b/resources/views/partials/topbar.blade.php @@ -51,7 +51,7 @@