feat(frontend): add new UI components and utilities
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
@@ -0,0 +1,87 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { Input } from '$lib/components/ui/input';
|
||||||
|
import { Label } from '$lib/components/ui/label';
|
||||||
|
import { Checkbox } from '$lib/components/ui/checkbox';
|
||||||
|
|
||||||
|
let {
|
||||||
|
cardNumber = $bindable(''),
|
||||||
|
cardExpiry = $bindable(''),
|
||||||
|
cardCVC = $bindable(''),
|
||||||
|
saveCard = $bindable(false),
|
||||||
|
showSaveCard = false,
|
||||||
|
disabled = false
|
||||||
|
}: {
|
||||||
|
cardNumber?: string;
|
||||||
|
cardExpiry?: string;
|
||||||
|
cardCVC?: string;
|
||||||
|
saveCard?: boolean;
|
||||||
|
showSaveCard?: boolean;
|
||||||
|
disabled?: boolean;
|
||||||
|
} = $props();
|
||||||
|
|
||||||
|
function formatNumber(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 formatExpiry(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;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="rounded-lg border border-gray-100 bg-gray-50 p-4">
|
||||||
|
<h4 class="mb-4 text-sm font-medium text-gray-700">Card Details</h4>
|
||||||
|
<div class="space-y-4">
|
||||||
|
<div class="space-y-2">
|
||||||
|
<Label for="cardNumber">Card Number</Label>
|
||||||
|
<Input
|
||||||
|
id="cardNumber"
|
||||||
|
type="text"
|
||||||
|
inputmode="numeric"
|
||||||
|
value={cardNumber}
|
||||||
|
oninput={(e) => (cardNumber = formatNumber((e.target as HTMLInputElement).value))}
|
||||||
|
placeholder="1234 5678 9012 3456"
|
||||||
|
maxlength={19}
|
||||||
|
{disabled}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="grid grid-cols-2 gap-4">
|
||||||
|
<div class="space-y-2">
|
||||||
|
<Label for="cardExpiry">Expiry (MM/YY)</Label>
|
||||||
|
<Input
|
||||||
|
id="cardExpiry"
|
||||||
|
type="text"
|
||||||
|
inputmode="numeric"
|
||||||
|
value={cardExpiry}
|
||||||
|
oninput={(e) => (cardExpiry = formatExpiry((e.target as HTMLInputElement).value))}
|
||||||
|
placeholder="MM/YY"
|
||||||
|
maxlength={5}
|
||||||
|
{disabled}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="space-y-2">
|
||||||
|
<Label for="cardCVC">CVC</Label>
|
||||||
|
<Input
|
||||||
|
id="cardCVC"
|
||||||
|
type="text"
|
||||||
|
inputmode="numeric"
|
||||||
|
bind:value={cardCVC}
|
||||||
|
placeholder="123"
|
||||||
|
maxlength={4}
|
||||||
|
{disabled}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{#if showSaveCard}
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<Checkbox id="saveCard" bind:checked={saveCard} disabled={disabled} />
|
||||||
|
<Label for="saveCard" class="text-sm font-normal">Save card for next time</Label>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
let { status }: { status: string } = $props();
|
||||||
|
|
||||||
|
const variantMap: Record<string, { label: string; variantClass: string }> = {
|
||||||
|
draft: { label: 'Draft', variantClass: 'bg-gray-100 text-gray-800' },
|
||||||
|
active: { label: 'Active', variantClass: 'bg-emerald-100 text-emerald-800' },
|
||||||
|
completed: { label: 'Completed', variantClass: 'bg-blue-100 text-blue-800' },
|
||||||
|
cancelled: { label: 'Cancelled', variantClass: 'bg-red-100 text-red-800' }
|
||||||
|
};
|
||||||
|
|
||||||
|
const display = $derived(variantMap[status] ?? variantMap.draft);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<span class="inline-flex items-center rounded-full px-2 py-0.5 text-xs font-medium {display.variantClass}">
|
||||||
|
{display.label}
|
||||||
|
</span>
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type { Snippet } from 'svelte';
|
||||||
|
|
||||||
|
let {
|
||||||
|
trigger
|
||||||
|
}: {
|
||||||
|
trigger?: Snippet;
|
||||||
|
} = $props();
|
||||||
|
|
||||||
|
let open = $state(false);
|
||||||
|
|
||||||
|
function toggle() {
|
||||||
|
open = !open;
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleClickOutside(e: MouseEvent) {
|
||||||
|
const target = e.target as HTMLElement;
|
||||||
|
if (!target.closest('[data-policy-popover]')) {
|
||||||
|
open = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleKeydown(e: KeyboardEvent) {
|
||||||
|
if (e.key === 'Escape') open = false;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<svelte:window onclick={handleClickOutside} onkeydown={handleKeydown} />
|
||||||
|
|
||||||
|
<span data-policy-popover class="relative inline-block">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onclick={toggle}
|
||||||
|
class="inline underline cursor-pointer text-xs hover:text-amber-700"
|
||||||
|
>
|
||||||
|
{#if trigger}
|
||||||
|
{@render trigger()}
|
||||||
|
{:else}
|
||||||
|
cancellation policy
|
||||||
|
{/if}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{#if open}
|
||||||
|
<div
|
||||||
|
class="absolute left-0 top-full z-50 mt-1 w-48 rounded-md border border-gray-200 bg-white p-2 shadow-lg"
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
href="/cancellation-policy"
|
||||||
|
target="_blank"
|
||||||
|
class="block w-full rounded px-3 py-2 text-left text-sm hover:bg-gray-100"
|
||||||
|
onclick={() => (open = false)}
|
||||||
|
>
|
||||||
|
Open
|
||||||
|
</a>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="block w-full rounded px-3 py-2 text-left text-sm hover:bg-gray-100"
|
||||||
|
onclick={() => {
|
||||||
|
open = false;
|
||||||
|
const win = window.open('/cancellation-policy?format=pdf', '_blank');
|
||||||
|
if (win) win.focus();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Download PDF
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</span>
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
export const POLICY = {
|
||||||
|
FULL_REFUND_THRESHOLD_HOURS: 72,
|
||||||
|
PARTIAL_REFUND_THRESHOLD_HOURS: 24,
|
||||||
|
NO_SHOW_THRESHOLD_HOURS: 24,
|
||||||
|
DEPOSIT_ADVANCE_HOURS: 36,
|
||||||
|
LOYALTY_STAMP_REDEMPTION_COST: 10,
|
||||||
|
RESCHEDULE_BLOCK_HOURS_WITH_PAYMENTS: 72,
|
||||||
|
RESCHEDULE_BLOCK_HOURS_NO_PAYMENTS: 24,
|
||||||
|
PROTECTED_DEPOSIT_MAX_PCT: 0.50,
|
||||||
|
REQUIRED_DEPOSIT_PCT: 0.20,
|
||||||
|
} as const;
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
/**
|
||||||
|
* Sanitizes text for display in toast messages to prevent XSS.
|
||||||
|
* Escapes common HTML entities - defense-in-depth since svelte-sonner renders as text.
|
||||||
|
*/
|
||||||
|
export function sanitizeText(text: string): string {
|
||||||
|
const map: Record<string, string> = {
|
||||||
|
'&': '&',
|
||||||
|
'<': '<',
|
||||||
|
'>': '>',
|
||||||
|
'"': '"',
|
||||||
|
"'": '''
|
||||||
|
};
|
||||||
|
return text.replace(/[&<>"']/g, (ch) => map[ch] || ch);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a sanitized error message from an API Response.
|
||||||
|
* Reads the response body as text and sanitizes it.
|
||||||
|
*/
|
||||||
|
export async function apiErrorText(response: Response): Promise<string> {
|
||||||
|
try {
|
||||||
|
const text = await response.text();
|
||||||
|
if (!text) return 'An error occurred';
|
||||||
|
return sanitizeText(text.slice(0, 200)); // limit length
|
||||||
|
} catch {
|
||||||
|
return 'An error occurred';
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
/**
|
||||||
|
* Generate a UUID v4 string.
|
||||||
|
*
|
||||||
|
* Uses `window.crypto.getRandomValues()` when available (secure context),
|
||||||
|
* falls back to `Math.random()` for non-secure contexts (e.g. plain HTTP).
|
||||||
|
*
|
||||||
|
* Safe for all environments — does NOT rely on `crypto.randomUUID()`
|
||||||
|
* which requires a secure context (HTTPS).
|
||||||
|
*/
|
||||||
|
export function generateUUID(): string {
|
||||||
|
const array = new Uint8Array(16);
|
||||||
|
if (typeof window !== 'undefined' && window.crypto?.getRandomValues) {
|
||||||
|
window.crypto.getRandomValues(array);
|
||||||
|
} else {
|
||||||
|
for (let i = 0; i < 16; i++) array[i] = Math.floor(Math.random() * 256);
|
||||||
|
}
|
||||||
|
// UUID v4 marker and variant
|
||||||
|
array[6] = (array[6] & 0x0f) | 0x40;
|
||||||
|
array[8] = (array[8] & 0x3f) | 0x80;
|
||||||
|
return [...array]
|
||||||
|
.map((b, i) => {
|
||||||
|
const hex = b.toString(16).padStart(2, '0');
|
||||||
|
if (i === 4 || i === 6 || i === 8 || i === 10) return '-' + hex;
|
||||||
|
return hex;
|
||||||
|
})
|
||||||
|
.join('');
|
||||||
|
}
|
||||||
@@ -0,0 +1,271 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { page } from '$app/stores';
|
||||||
|
import { onMount } from 'svelte';
|
||||||
|
|
||||||
|
let format = $state('html');
|
||||||
|
|
||||||
|
let pdfNotice = $state(true);
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
format = $page.url.searchParams.get('format') || 'html';
|
||||||
|
if (format === 'pdf') {
|
||||||
|
// Strip ?format=pdf from the URL so a refresh doesn't re-trigger the print dialog.
|
||||||
|
const clean = window.location.pathname + window.location.hash;
|
||||||
|
history.replaceState(null, '', clean);
|
||||||
|
|
||||||
|
// Open the print dialog once the page is rendered.
|
||||||
|
// The notice element is removed before print so it won't appear in the PDF.
|
||||||
|
setTimeout(() => {
|
||||||
|
pdfNotice = false;
|
||||||
|
// Small delay so Svelte can remove the element before the print engine snapshots.
|
||||||
|
setTimeout(() => window.print(), 50);
|
||||||
|
}, 100);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<svelte:head>
|
||||||
|
<title>Booking, Deposit & Cancellation Policy</title>
|
||||||
|
<style>
|
||||||
|
@media print {
|
||||||
|
:global(nav),
|
||||||
|
:global(.no-print) {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
:global(body) {
|
||||||
|
padding-top: 0 !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</svelte:head>
|
||||||
|
|
||||||
|
<div class="mx-auto max-w-2xl px-4 py-8 text-gray-900">
|
||||||
|
<h1 class="mb-2 border-b border-gray-200 pb-4 text-2xl font-bold">
|
||||||
|
Booking, Deposit & Cancellation Policy
|
||||||
|
</h1>
|
||||||
|
<p class="mb-8 font-mono text-xs text-gray-500">Last updated: 18 June 2026</p>
|
||||||
|
|
||||||
|
{#if format === 'pdf' && pdfNotice}
|
||||||
|
<p class="mb-6 rounded border border-gray-200 bg-gray-50 p-3 text-xs text-gray-600 italic">
|
||||||
|
Generating PDF… If the print dialog does not appear, use Ctrl+P / Cmd+P.
|
||||||
|
</p>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<div class="space-y-8 text-sm leading-relaxed text-gray-700">
|
||||||
|
<!-- Section 1 -->
|
||||||
|
<section>
|
||||||
|
<h2 class="mb-3 text-base font-semibold text-gray-900">
|
||||||
|
1. Deposit Requirements & Booking Rules
|
||||||
|
</h2>
|
||||||
|
<p class="mb-3">
|
||||||
|
Where a deposit is required to secure your appointment, you must pay at least
|
||||||
|
<strong>20%</strong> of the total service cost before the 24-hour deadline prior to the
|
||||||
|
appointment. The 20% can be paid in a single payment or accumulated across multiple
|
||||||
|
payments — what matters is the total when the deadline passes. When deposit restrictions
|
||||||
|
are active on your account, appointments must be scheduled at least
|
||||||
|
<strong>36 hours in advance</strong>.
|
||||||
|
</p>
|
||||||
|
<p class="mb-3">
|
||||||
|
To maintain fairness and prevent scheduling abuse, accounts with outstanding deposit
|
||||||
|
requirements are limited to <strong>one (1) active booking</strong> at any given time. No additional
|
||||||
|
appointments can be scheduled until your current appointment is either completed, cancelled, or
|
||||||
|
officially lapsed.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Section 2 -->
|
||||||
|
<section>
|
||||||
|
<h2 class="mb-3 text-base font-semibold text-gray-900">
|
||||||
|
2. Unpaid Deposits & The "Pending Release" Window
|
||||||
|
</h2>
|
||||||
|
<p class="mb-3">
|
||||||
|
If a required deposit is not paid at least 24 hours before the appointment begins, the
|
||||||
|
booking is shifted into a <strong>"Pending Release"</strong> status. The slot becomes
|
||||||
|
vulnerable — if another customer books an overlapping time and pays, your original
|
||||||
|
booking is automatically evicted.
|
||||||
|
</p>
|
||||||
|
<p class="mb-3">
|
||||||
|
While in this status, your appointment is <strong>not guaranteed</strong>. The system will
|
||||||
|
make this time slot visible to other clients. If another user attempts to book an
|
||||||
|
overlapping time and completes their deposit payment first, your unpaid booking will be
|
||||||
|
immediately and automatically evicted.
|
||||||
|
</p>
|
||||||
|
<p class="mb-3">
|
||||||
|
If the slot has not yet been claimed by another client, paying your outstanding deposit will
|
||||||
|
instantly restore your booking to a fully confirmed status.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Section 3 -->
|
||||||
|
<section>
|
||||||
|
<h2 class="mb-3 text-base font-semibold text-gray-900">3. Cancellation & Refund Tiers</h2>
|
||||||
|
<p class="mb-3">
|
||||||
|
We understand that plans can change. Eligibility for a refund depends strictly on the amount
|
||||||
|
of notice provided prior to your scheduled appointment time. These thresholds represent a
|
||||||
|
genuine pre-estimate of the operational costs and loss of business incurred by late
|
||||||
|
cancellations:
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="mt-4 divide-y divide-gray-200 rounded-md border border-gray-200">
|
||||||
|
<div class="bg-gray-50/50 p-4">
|
||||||
|
<p class="font-semibold text-gray-900">Notice of more than 72 hours</p>
|
||||||
|
<p class="mt-1 text-xs text-gray-600">
|
||||||
|
You are entitled to a full 100% refund of all deposits and advance payments made for the
|
||||||
|
booking.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="p-4">
|
||||||
|
<p class="font-semibold text-gray-900">Notice between 24 and 72 hours</p>
|
||||||
|
<p class="mt-1 text-xs text-gray-600">
|
||||||
|
Any advance payments made up to 50% of the total booking value are treated as a
|
||||||
|
Protected Deposit. This Protected Deposit is retained to cover the short-notice vacancy,
|
||||||
|
while any balance paid above 50% will be fully refunded.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="bg-gray-50/50 p-4">
|
||||||
|
<p class="font-semibold text-gray-900">Notice of less than 24 hours</p>
|
||||||
|
<p class="mt-1 text-xs text-gray-600">
|
||||||
|
All processed payments and deposits are entirely non-refundable and will be retained.
|
||||||
|
The cancellation will be logged as a missed appointment history strike.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h3 class="mt-6 mb-2 text-sm font-semibold text-gray-800">Refund Payment Method</h3>
|
||||||
|
<p class="mb-3 text-sm leading-relaxed text-gray-700">
|
||||||
|
Refunds are returned to the original payment method where possible:
|
||||||
|
</p>
|
||||||
|
<ul class="mb-4 list-disc space-y-2 pl-5 text-sm leading-relaxed text-gray-700">
|
||||||
|
<li>
|
||||||
|
<strong>Card payments</strong> (debit/credit card processed online or in-person): Refunded directly
|
||||||
|
back to the original card via Square. Processing times vary by card issuer (typically 3–10 working
|
||||||
|
days).
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<strong>Gift card payments</strong>: Refunded back to the original gift card. The
|
||||||
|
gift card's remaining balance is incremented and is immediately available for use.
|
||||||
|
Expired gift cards are non-refundable.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<strong>Cash payments</strong>: Credited to your account balance, available for immediate
|
||||||
|
use against future bookings or services.
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<p class="mb-4 text-xs leading-relaxed text-gray-500 italic">
|
||||||
|
If a card refund cannot be processed (e.g. the card is expired or the Square payment
|
||||||
|
reference is unavailable), the refund amount will be credited to your account balance as a
|
||||||
|
fallback, so you are never left out of pocket.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h3 class="mt-6 mb-2 text-sm font-semibold text-gray-800">Guest / Walk-In Bookings</h3>
|
||||||
|
<p class="mb-3 text-sm leading-relaxed text-gray-700">
|
||||||
|
Walk-in customers who book without creating an account are issued a <strong
|
||||||
|
>guest account</strong
|
||||||
|
> to hold their booking and payment records. The same cancellation and refund tiers above apply
|
||||||
|
to guest bookings. Refunds are returned as follows:
|
||||||
|
</p>
|
||||||
|
<ul class="mb-4 list-disc space-y-2 pl-5 text-sm leading-relaxed text-gray-700">
|
||||||
|
<li>
|
||||||
|
<strong>Card payments</strong>: Refunded back to the original card via Square (same as
|
||||||
|
registered users).
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<strong>Gift card payments</strong>: Refunded back to the original gift card. The
|
||||||
|
gift card's remaining balance is incremented. Expired gift cards are non-refundable.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<strong>Cash payments</strong>: Refunded in person at the salon. Please bring your receipt
|
||||||
|
and an admin will process your cash refund at the till.
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<p class="mb-3 text-xs leading-relaxed text-gray-500 italic">
|
||||||
|
Guest accounts do not hold rolling balances. All refunds for walk-in bookings are returned
|
||||||
|
via the original payment method or processed manually by salon staff.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Section 4 -->
|
||||||
|
<section>
|
||||||
|
<h2 class="mb-3 text-base font-semibold text-gray-900">4. Missed Appointments (No-Shows)</h2>
|
||||||
|
<p class="mb-3">
|
||||||
|
Failing to attend a confirmed appointment without notifying us in advance constitutes a
|
||||||
|
"No-Show". Cancelling a pending or deposit-lapsed booking within 24 hours does
|
||||||
|
<strong>not</strong> count as a no-show — only confirmed bookings (where the slot was
|
||||||
|
secured with a payment) can incur no-show strikes.
|
||||||
|
</p>
|
||||||
|
<p class="mb-3">
|
||||||
|
If your account accumulates <strong
|
||||||
|
>two (2) No-Show strikes within a rolling 6-month period</strong
|
||||||
|
>, our system will automatically restrict your account privileges, making upfront deposits
|
||||||
|
mandatory for future booking attempts (3 deposits required before deposits are automatically
|
||||||
|
cleared). No-show strikes older than 6 months are automatically excluded from the count.
|
||||||
|
</p>
|
||||||
|
<p class="mb-3">
|
||||||
|
Each completed booking with a payment reduces the required deposit count by one. Once the
|
||||||
|
count reaches zero, all prior no-show records within the 6-month window are forgiven and
|
||||||
|
your account returns to normal — no upfront deposits required — until a new no-show occurs.
|
||||||
|
The salon can also forgive individual no-shows at management's discretion, which
|
||||||
|
immediately removes them from the count.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Section 5 -->
|
||||||
|
<section>
|
||||||
|
<h2 class="mb-3 text-base font-semibold text-gray-900">
|
||||||
|
5. Statutory Rights & Exceptional Circumstances
|
||||||
|
</h2>
|
||||||
|
<p class="mb-3">
|
||||||
|
This policy is strictly aligned with the <strong>Consumer Rights Act 2015</strong>. Nothing
|
||||||
|
in these terms limits your statutory right to receive services carried out with reasonable
|
||||||
|
care and skill, or your right to a full refund if we are forced to cancel your appointment
|
||||||
|
due to our own scheduling conflicts.
|
||||||
|
</p>
|
||||||
|
<p class="mb-3">
|
||||||
|
Please note that in accordance with UK statutory exclusions for distance contracts, the
|
||||||
|
standard 14-day statutory cancellation "cooling-off" period under the Consumer Contracts
|
||||||
|
Regulations 2013 does not apply to online bookings scheduled for a specific date or time.
|
||||||
|
</p>
|
||||||
|
<p class="mb-3">
|
||||||
|
We recognize that genuine emergencies, sudden severe illness, or bereavement can occur. Our
|
||||||
|
management team retains complete administrative system access to waive cancellation fees,
|
||||||
|
refund deposits, or clear no-show history strikes on a case-by-case basis under exceptional
|
||||||
|
circumstances.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Section 6 -->
|
||||||
|
<section>
|
||||||
|
<h2 class="mb-3 text-base font-semibold text-gray-900">6. Policy Amendments and Updates</h2>
|
||||||
|
<p class="mb-3">
|
||||||
|
We reserve the right to amend, update, or modify this Booking, Deposit & Cancellation Policy
|
||||||
|
at any time to reflect changes in our business operations, system features, or legal
|
||||||
|
obligations under UK and Scottish law.
|
||||||
|
</p>
|
||||||
|
<p class="mb-3">
|
||||||
|
Any updates will be published directly to this page, and the "Last updated" date at the top
|
||||||
|
will change accordingly. For active bookings scheduled prior to an amendment, the terms in
|
||||||
|
place at the exact time your booking was created will apply. Continued use of our booking
|
||||||
|
system after a policy revision implies formal agreement to the updated terms.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Section 7 -->
|
||||||
|
<section class="border-t border-gray-200 pt-6">
|
||||||
|
<h2 class="mb-2 text-base font-semibold text-gray-900">7. Governing Law</h2>
|
||||||
|
<p class="mb-4">
|
||||||
|
These terms and conditions are governed by and construed in accordance with the laws of
|
||||||
|
Scotland. Any disputes or legal claims arising from these provisions shall be subject to the
|
||||||
|
exclusive jurisdiction of the Scottish courts.
|
||||||
|
</p>
|
||||||
|
<p class="text-xs text-gray-500">
|
||||||
|
If you need to request an adjustment due to exceptional circumstances or have questions
|
||||||
|
regarding your upcoming appointments, please use our official <a
|
||||||
|
href="/contact"
|
||||||
|
class="font-medium text-blue-600 underline hover:text-blue-800">Contact Channels</a
|
||||||
|
> to get in touch.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
Reference in New Issue
Block a user