fix: remove unused variables from Svelte components

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