feat(frontend): add gift card management UI and payment modal updates
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
@@ -52,10 +52,11 @@
|
||||
let loadingSavedCardList = $state(false);
|
||||
|
||||
async function fetchCustomerGiftCardBalance() {
|
||||
if (!booking.user_id) return;
|
||||
const targetUserId = booking.user_id ?? booking.user?.id;
|
||||
if (!targetUserId) return;
|
||||
loadingCustomerBalance = true;
|
||||
try {
|
||||
const res = await fetch(`/api/admin/users/${booking.user_id}/giftcard-balance`, {
|
||||
const res = await fetch(`/api/admin/users/${targetUserId}/giftcard-balance`, {
|
||||
headers: { Authorization: `Bearer ${authStore.currentToken}` }
|
||||
});
|
||||
if (res.ok) {
|
||||
@@ -74,10 +75,11 @@
|
||||
}
|
||||
|
||||
async function fetchSavedCardList() {
|
||||
if (!booking.user_id) return;
|
||||
const targetUserId = booking.user_id ?? booking.user?.id;
|
||||
if (!targetUserId) return;
|
||||
loadingSavedCardList = true;
|
||||
try {
|
||||
const res = await fetch(`/api/admin/users/${booking.user_id}/payment-methods`, {
|
||||
const res = await fetch(`/api/admin/users/${targetUserId}/payment-methods`, {
|
||||
headers: { Authorization: `Bearer ${authStore.currentToken}` }
|
||||
});
|
||||
if (res.ok) {
|
||||
@@ -98,8 +100,11 @@
|
||||
let serviceOverrides = $state<Record<string, ServiceOverride>>({});
|
||||
|
||||
$effect(() => {
|
||||
fetchCustomerGiftCardBalance();
|
||||
fetchSavedCardList();
|
||||
const uid = booking.user_id ?? booking.user?.id;
|
||||
if (uid) {
|
||||
fetchCustomerGiftCardBalance();
|
||||
fetchSavedCardList();
|
||||
}
|
||||
const services = booking.services ?? [];
|
||||
const overrides: Record<string, ServiceOverride> = {};
|
||||
for (const s of services) {
|
||||
@@ -532,7 +537,8 @@
|
||||
let selectedSavedCardId = $state<string | null>(null);
|
||||
|
||||
async function fetchSavedCards() {
|
||||
if (!booking.user_id) return;
|
||||
const targetUserId = booking.user_id ?? booking.user?.id;
|
||||
if (!targetUserId) return;
|
||||
loadingSavedCards = true;
|
||||
savedCards = [];
|
||||
selectedSavedCardId = null;
|
||||
@@ -658,16 +664,16 @@
|
||||
</div>
|
||||
|
||||
{#if booking.discounts && booking.discounts.length > 0}
|
||||
<div class="rounded-md border border-fuchsia-100 bg-fuchsia-50/40 p-4">
|
||||
<div class="rounded-md border border-gray-100 bg-gray-50/50 p-4">
|
||||
<div class="mb-3 flex items-center justify-between">
|
||||
<div class="text-sm font-semibold text-fuchsia-800 flex items-center gap-1.5">
|
||||
<svg class="h-4 w-4 text-fuchsia-600" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<div class="text-sm font-semibold text-gray-800 flex items-center gap-1.5">
|
||||
<svg class="h-4 w-4 text-gray-500" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M20.59 13.41l-7.17 7.17a2 2 0 0 1-2.83 0L2 12V2h10l8.59 8.59a2 2 0 0 1 0 2.82z"></path>
|
||||
<line x1="7" y1="7" x2="7.01" y2="7"></line>
|
||||
</svg>
|
||||
Applied Discounts
|
||||
</div>
|
||||
<span class="rounded-full bg-fuchsia-100 px-2 py-0.5 text-xs font-semibold text-fuchsia-700">
|
||||
<span class="rounded-full bg-fuchsia-50 px-2 py-0.5 text-xs font-medium text-fuchsia-600">
|
||||
{((discountSum / subtotal) * 100).toFixed(0)}% Off Total
|
||||
</span>
|
||||
</div>
|
||||
@@ -675,7 +681,7 @@
|
||||
{#each booking.discounts as d}
|
||||
<div class="flex items-center justify-between text-gray-600">
|
||||
<div class="flex items-center gap-1.5">
|
||||
<span class="h-1.5 w-1.5 rounded-full bg-fuchsia-500"></span>
|
||||
<span class="h-1.5 w-1.5 rounded-full bg-fuchsia-400"></span>
|
||||
<span>
|
||||
{#if d.discount_source === 'loyalty'}
|
||||
Loyalty Stamp Card (10% Off)
|
||||
@@ -686,7 +692,7 @@
|
||||
{/if}
|
||||
</span>
|
||||
</div>
|
||||
<span class="font-medium text-fuchsia-600">-{formatCurrency(d.discount_amount)}</span>
|
||||
<span class="font-medium text-gray-900">-{formatCurrency(d.discount_amount)}</span>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
@@ -895,7 +901,7 @@
|
||||
|
||||
<div class="flex gap-3">
|
||||
<Button variant="ghost" onclick={resetToSelect} class="flex-1">Back</Button>
|
||||
<Button onclick={handleCardPayment} class="flex-1 bg-green-600 hover:bg-green-700">
|
||||
<Button onclick={handleCardPayment} class="flex-1">
|
||||
Charge Card
|
||||
</Button>
|
||||
</div>
|
||||
@@ -903,7 +909,7 @@
|
||||
{:else if status === 'card-processing' || status === 'card-polling'}
|
||||
<div class="flex flex-col items-center justify-center py-8">
|
||||
<div
|
||||
class="mb-4 h-12 w-12 animate-spin rounded-full border-4 border-gray-200 border-t-green-600"
|
||||
class="mb-4 h-12 w-12 animate-spin rounded-full border-4 border-gray-200 border-t-primary"
|
||||
></div>
|
||||
<p class="text-lg font-medium text-gray-700">Waiting for customer to tap card...</p>
|
||||
<p class="mt-2 text-sm text-gray-500">This may take a few moments</p>
|
||||
@@ -952,7 +958,7 @@
|
||||
<Button variant="ghost" onclick={resetToSelect} class="flex-1">Back</Button>
|
||||
<Button
|
||||
onclick={handleCashPayment}
|
||||
class="flex-1 bg-green-600 hover:bg-green-700"
|
||||
class="flex-1"
|
||||
disabled={cashAmountNum < totalDue}
|
||||
>
|
||||
Confirm Cash
|
||||
@@ -962,7 +968,7 @@
|
||||
{:else if status === 'cash-confirming'}
|
||||
<div class="flex flex-col items-center justify-center py-8">
|
||||
<div
|
||||
class="mb-4 h-12 w-12 animate-spin rounded-full border-4 border-gray-200 border-t-green-600"
|
||||
class="mb-4 h-12 w-12 animate-spin rounded-full border-4 border-gray-200 border-t-primary"
|
||||
></div>
|
||||
<p class="text-lg font-medium text-gray-700">Processing cash payment...</p>
|
||||
</div>
|
||||
@@ -973,14 +979,14 @@
|
||||
<span class="text-xl font-bold text-gray-900">{formatCurrency(totalDue)}</span>
|
||||
</div>
|
||||
|
||||
{#if booking.user_id && customerBalance > 0}
|
||||
{#if (booking.user_id ?? booking.user?.id) && customerBalance > 0}
|
||||
<div class="space-y-2">
|
||||
<span class="text-xs font-semibold text-gray-500 uppercase tracking-wider block">Source</span>
|
||||
<div class="grid grid-cols-2 gap-2">
|
||||
<button
|
||||
type="button"
|
||||
class="rounded-lg border py-2 text-center text-xs font-medium transition-colors {useAccountBalance
|
||||
? 'border-fuchsia-600 bg-fuchsia-50 text-fuchsia-900 font-semibold'
|
||||
? 'border-input bg-fuchsia-100 text-foreground'
|
||||
: 'border-gray-200 hover:bg-gray-50'}"
|
||||
onclick={() => useAccountBalance = true}
|
||||
>
|
||||
@@ -989,7 +995,7 @@
|
||||
<button
|
||||
type="button"
|
||||
class="rounded-lg border py-2 text-center text-xs font-medium transition-colors {!useAccountBalance
|
||||
? 'border-fuchsia-600 bg-fuchsia-50 text-fuchsia-900 font-semibold'
|
||||
? 'border-input bg-fuchsia-100 text-foreground'
|
||||
: 'border-gray-200 hover:bg-gray-50'}"
|
||||
onclick={() => useAccountBalance = false}
|
||||
>
|
||||
@@ -1002,14 +1008,24 @@
|
||||
{#if useAccountBalance}
|
||||
<div>
|
||||
<label for="giftcard-amount" class="text-sm font-medium text-gray-700">Amount to pay with Balance (£)</label>
|
||||
<Input
|
||||
id="giftcard-amount"
|
||||
type="text"
|
||||
inputmode="decimal"
|
||||
value={giftCardPaymentAmount}
|
||||
oninput={(e) => giftCardPaymentAmount = (e.target as HTMLInputElement).value}
|
||||
class="mt-1 font-mono text-lg"
|
||||
/>
|
||||
<div class="mt-1 flex gap-2">
|
||||
<Input
|
||||
id="giftcard-amount"
|
||||
type="text"
|
||||
inputmode="decimal"
|
||||
value={giftCardPaymentAmount}
|
||||
oninput={(e) => giftCardPaymentAmount = (e.target as HTMLInputElement).value}
|
||||
class="flex-1 font-mono text-lg"
|
||||
/>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onclick={() => { giftCardPaymentAmount = Math.min(customerBalance, totalDue).toFixed(2); }}
|
||||
class="shrink-0 text-xs"
|
||||
>
|
||||
Full Balance
|
||||
</Button>
|
||||
</div>
|
||||
<p class="mt-1 text-xs text-gray-500">
|
||||
Available balance: {formatCurrency(customerBalance)}. Maximum of total due or balance can be used.
|
||||
</p>
|
||||
@@ -1036,7 +1052,7 @@
|
||||
<Button variant="ghost" onclick={resetToSelect} class="flex-1">Back</Button>
|
||||
<Button
|
||||
onclick={handleGiftCardPayment}
|
||||
class="flex-1 bg-green-600 hover:bg-green-700 text-white"
|
||||
class="flex-1"
|
||||
disabled={!giftCardValid}
|
||||
>
|
||||
Apply Gift Card
|
||||
@@ -1046,7 +1062,7 @@
|
||||
{:else if status === 'gift-confirming'}
|
||||
<div class="flex flex-col items-center justify-center py-8">
|
||||
<div
|
||||
class="mb-4 h-12 w-12 animate-spin rounded-full border-4 border-gray-200 border-t-green-600"
|
||||
class="mb-4 h-12 w-12 animate-spin rounded-full border-4 border-gray-200 border-t-primary"
|
||||
></div>
|
||||
<p class="text-lg font-medium text-gray-700">Processing gift card...</p>
|
||||
</div>
|
||||
@@ -1059,7 +1075,7 @@
|
||||
|
||||
{#if loadingSavedCards}
|
||||
<div class="flex justify-center py-8">
|
||||
<div class="h-8 w-8 animate-spin rounded-full border-4 border-gray-200 border-t-fuchsia-600"></div>
|
||||
<div class="h-8 w-8 animate-spin rounded-full border-4 border-gray-200 border-t-primary"></div>
|
||||
</div>
|
||||
{:else if savedCards.length === 0}
|
||||
<div class="rounded-md border border-gray-200 bg-gray-50 p-6 text-center">
|
||||
@@ -1073,7 +1089,7 @@
|
||||
<button
|
||||
type="button"
|
||||
class="w-full rounded-lg border p-3 text-left transition-colors {selectedSavedCardId === card.id
|
||||
? 'border-fuchsia-600 bg-fuchsia-50'
|
||||
? 'border-input bg-fuchsia-100'
|
||||
: 'border-gray-200 hover:bg-gray-50'}"
|
||||
onclick={() => selectedSavedCardId = card.id}
|
||||
>
|
||||
@@ -1108,7 +1124,7 @@
|
||||
<Button variant="ghost" onclick={resetToSelect} class="flex-1">Back</Button>
|
||||
<Button
|
||||
onclick={handleSavedCardPayment}
|
||||
class="flex-1 bg-green-600 hover:bg-green-700 text-white"
|
||||
class="flex-1"
|
||||
disabled={!selectedSavedCardId}
|
||||
>
|
||||
Charge Saved Card
|
||||
@@ -1118,7 +1134,7 @@
|
||||
{:else if status === 'saved-card-processing'}
|
||||
<div class="flex flex-col items-center justify-center py-8">
|
||||
<div
|
||||
class="mb-4 h-12 w-12 animate-spin rounded-full border-4 border-gray-200 border-t-green-600"
|
||||
class="mb-4 h-12 w-12 animate-spin rounded-full border-4 border-gray-200 border-t-primary"
|
||||
></div>
|
||||
<p class="text-lg font-medium text-gray-700">Processing saved card payment...</p>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user