fix: remove unused variables from Svelte components
This commit is contained in:
@@ -150,7 +150,7 @@
|
||||
const data = await response.json();
|
||||
overlappingBookings = data.bookings || [];
|
||||
}
|
||||
} catch (_err) {
|
||||
} catch {
|
||||
// Silently handle - overlapping bookings couldn't be fetched
|
||||
} finally {
|
||||
loadingOverlaps = false;
|
||||
@@ -337,7 +337,7 @@
|
||||
const text = await response.text();
|
||||
toast.error('Failed to confirm: ' + sanitizeText(text), { id: loadingToast });
|
||||
}
|
||||
} catch (_err) {
|
||||
} catch {
|
||||
toast.error('Network error confirming booking', { id: loadingToast });
|
||||
} finally {
|
||||
submitting = false;
|
||||
@@ -365,7 +365,7 @@
|
||||
const text = await response.text();
|
||||
toast.error('Failed to decline: ' + sanitizeText(text), { id: loadingToast });
|
||||
}
|
||||
} catch (_err) {
|
||||
} catch {
|
||||
toast.error('Network error declining booking', { id: loadingToast });
|
||||
} finally {
|
||||
submitting = false;
|
||||
|
||||
@@ -181,7 +181,6 @@
|
||||
let selectedTime = $state<string | null>(null);
|
||||
let workingHours = $state<Record<string, DayHours> | null>(null);
|
||||
let availableHours = $state<Record<string, DayAvailability> | null>(null);
|
||||
let _loadingWorkingHours = $state(false);
|
||||
let loadingAvailableHours = $state(false);
|
||||
let hoursRangeGeneration = $state(0);
|
||||
let hoursMonthGeneration = $state(0);
|
||||
@@ -485,7 +484,7 @@
|
||||
(user: { account_role: string }) => !excludedRoles.includes(user.account_role)
|
||||
);
|
||||
}
|
||||
} catch (_err) {
|
||||
} catch {
|
||||
toast.error('Failed to load users');
|
||||
} finally {
|
||||
loadingUsers = false;
|
||||
@@ -504,7 +503,7 @@
|
||||
if (response.ok) {
|
||||
services = await response.json();
|
||||
}
|
||||
} catch (_err) {
|
||||
} catch {
|
||||
toast.error('Failed to load services');
|
||||
} finally {
|
||||
loadingServices = false;
|
||||
@@ -528,7 +527,6 @@
|
||||
hoursRangeGeneration++;
|
||||
const gen = hoursRangeGeneration;
|
||||
|
||||
_loadingWorkingHours = true;
|
||||
loadingAvailableHours = true;
|
||||
|
||||
try {
|
||||
@@ -572,10 +570,9 @@
|
||||
workingHours = { ...workingHours, ...whMap };
|
||||
availableHours = { ...availableHours, ...ahMap };
|
||||
}
|
||||
} catch (_err) {
|
||||
} catch {
|
||||
toast.error('Failed to load availability');
|
||||
} finally {
|
||||
_loadingWorkingHours = false;
|
||||
loadingAvailableHours = false;
|
||||
}
|
||||
}
|
||||
@@ -597,7 +594,6 @@
|
||||
hoursMonthGeneration++;
|
||||
const gen = hoursMonthGeneration;
|
||||
|
||||
_loadingWorkingHours = true;
|
||||
loadingAvailableHours = true;
|
||||
|
||||
try {
|
||||
@@ -636,10 +632,9 @@
|
||||
workingHours = { ...workingHours, ...whMap };
|
||||
availableHours = { ...availableHours, ...ahMap };
|
||||
}
|
||||
} catch (_err) {
|
||||
} catch {
|
||||
toast.error('Failed to load availability');
|
||||
} finally {
|
||||
_loadingWorkingHours = false;
|
||||
loadingAvailableHours = false;
|
||||
loadingMonthKeys = new SvelteSet([...loadingMonthKeys].filter((k) => k !== monthKey));
|
||||
}
|
||||
@@ -725,7 +720,7 @@
|
||||
toast.error(`Failed to reserve slot: ${errorText}`);
|
||||
return false;
|
||||
}
|
||||
} catch (_err) {
|
||||
} catch {
|
||||
toast.error('Failed to reserve slot');
|
||||
return false;
|
||||
} finally {
|
||||
@@ -1020,7 +1015,7 @@
|
||||
const errorText = await res.text();
|
||||
toast.error(`Failed to create booking: ${errorText}`);
|
||||
}
|
||||
} catch (_err) {
|
||||
} catch {
|
||||
toast.error('An error occurred while creating booking');
|
||||
} finally {
|
||||
submitting = false;
|
||||
@@ -1148,8 +1143,10 @@
|
||||
<div class="max-h-[300px] overflow-y-auto rounded-md border border-gray-200">
|
||||
{#if loadingUsers}
|
||||
<div class="space-y-2 p-2">
|
||||
{#each Array(3) as _, i (i)}
|
||||
{#each Array(3) as __, i (i)}
|
||||
{__}
|
||||
<Skeleton class="h-10 w-full" />
|
||||
{__}
|
||||
{/each}
|
||||
</div>
|
||||
{:else if users.length === 0}
|
||||
@@ -1260,8 +1257,10 @@
|
||||
<Card.Content class="space-y-4">
|
||||
{#if loadingServices}
|
||||
<div class="grid grid-cols-1 gap-4 md:grid-cols-2">
|
||||
{#each Array(4) as _, i (i)}
|
||||
{#each Array(4) as __, i (i)}
|
||||
{__}
|
||||
<Skeleton class="h-28 w-full" />
|
||||
{__}
|
||||
{/each}
|
||||
</div>
|
||||
{:else if services.length === 0}
|
||||
@@ -1291,10 +1290,12 @@
|
||||
/>
|
||||
<Button variant="outline" size="sm" onclick={fetchCustomServices}>Search</Button>
|
||||
</div>
|
||||
{#if loadingCustomServices}
|
||||
{#if loadingCustomServices}
|
||||
<div class="space-y-2">
|
||||
{#each Array(3) as _, i (i)}
|
||||
{#each Array(3) as __, i (i)}
|
||||
{__}
|
||||
<Skeleton class="h-10 w-full" />
|
||||
{__}
|
||||
{/each}
|
||||
</div>
|
||||
{:else if customServices.length > 0}
|
||||
|
||||
@@ -86,7 +86,7 @@
|
||||
const text = await response.text();
|
||||
toast.error('Failed to cancel: ' + text);
|
||||
}
|
||||
} catch (_err) {
|
||||
} catch {
|
||||
toast.error('Network error');
|
||||
} finally {
|
||||
cancelling = false;
|
||||
|
||||
@@ -286,7 +286,8 @@
|
||||
<Card.Content>
|
||||
{#if loading}
|
||||
<div class="space-y-4">
|
||||
{#each Array(3) as _, i (i)}
|
||||
{#each Array(3) as __, i (i)}
|
||||
{__}
|
||||
<div class="space-y-2">
|
||||
<Skeleton class="h-4 w-32" />
|
||||
<Skeleton class="h-5 w-full" />
|
||||
|
||||
@@ -283,7 +283,8 @@
|
||||
|
||||
{#if loading}
|
||||
<div class="space-y-3">
|
||||
{#each Array(3) as _, i (i)}
|
||||
{#each Array(3) as __, i (i)}
|
||||
{__}
|
||||
<Skeleton class="h-12 w-full" />
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
@@ -275,7 +275,8 @@
|
||||
<Card.Content>
|
||||
{#if loading}
|
||||
<div class="space-y-3">
|
||||
{#each Array(3) as _, i (i)}
|
||||
{#each Array(3) as __, i (i)}
|
||||
{__}
|
||||
<Skeleton class="h-16 w-full" />
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
@@ -49,7 +49,6 @@
|
||||
let cardQuery = $state('');
|
||||
let cards = $state<GiftCard[]>([]);
|
||||
let balances = $state<UserBalance[]>([]);
|
||||
let _totalCards = $state(0);
|
||||
let totalBalanceRecords = $state(0);
|
||||
let currentPage = $state(1);
|
||||
let totalPages = $state(1);
|
||||
@@ -138,18 +137,12 @@
|
||||
// Page 3: Recipient email input
|
||||
let generateEmail = $state('');
|
||||
|
||||
let _tillGuestEmail = $state<string | undefined>(undefined);
|
||||
|
||||
// Payment Processing States
|
||||
let cashAmount = $state('');
|
||||
let _cashTendered = $state(0);
|
||||
let _extraAsTip = $state(false);
|
||||
|
||||
let ephemeralCardNumber = $state('');
|
||||
let ephemeralCardExpiry = $state('');
|
||||
let ephemeralCardCVC = $state('');
|
||||
let _ephemeralCardError = $state('');
|
||||
|
||||
let paymentError = $state('');
|
||||
let paymentResult = $state<{
|
||||
id: string;
|
||||
@@ -158,7 +151,6 @@
|
||||
status?: string;
|
||||
} | null>(null);
|
||||
let cardMachineItemID = $state<string | null>(null);
|
||||
let _checkoutId = $state<string | null>(null);
|
||||
let processingMessage = $state('Processing payment...');
|
||||
|
||||
// Idempotency Key
|
||||
@@ -242,14 +234,6 @@
|
||||
transferAmount && !transferAmountError && transferToCode && !transferCodeError
|
||||
);
|
||||
|
||||
// TillPayment state
|
||||
const _showTillPayment = $state(false);
|
||||
const _tillAmount = $state(0);
|
||||
const _tillAction = $state<'create' | 'topup'>('create');
|
||||
const _tillGiftCardId = $state<string | undefined>(undefined);
|
||||
const _tillUserId = $state<string | undefined>(undefined);
|
||||
const _tillDelivery = $state<'account' | 'code'>('code');
|
||||
|
||||
async function fetchGiftCards(page: number = 1, search: string = '') {
|
||||
loading = true;
|
||||
loadingSearch = true;
|
||||
@@ -267,14 +251,13 @@
|
||||
summary.total_user_balances = data.total_user_balances;
|
||||
cards = data.gift_cards;
|
||||
balances = data.user_balances;
|
||||
_totalCards = data.total;
|
||||
totalBalanceRecords = data.ub_total ?? data.user_balances?.length ?? 0;
|
||||
totalBalanceRecords = data.ub_total ?? data.user_balances?.length ?? 0;
|
||||
currentPage = data.page;
|
||||
totalPages = data.totalPages;
|
||||
} else {
|
||||
toast.error('Failed to fetch gift cards');
|
||||
}
|
||||
} catch (_err) {
|
||||
} catch {
|
||||
toast.error('Network error fetching gift cards');
|
||||
} finally {
|
||||
loading = false;
|
||||
@@ -297,7 +280,7 @@
|
||||
} else {
|
||||
toast.error('Failed to fetch expired balances');
|
||||
}
|
||||
} catch (_err) {
|
||||
} catch {
|
||||
toast.error('Network error fetching expired balances');
|
||||
} finally {
|
||||
loadingExpired = false;
|
||||
@@ -321,7 +304,7 @@
|
||||
const err = await res.json();
|
||||
toast.error(err.error || 'Failed to claim balance');
|
||||
}
|
||||
} catch (_err) {
|
||||
} catch {
|
||||
toast.error('Network error claiming balance');
|
||||
} finally {
|
||||
claimingId = null;
|
||||
@@ -415,7 +398,7 @@
|
||||
const errText = await res.text();
|
||||
toast.error(errText || 'Failed to create inventory card');
|
||||
}
|
||||
} catch (_err) {
|
||||
} catch {
|
||||
toast.error('Network error');
|
||||
} finally {
|
||||
creating = false;
|
||||
@@ -457,7 +440,7 @@
|
||||
const errText = await res.text();
|
||||
toast.error(errText || 'Failed to transfer balance');
|
||||
}
|
||||
} catch (_err) {
|
||||
} catch {
|
||||
toast.error('Network error transferring balance');
|
||||
} finally {
|
||||
transferring = false;
|
||||
@@ -475,7 +458,6 @@
|
||||
generateEmail = '';
|
||||
generateUserQuery = '';
|
||||
generateUsers = [];
|
||||
_tillGuestEmail = undefined;
|
||||
}
|
||||
|
||||
function resetTopUpModal() {
|
||||
@@ -485,16 +467,12 @@
|
||||
|
||||
// Reset payment
|
||||
cashAmount = '';
|
||||
_cashTendered = 0;
|
||||
_extraAsTip = false;
|
||||
ephemeralCardNumber = '';
|
||||
ephemeralCardExpiry = '';
|
||||
ephemeralCardCVC = '';
|
||||
_ephemeralCardError = '';
|
||||
paymentError = '';
|
||||
paymentResult = null;
|
||||
cardMachineItemID = null;
|
||||
_checkoutId = null;
|
||||
idempotencyKey = '';
|
||||
}
|
||||
|
||||
@@ -607,7 +585,6 @@
|
||||
const data = await res.json();
|
||||
cardMachineItemID = data.item_id || null;
|
||||
if (data.status === 'pending' && data.checkout_id) {
|
||||
_checkoutId = data.checkout_id;
|
||||
setModalStep(actionType, 'processing');
|
||||
pollEmbeddedCheckout(data.checkout_id, amt, actionType);
|
||||
} else {
|
||||
@@ -1276,14 +1253,12 @@
|
||||
</thead>
|
||||
<tbody>
|
||||
{#if loading}
|
||||
{#each Array(3) as _, i (i)}
|
||||
<tr class="border-b">
|
||||
<td class="py-3"><Skeleton class="h-4 w-36" /></td>
|
||||
<td class="py-3"><Skeleton class="h-4 w-44" /></td>
|
||||
<td class="py-3"><Skeleton class="h-4 w-16" /></td>
|
||||
<td class="py-3"><Skeleton class="h-4 w-24" /></td>
|
||||
</tr>
|
||||
{/each}
|
||||
{:else if balances.length === 0}
|
||||
<tr>
|
||||
<td colspan="4" class="py-8 text-center text-gray-500">
|
||||
@@ -1309,7 +1284,8 @@
|
||||
<!-- Mobile view - Balances -->
|
||||
<div class="grid gap-4 md:hidden">
|
||||
{#if loading}
|
||||
{#each Array(2) as _, i (i)}
|
||||
{#each Array(2) as __, i (i)}
|
||||
{__}
|
||||
<div class="space-y-3 rounded-lg border p-4">
|
||||
<Skeleton class="h-4 w-36" />
|
||||
<Skeleton class="h-4 w-full" />
|
||||
@@ -1361,7 +1337,6 @@
|
||||
</thead>
|
||||
<tbody>
|
||||
{#if loadingExpired}
|
||||
{#each Array(3) as _, i (i)}
|
||||
<tr class="border-b">
|
||||
<td class="py-3"><Skeleton class="h-4 w-32" /></td>
|
||||
<td class="py-3"><Skeleton class="h-4 w-16" /></td>
|
||||
@@ -1369,7 +1344,21 @@
|
||||
<td class="py-3"><Skeleton class="h-4 w-20" /></td>
|
||||
<td class="py-3 text-center"><Skeleton class="mx-auto h-4 w-24" /></td>
|
||||
</tr>
|
||||
{/each}
|
||||
<tr class="border-b">
|
||||
<td class="py-3"><Skeleton class="h-4 w-32" /></td>
|
||||
<td class="py-3"><Skeleton class="h-4 w-16" /></td>
|
||||
<td class="py-3"><Skeleton class="h-4 w-24" /></td>
|
||||
<td class="py-3"><Skeleton class="h-4 w-20" /></td>
|
||||
<td class="py-3 text-center"><Skeleton class="mx-auto h-4 w-24" /></td>
|
||||
</tr>
|
||||
<tr class="border-b">
|
||||
<td class="py-3"><Skeleton class="h-4 w-32" /></td>
|
||||
<td class="py-3"><Skeleton class="h-4 w-16" /></td>
|
||||
<td class="py-3"><Skeleton class="h-4 w-24" /></td>
|
||||
<td class="py-3"><Skeleton class="h-4 w-20" /></td>
|
||||
<td class="py-3 text-center"><Skeleton class="mx-auto h-4 w-24" /></td>
|
||||
</tr>
|
||||
|
||||
{:else if expiredBalances.length === 0}
|
||||
<tr>
|
||||
<td colspan="5" class="py-8 text-center text-gray-500">
|
||||
@@ -1424,7 +1413,8 @@
|
||||
<!-- Mobile view - Expired Balances -->
|
||||
<div class="grid gap-4 md:hidden">
|
||||
{#if loadingExpired}
|
||||
{#each Array(2) as _, i (i)}
|
||||
{#each Array(2) as __, i (i)}
|
||||
{__}
|
||||
<div class="space-y-3 rounded-lg border p-4">
|
||||
<Skeleton class="h-4 w-36" />
|
||||
<Skeleton class="h-4 w-full" />
|
||||
|
||||
@@ -342,7 +342,8 @@
|
||||
<Card.Content class="space-y-4">
|
||||
{#if exceptionGroupsLoading}
|
||||
<div class="grid grid-cols-1 gap-4 md:grid-cols-2">
|
||||
{#each Array(2) as _, i (i)}
|
||||
{#each Array(2) as __, i (i)}
|
||||
{__}
|
||||
<Skeleton class="h-32 w-full" />
|
||||
{/each}
|
||||
</div>
|
||||
@@ -567,7 +568,7 @@
|
||||
disabled={!row.is_open}
|
||||
class="flex h-9 w-24 rounded-md border border-input bg-background px-2 py-1 text-sm shadow-xs ring-offset-background transition-[color,box-shadow] outline-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50"
|
||||
>
|
||||
{#each Array(24) as _, hour (hour)}
|
||||
{#each Array.from({ length: 24 }, (_, hour) => hour) as hour (hour)}
|
||||
{#each TIME15 as min (min)}
|
||||
<option value="{String(hour).padStart(2, '0')}:{min}"
|
||||
>{String(hour).padStart(2, '0')}:{min}</option
|
||||
@@ -582,7 +583,7 @@
|
||||
disabled={!row.is_open}
|
||||
class="flex h-9 w-24 rounded-md border border-input bg-background px-2 py-1 text-sm shadow-xs ring-offset-background transition-[color,box-shadow] outline-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50"
|
||||
>
|
||||
{#each Array(24) as _, hour (hour)}
|
||||
{#each Array.from({ length: 24 }, (_, hour) => hour) as hour (hour)}
|
||||
{#each TIME15 as min (min)}
|
||||
<option value="{String(hour).padStart(2, '0')}:{min}"
|
||||
>{String(hour).padStart(2, '0')}:{min}</option
|
||||
|
||||
@@ -361,7 +361,6 @@
|
||||
}
|
||||
|
||||
let availableTags = $state<string[]>([]);
|
||||
let _loadingTags = $state(true);
|
||||
let isMobile = $state(false);
|
||||
|
||||
async function fetchTags() {
|
||||
@@ -373,8 +372,6 @@
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Failed to fetch tags:', e);
|
||||
} finally {
|
||||
_loadingTags = false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -297,7 +297,6 @@
|
||||
</thead>
|
||||
<tbody>
|
||||
{#if loading}
|
||||
{#each Array(2) as _, i (i)}
|
||||
<tr class="border-b">
|
||||
<td class="py-3"><Skeleton class="h-4 w-32" /></td>
|
||||
<td class="py-3"><Skeleton class="h-4 w-48" /></td>
|
||||
@@ -310,7 +309,19 @@
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{/each}
|
||||
<tr class="border-b">
|
||||
<td class="py-3"><Skeleton class="h-4 w-32" /></td>
|
||||
<td class="py-3"><Skeleton class="h-4 w-48" /></td>
|
||||
<td class="py-3"><Skeleton class="mx-auto h-4 w-12" /></td>
|
||||
<td class="py-3"><Skeleton class="mx-auto h-4 w-12" /></td>
|
||||
<td class="py-3 text-center">
|
||||
<div class="flex justify-center gap-2">
|
||||
<Skeleton class="h-8 w-16" />
|
||||
<Skeleton class="h-8 w-16" />
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
{:else}
|
||||
{#each patchTests as pt (pt.id)}
|
||||
<tr class="border-b hover:bg-gray-50">
|
||||
@@ -346,7 +357,8 @@
|
||||
|
||||
<div class="space-y-4 md:hidden">
|
||||
{#if loading}
|
||||
{#each Array(2) as _, i (i)}
|
||||
{#each Array(2) as __, i (i)}
|
||||
{__}
|
||||
<div class="space-y-3 rounded-lg border p-4">
|
||||
<Skeleton class="h-5 w-32" />
|
||||
<Skeleton class="h-4 w-48" />
|
||||
|
||||
@@ -366,7 +366,6 @@
|
||||
</thead>
|
||||
<tbody>
|
||||
{#if servicesLoading}
|
||||
{#each Array(3) as _, i (i)}
|
||||
<tr class="border-b">
|
||||
<td class="py-3"><Skeleton class="h-4 w-32" /></td>
|
||||
<td class="py-3"><Skeleton class="h-4 w-48" /></td>
|
||||
@@ -380,7 +379,33 @@
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{/each}
|
||||
<tr class="border-b">
|
||||
<td class="py-3"><Skeleton class="h-4 w-32" /></td>
|
||||
<td class="py-3"><Skeleton class="h-4 w-48" /></td>
|
||||
<td class="py-3 text-right"><Skeleton class="ml-auto h-4 w-16" /></td>
|
||||
<td class="py-3 text-right"><Skeleton class="ml-auto h-4 w-20" /></td>
|
||||
<td class="py-3 text-center"><Skeleton class="mx-auto h-4 w-16" /></td>
|
||||
<td class="py-3 text-center">
|
||||
<div class="flex justify-center gap-2">
|
||||
<Skeleton class="h-8 w-16" />
|
||||
<Skeleton class="h-8 w-16" />
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="border-b">
|
||||
<td class="py-3"><Skeleton class="h-4 w-32" /></td>
|
||||
<td class="py-3"><Skeleton class="h-4 w-48" /></td>
|
||||
<td class="py-3 text-right"><Skeleton class="ml-auto h-4 w-16" /></td>
|
||||
<td class="py-3 text-right"><Skeleton class="ml-auto h-4 w-20" /></td>
|
||||
<td class="py-3 text-center"><Skeleton class="mx-auto h-4 w-16" /></td>
|
||||
<td class="py-3 text-center">
|
||||
<div class="flex justify-center gap-2">
|
||||
<Skeleton class="h-8 w-16" />
|
||||
<Skeleton class="h-8 w-16" />
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
{:else}
|
||||
{#each services as service (service.id)}
|
||||
<tr class="border-b hover:bg-gray-50">
|
||||
@@ -438,7 +463,8 @@
|
||||
|
||||
<div class="space-y-4 md:hidden">
|
||||
{#if servicesLoading}
|
||||
{#each Array(3) as _, i (i)}
|
||||
{#each Array(3) as __, i (i)}
|
||||
{__}
|
||||
<div class="rounded-lg border p-4">
|
||||
<div class="space-y-3">
|
||||
<Skeleton class="h-5 w-32" />
|
||||
|
||||
@@ -552,7 +552,8 @@
|
||||
<Card.Content class="space-y-4">
|
||||
{#if loading}
|
||||
<div class="space-y-3">
|
||||
{#each Array(3) as _, i (i)}
|
||||
{#each Array(3) as __, i (i)}
|
||||
{__}
|
||||
<Skeleton class="h-16 w-full" />
|
||||
{/each}
|
||||
</div>
|
||||
@@ -665,7 +666,8 @@
|
||||
>
|
||||
Prev
|
||||
</Button>
|
||||
{#each Array(totalPages) as _, i (i)}
|
||||
{#each Array(totalPages) as __, i (i)}
|
||||
{__}
|
||||
<Button
|
||||
variant={currentPage === i + 1 ? 'default' : 'outline'}
|
||||
size="sm"
|
||||
|
||||
@@ -507,7 +507,8 @@
|
||||
Customer Relationship
|
||||
</h3>
|
||||
<div class="space-y-2">
|
||||
{#each Array(5) as _, i (i)}
|
||||
{#each Array(5) as __, i (i)}
|
||||
{__}
|
||||
<div class="h-10 animate-pulse rounded-md bg-gray-200"></div>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
@@ -176,7 +176,8 @@
|
||||
|
||||
<div class="mt-3 max-h-60 space-y-2 overflow-y-auto">
|
||||
{#if initialLoad}
|
||||
{#each Array(3) as _, i (i)}
|
||||
{#each Array(3) as __, i (i)}
|
||||
{__}
|
||||
<div class="rounded bg-gray-50 p-2">
|
||||
<Skeleton class="mb-1 h-4 w-32" />
|
||||
<Skeleton class="h-3 w-48" />
|
||||
|
||||
@@ -31,7 +31,6 @@
|
||||
|
||||
let _reservationId = $state<string | null>(null);
|
||||
let reservationExpiresAt = $state<Date | null>(null);
|
||||
let _reservationCountdown = $state<string>('');
|
||||
let isReserving = $state(false);
|
||||
let reservedDuration = $state(0);
|
||||
let reservedStartTime = $state<string | null>(null);
|
||||
@@ -48,7 +47,7 @@
|
||||
shortestServiceMinutes = Math.min(...durations);
|
||||
}
|
||||
}
|
||||
} catch (_err) {
|
||||
} catch {
|
||||
// Silently handled - services list remains empty
|
||||
}
|
||||
}
|
||||
@@ -82,7 +81,6 @@
|
||||
// Clear local state first so a slow DELETE doesn't block the UI.
|
||||
_reservationId = null;
|
||||
reservationExpiresAt = null;
|
||||
_reservationCountdown = '';
|
||||
reservedDuration = 0;
|
||||
reservedStartTime = null;
|
||||
if (window.__walkInCountdownInterval) {
|
||||
@@ -209,7 +207,7 @@
|
||||
const dayEndTimeVal = minutesToTime(dayEndMinutesVal);
|
||||
|
||||
if (shouldApplyLunchProtection(dayStartTimeVal, dayEndTimeVal)) {
|
||||
const { windowStart, windowEnd: _windowEnd } = calculateMiddleWindow(
|
||||
const { windowStart } = calculateMiddleWindow(
|
||||
dayStartTimeVal,
|
||||
dayEndTimeVal
|
||||
);
|
||||
@@ -330,7 +328,6 @@
|
||||
|
||||
const updateCountdown = () => {
|
||||
if (!reservationExpiresAt) {
|
||||
_reservationCountdown = '';
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -338,7 +335,6 @@
|
||||
const diff = reservationExpiresAt.getTime() - now.getTime();
|
||||
|
||||
if (diff <= 0) {
|
||||
_reservationCountdown = 'Expired';
|
||||
_reservationId = null;
|
||||
reservationExpiresAt = null;
|
||||
toast.error('Slot released — please re-check availability');
|
||||
@@ -348,10 +344,6 @@
|
||||
showCreateModal = false;
|
||||
return;
|
||||
}
|
||||
|
||||
const minutes = Math.floor(diff / 60000);
|
||||
const seconds = Math.floor((diff % 60000) / 1000);
|
||||
_reservationCountdown = `${minutes}:${seconds.toString().padStart(2, '0')}`;
|
||||
};
|
||||
|
||||
updateCountdown();
|
||||
|
||||
@@ -32,7 +32,6 @@
|
||||
availableStartTime?: string;
|
||||
reservationExpiresAt?: Date | null;
|
||||
onBookingCreated?: () => void;
|
||||
_onclose?: () => void;
|
||||
}
|
||||
|
||||
let {
|
||||
@@ -41,7 +40,6 @@
|
||||
availableStartTime,
|
||||
reservationExpiresAt,
|
||||
onBookingCreated,
|
||||
_onclose
|
||||
}: Props = $props();
|
||||
|
||||
// =============== State ===============
|
||||
@@ -313,7 +311,7 @@
|
||||
if (response.ok) {
|
||||
services = await response.json();
|
||||
}
|
||||
} catch (_err) {
|
||||
} catch {
|
||||
toast.error('Failed to load services');
|
||||
} finally {
|
||||
loadingServices = false;
|
||||
@@ -558,7 +556,7 @@
|
||||
const errorText = await res.text();
|
||||
toast.error(`Failed to create booking: ${errorText}`);
|
||||
}
|
||||
} catch (_err) {
|
||||
} catch {
|
||||
toast.error('An error occurred while creating booking');
|
||||
} finally {
|
||||
submitting = false;
|
||||
@@ -723,7 +721,8 @@
|
||||
<div class="max-h-[300px] overflow-y-auto rounded-md border border-gray-200">
|
||||
{#if loadingUsers}
|
||||
<div class="space-y-2 p-2">
|
||||
{#each Array(3) as _, i (i)}
|
||||
{#each Array(3) as __, i (i)}
|
||||
{__}
|
||||
<Skeleton class="h-10 w-full" />
|
||||
{/each}
|
||||
</div>
|
||||
@@ -836,7 +835,8 @@
|
||||
<Card.Content class="space-y-4">
|
||||
{#if loadingServices}
|
||||
<div class="grid grid-cols-1 gap-4 md:grid-cols-2">
|
||||
{#each Array(4) as _, i (i)}
|
||||
{#each Array(4) as __, i (i)}
|
||||
{__}
|
||||
<Skeleton class="h-28 w-full" />
|
||||
{/each}
|
||||
</div>
|
||||
@@ -869,7 +869,8 @@
|
||||
</div>
|
||||
{#if loadingCustomServices}
|
||||
<div class="space-y-2">
|
||||
{#each Array(3) as _, i (i)}
|
||||
{#each Array(3) as __, i (i)}
|
||||
{__}
|
||||
<Skeleton class="h-10 w-full" />
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
@@ -423,7 +423,6 @@
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-gray-200">
|
||||
{#each Array(7) as _, i (i)}
|
||||
<tr>
|
||||
<td class="px-4 py-4">
|
||||
<div class="flex items-center gap-2">
|
||||
@@ -444,14 +443,135 @@
|
||||
<Skeleton class="ml-auto h-4 w-12" />
|
||||
</td>
|
||||
</tr>
|
||||
{/each}
|
||||
<tr>
|
||||
<td class="px-4 py-4">
|
||||
<div class="flex items-center gap-2">
|
||||
<Skeleton class="h-8 w-8 rounded-full" />
|
||||
<Skeleton class="h-4 w-20" />
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-4 py-4 text-center">
|
||||
<Skeleton class="mx-auto h-6 w-16 rounded-full" />
|
||||
</td>
|
||||
<td class="px-4 py-4">
|
||||
<Skeleton class="h-4 w-20" />
|
||||
</td>
|
||||
<td class="px-4 py-4">
|
||||
<Skeleton class="h-4 w-20" />
|
||||
</td>
|
||||
<td class="px-4 py-4 text-right">
|
||||
<Skeleton class="ml-auto h-4 w-12" />
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="px-4 py-4">
|
||||
<div class="flex items-center gap-2">
|
||||
<Skeleton class="h-8 w-8 rounded-full" />
|
||||
<Skeleton class="h-4 w-20" />
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-4 py-4 text-center">
|
||||
<Skeleton class="mx-auto h-6 w-16 rounded-full" />
|
||||
</td>
|
||||
<td class="px-4 py-4">
|
||||
<Skeleton class="h-4 w-20" />
|
||||
</td>
|
||||
<td class="px-4 py-4">
|
||||
<Skeleton class="h-4 w-20" />
|
||||
</td>
|
||||
<td class="px-4 py-4 text-right">
|
||||
<Skeleton class="ml-auto h-4 w-12" />
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="px-4 py-4">
|
||||
<div class="flex items-center gap-2">
|
||||
<Skeleton class="h-8 w-8 rounded-full" />
|
||||
<Skeleton class="h-4 w-20" />
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-4 py-4 text-center">
|
||||
<Skeleton class="mx-auto h-6 w-16 rounded-full" />
|
||||
</td>
|
||||
<td class="px-4 py-4">
|
||||
<Skeleton class="h-4 w-20" />
|
||||
</td>
|
||||
<td class="px-4 py-4">
|
||||
<Skeleton class="h-4 w-20" />
|
||||
</td>
|
||||
<td class="px-4 py-4 text-right">
|
||||
<Skeleton class="ml-auto h-4 w-12" />
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="px-4 py-4">
|
||||
<div class="flex items-center gap-2">
|
||||
<Skeleton class="h-8 w-8 rounded-full" />
|
||||
<Skeleton class="h-4 w-20" />
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-4 py-4 text-center">
|
||||
<Skeleton class="mx-auto h-6 w-16 rounded-full" />
|
||||
</td>
|
||||
<td class="px-4 py-4">
|
||||
<Skeleton class="h-4 w-20" />
|
||||
</td>
|
||||
<td class="px-4 py-4">
|
||||
<Skeleton class="h-4 w-20" />
|
||||
</td>
|
||||
<td class="px-4 py-4 text-right">
|
||||
<Skeleton class="ml-auto h-4 w-12" />
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="px-4 py-4">
|
||||
<div class="flex items-center gap-2">
|
||||
<Skeleton class="h-8 w-8 rounded-full" />
|
||||
<Skeleton class="h-4 w-20" />
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-4 py-4 text-center">
|
||||
<Skeleton class="mx-auto h-6 w-16 rounded-full" />
|
||||
</td>
|
||||
<td class="px-4 py-4">
|
||||
<Skeleton class="h-4 w-20" />
|
||||
</td>
|
||||
<td class="px-4 py-4">
|
||||
<Skeleton class="h-4 w-20" />
|
||||
</td>
|
||||
<td class="px-4 py-4 text-right">
|
||||
<Skeleton class="ml-auto h-4 w-12" />
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="px-4 py-4">
|
||||
<div class="flex items-center gap-2">
|
||||
<Skeleton class="h-8 w-8 rounded-full" />
|
||||
<Skeleton class="h-4 w-20" />
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-4 py-4 text-center">
|
||||
<Skeleton class="mx-auto h-6 w-16 rounded-full" />
|
||||
</td>
|
||||
<td class="px-4 py-4">
|
||||
<Skeleton class="h-4 w-20" />
|
||||
</td>
|
||||
<td class="px-4 py-4">
|
||||
<Skeleton class="h-4 w-20" />
|
||||
</td>
|
||||
<td class="px-4 py-4 text-right">
|
||||
<Skeleton class="ml-auto h-4 w-12" />
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Mobile Skeleton -->
|
||||
<div class="space-y-3 md:hidden">
|
||||
{#each Array(7) as _, i (i)}
|
||||
{#each Array(7) as __, i (i)}
|
||||
{__}
|
||||
<div class="rounded-lg border p-4">
|
||||
<div class="mb-3 flex items-center justify-between">
|
||||
<div class="flex items-center gap-2">
|
||||
@@ -500,34 +620,34 @@
|
||||
<Checkbox bind:checked={row.is_open} />
|
||||
</td>
|
||||
<td class="py-2">
|
||||
<select
|
||||
bind:value={row.start_time}
|
||||
disabled={!row.is_open}
|
||||
class="flex h-9 max-w-[70px] rounded-md border border-input bg-background px-2 py-1 text-sm shadow-xs ring-offset-background transition-[color,box-shadow] outline-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50"
|
||||
>
|
||||
{#each Array(24) as _, hour (hour)}
|
||||
{#each TIME15 as min (min)}
|
||||
<option value="{String(hour).padStart(2, '0')}:{min}"
|
||||
>{String(hour).padStart(2, '0')}:{min}</option
|
||||
>
|
||||
<select
|
||||
bind:value={row.start_time}
|
||||
disabled={!row.is_open}
|
||||
class="flex h-9 max-w-[70px] rounded-md border border-input bg-background px-2 py-1 text-sm shadow-xs ring-offset-background transition-[color,box-shadow] outline-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50"
|
||||
>
|
||||
{#each Array.from({ length: 24 }, (_, hour) => hour) as hour (hour)}
|
||||
{#each TIME15 as min (min)}
|
||||
<option value="{String(hour).padStart(2, '0')}:{min}"
|
||||
>{String(hour).padStart(2, '0')}:{min}</option
|
||||
>
|
||||
{/each}
|
||||
{/each}
|
||||
{/each}
|
||||
</select>
|
||||
</select>
|
||||
</td>
|
||||
<td class="py-2">
|
||||
<select
|
||||
bind:value={row.end_time}
|
||||
disabled={!row.is_open}
|
||||
class="flex h-9 max-w-[70px] rounded-md border border-input bg-background px-2 py-1 text-sm shadow-xs ring-offset-background transition-[color,box-shadow] outline-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50"
|
||||
>
|
||||
{#each Array(24) as _, hour (hour)}
|
||||
{#each TIME15 as min (min)}
|
||||
<option value="{String(hour).padStart(2, '0')}:{min}"
|
||||
>{String(hour).padStart(2, '0')}:{min}</option
|
||||
>
|
||||
<select
|
||||
bind:value={row.end_time}
|
||||
disabled={!row.is_open}
|
||||
class="flex h-9 max-w-[70px] rounded-md border border-input bg-background px-2 py-1 text-sm shadow-xs ring-offset-background transition-[color,box-shadow] outline-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50"
|
||||
>
|
||||
{#each Array.from({ length: 24 }, (_, hour) => hour) as hour (hour)}
|
||||
{#each TIME15 as min (min)}
|
||||
<option value="{String(hour).padStart(2, '0')}:{min}"
|
||||
>{String(hour).padStart(2, '0')}:{min}</option
|
||||
>
|
||||
{/each}
|
||||
{/each}
|
||||
{/each}
|
||||
</select>
|
||||
</select>
|
||||
</td>
|
||||
</tr>
|
||||
{/each}
|
||||
|
||||
@@ -76,13 +76,11 @@
|
||||
phone: '',
|
||||
specialRequests: ''
|
||||
});
|
||||
let _isSubmitting = $state(false);
|
||||
let idempotencyKey = $state<string>('');
|
||||
|
||||
// =============== Payment State ===============
|
||||
let userDepositsRequired = $state<number>(0);
|
||||
let hasActiveBooking = $state<boolean>(false);
|
||||
let _activeBookingCheckDone = $state<boolean>(false);
|
||||
let paymentMethods = $state<
|
||||
Array<{ id: string; brand: string; last4: string; expiry_month: number; expiry_year: number }>
|
||||
>([]);
|
||||
@@ -100,11 +98,8 @@
|
||||
let newCardNumber = $state('');
|
||||
let newCardExpiry = $state('');
|
||||
let newCardCVC = $state('');
|
||||
const _saveCardForFuture = $state(false);
|
||||
|
||||
// Payment flow state
|
||||
let depositPaid = $state(false);
|
||||
let _showPaymentForm = $state(false);
|
||||
|
||||
const depositCardFormValid = $derived(
|
||||
selectedPaymentMethod !== null ||
|
||||
@@ -213,7 +208,7 @@
|
||||
const user = await response.json();
|
||||
userDepositsRequired = user.deposits_required ?? 0;
|
||||
}
|
||||
} catch (_err) {
|
||||
} catch {
|
||||
userDepositsRequired = 0;
|
||||
}
|
||||
}
|
||||
@@ -221,20 +216,18 @@
|
||||
async function fetchActiveBookingStatus() {
|
||||
if (!authStore.isAuthenticated) {
|
||||
hasActiveBooking = false;
|
||||
_activeBookingCheckDone = true;
|
||||
return;
|
||||
}
|
||||
|
||||
_activeBookingCheckDone = false;
|
||||
|
||||
try {
|
||||
// Check for pending bookings
|
||||
const pendingResp = await apiFetch('/api/bookings?status=pending&perPage=1');
|
||||
if (pendingResp.ok) {
|
||||
const data = await pendingResp.json();
|
||||
if (data.bookings && data.bookings.length > 0) {
|
||||
hasActiveBooking = true;
|
||||
_activeBookingCheckDone = true;
|
||||
return;
|
||||
hasActiveBooking = true;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -244,10 +237,8 @@
|
||||
const data = await confirmedResp.json();
|
||||
hasActiveBooking = data.bookings && data.bookings.length > 0;
|
||||
}
|
||||
} catch (_err) {
|
||||
} catch {
|
||||
hasActiveBooking = false;
|
||||
} finally {
|
||||
_activeBookingCheckDone = true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -338,7 +329,7 @@
|
||||
const text = await response.text();
|
||||
toast.warning(text || 'Payment failed — you can pay again from your booking details.');
|
||||
}
|
||||
} catch (_err) {
|
||||
} catch {
|
||||
toast.error(
|
||||
'An error occurred. Your booking may still be confirmed — check your appointments.'
|
||||
);
|
||||
@@ -367,15 +358,12 @@
|
||||
let reservationExpiresAt = $state<Date | null>(null);
|
||||
let reservationCountdown = $state<string>('');
|
||||
let reservationExpired = $state(false);
|
||||
let _isReserving = $state(false);
|
||||
|
||||
// Track the reserved slot so we can detect changes on back-navigate in nextStep()
|
||||
let _reservedSlotTime: string | null = $state(null);
|
||||
let _reservedSlotDate: string | null = $state(null);
|
||||
|
||||
// =============== Slot Reservation Functions ===============
|
||||
async function reserveSlot() {
|
||||
_isReserving = true;
|
||||
try {
|
||||
if (!selectedDate || !selectedTime) {
|
||||
toast.error('Please select a date and time');
|
||||
@@ -398,7 +386,6 @@
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const _errorText = await response.text();
|
||||
if (response.status === 429) {
|
||||
toast.error('Too many active reservations. Please wait or log in.');
|
||||
} else if (response.status === 409) {
|
||||
@@ -418,11 +405,9 @@
|
||||
_reservedSlotDate = selectedDate ? selectedDate.toString() : null;
|
||||
startCountdown();
|
||||
return true;
|
||||
} catch (_error) {
|
||||
} catch {
|
||||
toast.error('Network error while reserving slot.');
|
||||
return false;
|
||||
} finally {
|
||||
_isReserving = false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -542,7 +527,7 @@
|
||||
} else {
|
||||
toast.error('Failed to load services');
|
||||
}
|
||||
} catch (_err) {
|
||||
} catch {
|
||||
toast.error('Network error loading services');
|
||||
} finally {
|
||||
servicesLoading = false;
|
||||
@@ -752,7 +737,7 @@
|
||||
// MERGE instead of replace — preserves data from previously loaded months
|
||||
workingHours = { ...workingHours, ...whMap };
|
||||
availableHours = { ...availableHours, ...ahMap };
|
||||
} catch (_error) {
|
||||
} catch {
|
||||
// Clean up loadingMonths for the range
|
||||
for (let i = 0; i < months; i++) {
|
||||
let mYear = startDate.year;
|
||||
@@ -852,7 +837,7 @@
|
||||
|
||||
availableHoursCache[monthKey] = availableHoursMap;
|
||||
availableHours = { ...availableHours, ...availableHoursMap };
|
||||
} catch (_error) {
|
||||
} catch {
|
||||
if (!selectedDate) {
|
||||
selectedDate = minDate;
|
||||
}
|
||||
@@ -1393,7 +1378,6 @@
|
||||
}
|
||||
|
||||
async function submitAndProceed() {
|
||||
_isSubmitting = true;
|
||||
try {
|
||||
// Always generate a fresh idempotency key per submission attempt. The
|
||||
// previous behavior kept the key across the component lifetime, which
|
||||
@@ -1404,7 +1388,6 @@
|
||||
|
||||
if (!selectedDate || !selectedTime) {
|
||||
toast.error('Please select a date and time');
|
||||
_isSubmitting = false;
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1429,13 +1412,11 @@
|
||||
});
|
||||
|
||||
if (!guestResponse.ok) {
|
||||
const _errorText = await guestResponse.text();
|
||||
if (guestResponse.status === 409) {
|
||||
toast.error('Email already registered — please log in to book.');
|
||||
} else {
|
||||
toast.error('Failed to create guest account. Please try again.');
|
||||
}
|
||||
_isSubmitting = false;
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1528,10 +1509,8 @@
|
||||
toast.error('Failed to submit booking: ' + errorMessage);
|
||||
}
|
||||
}
|
||||
} catch (_error) {
|
||||
} catch {
|
||||
toast.error('Network error. Please check your connection and try again.');
|
||||
} finally {
|
||||
_isSubmitting = false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1585,7 +1564,6 @@
|
||||
)) &&
|
||||
!reservationExpired
|
||||
);
|
||||
const _canProceedStep4 = $derived(true);
|
||||
</script>
|
||||
|
||||
<div class="mx-auto max-w-4xl p-6">
|
||||
|
||||
@@ -62,7 +62,6 @@
|
||||
);
|
||||
|
||||
let customerBalance = $state(0);
|
||||
let _loadingCustomerBalance = $state(false);
|
||||
let giftCardPaymentAmount = $state('');
|
||||
let savedCardList = $state<
|
||||
Array<{
|
||||
@@ -73,13 +72,10 @@
|
||||
cardholder_name?: string;
|
||||
}>
|
||||
>([]);
|
||||
let _loadingSavedCardList = $state(false);
|
||||
|
||||
async function fetchCustomerGiftCardBalance() {
|
||||
const targetUserId = booking.user_id ?? booking.user?.id;
|
||||
if (!targetUserId) return;
|
||||
_loadingCustomerBalance = true;
|
||||
try {
|
||||
try {
|
||||
const res = await apiFetch(`/api/admin/users/${targetUserId}/giftcard-balance`);
|
||||
if (res.ok) {
|
||||
const data = await res.json();
|
||||
@@ -91,15 +87,12 @@
|
||||
}
|
||||
} catch {
|
||||
// ignore
|
||||
} finally {
|
||||
_loadingCustomerBalance = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchSavedCardList() {
|
||||
const targetUserId = booking.user_id ?? booking.user?.id;
|
||||
if (!targetUserId) return;
|
||||
_loadingSavedCardList = true;
|
||||
try {
|
||||
const res = await apiFetch(`/api/admin/users/${targetUserId}/payment-methods`);
|
||||
if (res.ok) {
|
||||
@@ -107,8 +100,6 @@
|
||||
}
|
||||
} catch {
|
||||
// ignore
|
||||
} finally {
|
||||
_loadingSavedCardList = false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -330,7 +321,7 @@
|
||||
error = data.error_message || 'Payment failed';
|
||||
toast.error(error as string);
|
||||
}
|
||||
} catch (_err) {
|
||||
} catch {
|
||||
stopPolling();
|
||||
status = 'error';
|
||||
error = 'Failed to check payment status';
|
||||
|
||||
@@ -61,76 +61,7 @@
|
||||
let newCardCVC = $state('');
|
||||
let saveCardForFuture = $state(false);
|
||||
|
||||
function formatAndPreserveCursor(
|
||||
input: HTMLInputElement,
|
||||
formatter: (val: string) => string,
|
||||
charRegex: RegExp = /\d/
|
||||
): string {
|
||||
const rawValue = input.value;
|
||||
const oldSelectionStart = input.selectionStart || 0;
|
||||
|
||||
let charsBeforeCursor = 0;
|
||||
for (let i = 0; i < oldSelectionStart; i++) {
|
||||
if (charRegex.test(rawValue[i])) {
|
||||
charsBeforeCursor++;
|
||||
}
|
||||
}
|
||||
|
||||
const formatted = formatter(rawValue);
|
||||
input.value = formatted;
|
||||
|
||||
let newSelectionStart = 0;
|
||||
let charsFound = 0;
|
||||
for (let i = 0; i < formatted.length; i++) {
|
||||
if (charsFound === charsBeforeCursor) {
|
||||
break;
|
||||
}
|
||||
if (charRegex.test(formatted[i])) {
|
||||
charsFound++;
|
||||
}
|
||||
newSelectionStart++;
|
||||
}
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
input.setSelectionRange(newSelectionStart, newSelectionStart);
|
||||
});
|
||||
|
||||
return formatted;
|
||||
}
|
||||
|
||||
function formatCardNumber(value: string): string {
|
||||
const digits = value.replace(/\D/g, '').substring(0, 16);
|
||||
const groups = digits.match(/.{1,4}/g);
|
||||
return groups ? groups.join(' ') : digits;
|
||||
}
|
||||
|
||||
function _handleCardNumberInput(e: Event) {
|
||||
const input = e.target as HTMLInputElement;
|
||||
const formatted = formatAndPreserveCursor(input, formatCardNumber);
|
||||
newCardNumber = formatted;
|
||||
}
|
||||
|
||||
function formatExpiryDate(value: string): string {
|
||||
const digits = value.replace(/\D/g, '').substring(0, 4);
|
||||
if (digits.length >= 3) {
|
||||
return digits.substring(0, 2) + '/' + digits.substring(2);
|
||||
}
|
||||
return digits;
|
||||
}
|
||||
|
||||
function _handleExpiryInput(e: Event) {
|
||||
const input = e.target as HTMLInputElement;
|
||||
const formatted = formatAndPreserveCursor(input, formatExpiryDate);
|
||||
newCardExpiry = formatted;
|
||||
}
|
||||
|
||||
function _handleCvcInput(e: Event) {
|
||||
const input = e.target as HTMLInputElement;
|
||||
const formatted = formatAndPreserveCursor(input, (val) =>
|
||||
val.replace(/\D/g, '').substring(0, 4)
|
||||
);
|
||||
newCardCVC = formatted;
|
||||
}
|
||||
|
||||
function parseExpiryParts(value: string): { month: number; year: number } | null {
|
||||
if (!/^\d{2}\/\d{2}$/.test(value)) return null;
|
||||
@@ -310,16 +241,6 @@
|
||||
(booking.status === 'pending_release' && (lockTimer <= 0 || !lockAcquired))
|
||||
);
|
||||
|
||||
const _payButtonError = $derived(
|
||||
status === 'processing'
|
||||
? null
|
||||
: !cardSelected
|
||||
? cardValidationError
|
||||
: paymentType === 'partial'
|
||||
? partialValidationError
|
||||
: null
|
||||
);
|
||||
|
||||
function campaignDiscountCents(): number {
|
||||
return discountPreview?.eligible
|
||||
? discountPreview.discounts.reduce((sum, d) => sum + Math.round(d.amount * 100), 0)
|
||||
|
||||
@@ -13,12 +13,11 @@
|
||||
import type { Booking as BookingType } from '$lib/types/booking';
|
||||
|
||||
interface Props {
|
||||
_openBookingModal: (bookingId: string) => void;
|
||||
openEditBookingModal: (bookingId: string, nextAppointmentStart?: string | null) => void;
|
||||
openUserModal: (userId: string) => void;
|
||||
}
|
||||
|
||||
const { _openBookingModal, openEditBookingModal, openUserModal }: Props = $props();
|
||||
const { openEditBookingModal, openUserModal }: Props = $props();
|
||||
|
||||
type Booking = {
|
||||
id: string;
|
||||
@@ -270,7 +269,7 @@
|
||||
showPaymentModal = true;
|
||||
}
|
||||
|
||||
function handlePaymentComplete(_payment: unknown) {
|
||||
function handlePaymentComplete() {
|
||||
toast.success('Payment completed');
|
||||
showPaymentModal = false;
|
||||
}
|
||||
|
||||
@@ -312,7 +312,8 @@
|
||||
<Card.Content>
|
||||
{#if loading}
|
||||
<div class="space-y-3">
|
||||
{#each Array(3) as _, i (i)}
|
||||
{#each Array(3) as __, i (i)}
|
||||
{__}
|
||||
<div class="rounded-lg border p-3">
|
||||
<div class="flex items-start justify-between">
|
||||
<div class="flex-1 space-y-2">
|
||||
|
||||
@@ -687,7 +687,8 @@
|
||||
<Card.Content>
|
||||
{#if loading}
|
||||
<div class="space-y-3">
|
||||
{#each Array(5) as _, i (i)}
|
||||
{#each Array(5) as __, i (i)}
|
||||
{__}
|
||||
<div class="flex items-center gap-3 rounded-lg border p-3 sm:gap-4">
|
||||
<Skeleton class="h-4 w-[45px] sm:w-[60px] lg:w-[80px]" />
|
||||
<Skeleton class="h-10 w-1 shrink-0" />
|
||||
|
||||
@@ -210,7 +210,8 @@
|
||||
<Card.Content>
|
||||
{#if loading && !stats}
|
||||
<div class="space-y-3">
|
||||
{#each Array(6) as _, i (i)}
|
||||
{#each Array(6) as __, i (i)}
|
||||
{__}
|
||||
<Skeleton class="h-5 w-full" />
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
value?: string;
|
||||
error?: string;
|
||||
placeholder?: string;
|
||||
_required?: boolean;
|
||||
disabled?: boolean;
|
||||
class?: string;
|
||||
id?: string;
|
||||
@@ -18,7 +17,6 @@
|
||||
value = $bindable(''),
|
||||
error = $bindable(''),
|
||||
placeholder = '07123 456789 or +44 7123 456789',
|
||||
_required = false,
|
||||
disabled = false,
|
||||
class: className = '',
|
||||
id = 'phone',
|
||||
@@ -29,7 +27,6 @@
|
||||
let touched = $state(false);
|
||||
|
||||
function handleKeyDown(e: KeyboardEvent) {
|
||||
const _target = e.target as HTMLInputElement;
|
||||
const allowedKeys = [
|
||||
'Backspace',
|
||||
'Delete',
|
||||
|
||||
Reference in New Issue
Block a user