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:
File diff suppressed because it is too large
Load Diff
@@ -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>
|
||||
|
||||
@@ -6,18 +6,21 @@
|
||||
import { Input } from '$lib/components/ui/input';
|
||||
import { authStore } from '$lib/stores/auth.svelte';
|
||||
import { formatCardCode } from '$lib/utils/format';
|
||||
import { isValidUKPhone, formatPhoneDisplay, toE164UK } from '$lib/utils/phone';
|
||||
|
||||
interface Props {
|
||||
amount: number;
|
||||
itemType: string;
|
||||
action: 'create' | 'topup';
|
||||
giftCardId?: string;
|
||||
userId?: string;
|
||||
isGuestMode?: boolean;
|
||||
guestEmail?: string;
|
||||
delivery?: 'account' | 'code';
|
||||
onClose: () => void;
|
||||
onComplete: (result: TillSaleResult) => void;
|
||||
}
|
||||
|
||||
let { amount, itemType, action, giftCardId = undefined, onClose, onComplete }: Props = $props();
|
||||
let { amount, itemType, action, giftCardId = undefined, userId = undefined, isGuestMode = false, guestEmail = undefined, delivery: deliveryProp = undefined, onClose, onComplete }: Props = $props();
|
||||
|
||||
type TillSaleResult = {
|
||||
id: string;
|
||||
@@ -31,7 +34,6 @@
|
||||
|
||||
type PaymentStep =
|
||||
| 'customer-selection'
|
||||
| 'delivery-selection'
|
||||
| 'payment-selection'
|
||||
| 'cash-entering'
|
||||
| 'cash-confirming'
|
||||
@@ -46,6 +48,30 @@
|
||||
let error = $state<string | null>(null);
|
||||
let result = $state<TillSaleResult | null>(null);
|
||||
|
||||
// Idempotency key — generated once per modal open, reused on retry
|
||||
let idempotencyKey = $state<string>('');
|
||||
|
||||
function getIdempotencyKey(): string {
|
||||
if (!idempotencyKey) {
|
||||
const array = new Uint8Array(16);
|
||||
if (typeof window !== 'undefined' && window.crypto) {
|
||||
window.crypto.getRandomValues(array);
|
||||
} else {
|
||||
for (let i = 0; i < 16; i++) array[i] = Math.floor(Math.random() * 256);
|
||||
}
|
||||
array[6] = (array[6] & 0x0f) | 0x40;
|
||||
array[8] = (array[8] & 0x3f) | 0x80;
|
||||
idempotencyKey = [...array]
|
||||
.map((b, i) => {
|
||||
const hex = b.toString(16).padStart(2, '0');
|
||||
if (i === 4 || i === 6 || i === 8 || i === 10) return '-' + hex;
|
||||
return hex;
|
||||
})
|
||||
.join('');
|
||||
}
|
||||
return idempotencyKey;
|
||||
}
|
||||
|
||||
// Selected customer info
|
||||
let selectedCustomer = $state<{ id: string; name: string } | null>(null);
|
||||
let isGuest = $state(false);
|
||||
@@ -89,11 +115,7 @@
|
||||
let searchTotal = $state(0);
|
||||
|
||||
// Customer Selection - Guest / Walk-in
|
||||
let guestName = $state('');
|
||||
let guestPhone = $state('');
|
||||
let guestPhoneError = $state('');
|
||||
let creatingGuest = $state(false);
|
||||
let guestNameError = $state('');
|
||||
|
||||
// Saved Cards (member users)
|
||||
let savedCardList = $state<
|
||||
@@ -203,28 +225,6 @@
|
||||
return { month, year };
|
||||
}
|
||||
|
||||
// =============== Phone Validation (UK) ===============
|
||||
|
||||
function validateGuestPhone(phone: string): boolean {
|
||||
if (!phone.trim()) {
|
||||
guestPhoneError = 'Phone number is required';
|
||||
return false;
|
||||
}
|
||||
const valid = isValidUKPhone(phone);
|
||||
guestPhoneError = valid ? '' : 'Invalid UK phone number';
|
||||
return valid;
|
||||
}
|
||||
|
||||
function formatGuestPhoneInput(value: string): string {
|
||||
return formatPhoneDisplay(value);
|
||||
}
|
||||
|
||||
function handleGuestPhoneInput(e: Event) {
|
||||
const target = e.target as HTMLInputElement;
|
||||
guestPhone = formatGuestPhoneInput(target.value);
|
||||
if (guestPhoneError) validateGuestPhone(guestPhone);
|
||||
}
|
||||
|
||||
// =============== Formatting ===============
|
||||
|
||||
function formatCurrency(n: number): string {
|
||||
@@ -232,6 +232,7 @@
|
||||
}
|
||||
|
||||
function resetView() {
|
||||
idempotencyKey = '';
|
||||
step = 'customer-selection';
|
||||
error = null;
|
||||
cashAmount = '';
|
||||
@@ -242,9 +243,6 @@
|
||||
delivery = 'code';
|
||||
userQuery = '';
|
||||
users = [];
|
||||
guestName = '';
|
||||
guestPhone = '';
|
||||
guestPhoneError = '';
|
||||
currentCustomerInfo = null;
|
||||
customerTab = 'current';
|
||||
currentPage = 1;
|
||||
@@ -262,7 +260,16 @@
|
||||
|
||||
// Auto-fetch current customer on mount
|
||||
onMount(() => {
|
||||
if (deliveryProp) delivery = deliveryProp;
|
||||
fetchCurrentCustomer();
|
||||
if (userId) {
|
||||
selectedCustomer = { id: userId, name: '' };
|
||||
isGuest = false;
|
||||
delivery = deliveryProp ?? 'account';
|
||||
step = 'payment-selection';
|
||||
} else if (isGuestMode) {
|
||||
createGuestCustomer();
|
||||
}
|
||||
});
|
||||
|
||||
// Auto-fetch saved cards when entering payment-selection for members
|
||||
@@ -307,13 +314,9 @@
|
||||
function selectCurrentCustomer() {
|
||||
if (!currentCustomerInfo) return;
|
||||
selectedCustomer = { id: currentCustomerInfo.id, name: currentCustomerInfo.name };
|
||||
isGuest = false;
|
||||
delivery = 'account';
|
||||
if (action === 'create') {
|
||||
step = 'delivery-selection';
|
||||
} else {
|
||||
step = 'payment-selection';
|
||||
}
|
||||
isGuest = currentCustomerInfo.email?.endsWith('@guest.invalid') || false;
|
||||
delivery = (action === 'topup' || isGuest) ? 'code' : 'account';
|
||||
step = 'payment-selection';
|
||||
}
|
||||
|
||||
async function searchCustomers(page: number = 1) {
|
||||
@@ -355,8 +358,8 @@
|
||||
const timestamp = Date.now();
|
||||
const firstName = 'Walk-in';
|
||||
const lastName = 'Guest';
|
||||
const email = `till-${timestamp}@guest.invalid`;
|
||||
const phone = '';
|
||||
const email = guestEmail || `till-${timestamp}@guest.invalid`;
|
||||
const phone = '07451234567';
|
||||
|
||||
const res = await fetch('/api/users/guest', {
|
||||
method: 'POST',
|
||||
@@ -367,7 +370,7 @@
|
||||
body: JSON.stringify({
|
||||
firstName,
|
||||
lastName,
|
||||
phone: toE164UK(guestPhone) ?? guestPhone,
|
||||
phone,
|
||||
email
|
||||
})
|
||||
});
|
||||
@@ -387,15 +390,11 @@
|
||||
}
|
||||
}
|
||||
|
||||
function selectCustomer(user: { id: string; fullName: string }) {
|
||||
function selectCustomer(user: { id: string; fullName: string; email?: string }) {
|
||||
selectedCustomer = { id: user.id, name: user.fullName };
|
||||
isGuest = false;
|
||||
delivery = 'account';
|
||||
if (action === 'create') {
|
||||
step = 'delivery-selection';
|
||||
} else {
|
||||
step = 'payment-selection';
|
||||
}
|
||||
isGuest = user.email?.endsWith('@guest.invalid') || false;
|
||||
delivery = (action === 'topup' || isGuest) ? 'code' : 'account';
|
||||
step = 'payment-selection';
|
||||
}
|
||||
|
||||
// =============== Payment ===============
|
||||
@@ -409,7 +408,8 @@
|
||||
item_type: itemType,
|
||||
action: action,
|
||||
amount: amount,
|
||||
payment_method: 'cash'
|
||||
payment_method: 'cash',
|
||||
idempotency_key: getIdempotencyKey()
|
||||
};
|
||||
if (giftCardId) body.gift_card_id = giftCardId;
|
||||
if (selectedCustomer) body.user_id = selectedCustomer.id;
|
||||
@@ -444,7 +444,8 @@
|
||||
item_type: itemType,
|
||||
action: action,
|
||||
amount: amount,
|
||||
payment_method: 'card_machine'
|
||||
payment_method: 'card_machine',
|
||||
idempotency_key: getIdempotencyKey()
|
||||
};
|
||||
if (giftCardId) body.gift_card_id = giftCardId;
|
||||
if (selectedCustomer) body.user_id = selectedCustomer.id;
|
||||
@@ -530,6 +531,7 @@
|
||||
action: action,
|
||||
amount: amount,
|
||||
payment_method: 'online_square',
|
||||
idempotency_key: getIdempotencyKey(),
|
||||
card_number: cardNum,
|
||||
card_exp_month: expMonth,
|
||||
card_exp_year: expYear,
|
||||
@@ -563,18 +565,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
// =============== Delivery Choice ===============
|
||||
|
||||
function selectDeliveryAccount() {
|
||||
delivery = 'account';
|
||||
step = 'payment-selection';
|
||||
}
|
||||
|
||||
function selectDeliveryCode() {
|
||||
delivery = 'code';
|
||||
step = 'payment-selection';
|
||||
}
|
||||
|
||||
// =============== Saved Cards (members only) ===============
|
||||
|
||||
async function fetchSavedCards() {
|
||||
@@ -605,6 +595,7 @@
|
||||
action: action,
|
||||
amount: amount,
|
||||
payment_method: 'saved_card',
|
||||
idempotency_key: getIdempotencyKey(),
|
||||
user_saved_card_id: selectedSavedCardId
|
||||
};
|
||||
if (giftCardId) body.gift_card_id = giftCardId;
|
||||
@@ -942,6 +933,32 @@
|
||||
<span class="text-xl font-bold text-gray-900">{formatCurrency(amount)}</span>
|
||||
</div>
|
||||
|
||||
{#if action === 'create' && !isGuest}
|
||||
<div class="space-y-1.5">
|
||||
<span class="text-xs font-semibold text-gray-500 uppercase tracking-wider block text-muted-foreground">Fulfillment Method</span>
|
||||
<div class="flex gap-2">
|
||||
<button
|
||||
type="button"
|
||||
class="flex-1 rounded-lg border px-4 py-2 text-center text-sm font-medium transition-colors {delivery === 'account'
|
||||
? 'border-input bg-fuchsia-100 text-foreground'
|
||||
: 'border-gray-200 bg-white text-gray-700 hover:bg-gray-50'}"
|
||||
onclick={() => (delivery = 'account')}
|
||||
>
|
||||
Add to Account
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="flex-1 rounded-lg border px-4 py-2 text-center text-sm font-medium transition-colors {delivery === 'code'
|
||||
? 'border-input bg-fuchsia-100 text-foreground'
|
||||
: 'border-gray-200 bg-white text-gray-700 hover:bg-gray-50'}"
|
||||
onclick={() => (delivery = 'code')}
|
||||
>
|
||||
Generate Code
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="grid grid-cols-1 gap-3 sm:grid-cols-2">
|
||||
<button
|
||||
type="button"
|
||||
@@ -1022,54 +1039,6 @@
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="flex gap-3 pt-2">
|
||||
<Button
|
||||
variant="ghost"
|
||||
onclick={() => {
|
||||
step = 'customer-selection';
|
||||
selectedCustomer = null;
|
||||
}}
|
||||
class="flex-1">Back</Button
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
{:else if step === 'delivery-selection'}
|
||||
<Dialog.Header>
|
||||
<Dialog.Title>Delivery Method</Dialog.Title>
|
||||
<Dialog.Description>
|
||||
How should {selectedCustomer?.name || 'the customer'} receive their gift card?
|
||||
</Dialog.Description>
|
||||
</Dialog.Header>
|
||||
|
||||
<div class="space-y-4 py-4">
|
||||
<div class="flex justify-between rounded-md border border-gray-200 bg-white p-4">
|
||||
<span class="text-base font-semibold text-gray-700">Total</span>
|
||||
<span class="text-xl font-bold text-gray-900">{formatCurrency(amount)}</span>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 gap-3">
|
||||
<button
|
||||
type="button"
|
||||
class="rounded-lg border border-input px-4 py-6 text-left transition-colors hover:bg-blue-50"
|
||||
onclick={selectDeliveryAccount}
|
||||
>
|
||||
<p class="text-sm font-semibold">Add to Account</p>
|
||||
<p class="mt-1 text-xs text-gray-500">
|
||||
Balance is credited immediately — no code needed
|
||||
</p>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="rounded-lg border border-input px-4 py-6 text-left transition-colors hover:bg-fuchsia-50"
|
||||
onclick={selectDeliveryCode}
|
||||
>
|
||||
<p class="text-sm font-semibold">Generate Gift Code</p>
|
||||
<p class="mt-1 text-xs text-gray-500">
|
||||
A 12-character code is generated — share with the customer
|
||||
</p>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="flex gap-3 pt-2">
|
||||
<Button
|
||||
variant="ghost"
|
||||
|
||||
Reference in New Issue
Block a user