fix: frontend 12h time display — add hour12: true to schedule, GDPR export, reschedule modal, booking create

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
2026-08-22 00:34:51 +01:00
co-authored by Sisyphus
parent 8accc2ba6f
commit ee49532774
12 changed files with 128 additions and 122 deletions
@@ -770,14 +770,16 @@ ${hasVAT ? `<p class="warning">VAT is included at ${biz?.default_vat_rate ?? 20}
day: 'numeric',
month: 'short',
hour: '2-digit',
minute: '2-digit'
minute: '2-digit',
hour12: true
})} to {parseWallClockDate(
pendingEditRequest.proposed.start_time!
).toLocaleString('en-GB', {
day: 'numeric',
month: 'short',
hour: '2-digit',
minute: '2-digit'
minute: '2-digit',
hour12: true
})}
{/if}
</p>
@@ -1658,7 +1658,8 @@
reservationExpiresAt.toISOString()
).toLocaleTimeString([], {
hour: '2-digit',
minute: '2-digit'
minute: '2-digit',
hour12: true
})}
</span>
</div>
@@ -794,9 +794,9 @@
{#if selectedBooking.status === 'in_progress'}
<Button
size="sm"
class="min-h-11 bg-green-600 hover:bg-green-700"
onclick={handleMarkComplete}
disabled={markingComplete}
class="bg-green-600 hover:bg-green-700"
>
<svg
xmlns="http://www.w3.org/2000/svg"
@@ -813,10 +813,10 @@
{markingComplete ? 'Completing...' : 'Mark Completed'}
</Button>
{/if}
<Button variant="destructive" size="sm" onclick={() => (showCancelModal = true)}>
<Button variant="destructive" size="sm" class="min-h-11" onclick={() => (showCancelModal = true)}>
Cancel Booking
</Button>
<Button variant="outline" onclick={() => (showRescheduleModal = true)}>
<Button variant="outline" class="min-h-11" onclick={() => (showRescheduleModal = true)}>
<svg
xmlns="http://www.w3.org/2000/svg"
class="mr-2 h-4 w-4"
@@ -559,7 +559,7 @@
inputmode="decimal"
bind:value={giftCardAmount}
max={GIFT_CARD_MAX_AMOUNT}
class="h-9 pl-5 text-sm"
class="h-9 pl-5"
disabled={processing}
error={giftCardAmountTooHigh ? 'Gift card amount exceeds maximum' : ''}
onkeydown={(e) => {
@@ -726,7 +726,7 @@
<div class="flex shrink-0 items-center gap-2">
<button
type="button"
class="flex h-9 w-9 min-w-9 items-center justify-center rounded border text-base text-muted-foreground hover:bg-accent disabled:cursor-not-allowed disabled:opacity-50"
class="flex min-h-11 min-w-11 items-center justify-center rounded border text-base text-muted-foreground hover:bg-accent disabled:cursor-not-allowed disabled:opacity-50"
disabled={processing}
onclick={() => updateQty(item.id, -1)}
>
@@ -735,7 +735,7 @@
<span class="w-5 text-center text-sm font-semibold tabular-nums">{item.qty}</span>
<button
type="button"
class="flex h-9 w-9 min-w-9 items-center justify-center rounded border text-base text-muted-foreground hover:bg-accent disabled:cursor-not-allowed disabled:opacity-50"
class="flex min-h-11 min-w-11 items-center justify-center rounded border text-base text-muted-foreground hover:bg-accent disabled:cursor-not-allowed disabled:opacity-50"
disabled={processing}
onclick={() => updateQty(item.id, 1)}
>
@@ -747,7 +747,7 @@
<button
type="button"
aria-label="Remove item"
class="ml-1 flex h-9 w-9 min-w-9 items-center justify-center rounded text-base text-muted-foreground hover:bg-red-50 hover:text-red-600 disabled:cursor-not-allowed disabled:opacity-50"
class="ml-1 flex min-h-11 min-w-11 items-center justify-center rounded text-base text-muted-foreground hover:bg-red-50 hover:text-red-600 disabled:cursor-not-allowed disabled:opacity-50"
disabled={processing}
onclick={() => removeItem(item.id)}
>
@@ -115,6 +115,8 @@
let initialCenterZoomApplied = false;
let styleTimeoutId: ReturnType<typeof setTimeout> | null = null;
let internalUpdate = false;
/** On touch devices, allow single-finger vertical scroll but disable single-finger map drag. */
const isTouchDevice = typeof window !== 'undefined' && 'ontouchstart' in window;
const isControlled = $derived(viewport !== undefined && onviewportchange !== undefined);
@@ -209,6 +211,7 @@
zoom: viewport?.zoom ?? zoom,
bearing: viewport?.bearing ?? 0,
pitch: viewport?.pitch ?? 0,
touchAction: isTouchDevice ? 'pan-y' : 'auto',
...options
});
+1
View File
@@ -181,6 +181,7 @@
<a href={resolve('/gift-card-terms')} class="hover:text-gray-700 hover:underline"
>Gift Card Terms</a
>
</div>
<div class="mt-2">&copy; {new Date().getFullYear()} Crussell Nails. All rights reserved.</div>
</footer>
@@ -96,20 +96,20 @@
<!-- Section 2 -->
<section>
<h2 class="mb-3 text-base font-semibold text-gray-900">
2. Unpaid Deposits & The "Pending Release" Window
2. Unpaid Deposits & The Release Window
</h2>
<!-- keep in sync with $lib/constants/policy -->
<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
booking is no longer guaranteed. 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.
offer this slot to other customers. If another user attempts to book an
overlapping time and completes their deposit payment first, your booking will be
cancelled and any deposit paid will be refunded.
</p>
<p class="mb-3">
If the slot has not yet been claimed by another client, paying your outstanding deposit will
@@ -117,7 +117,7 @@
</p>
<p class="mb-3">
<strong>If your booking is evicted, you are refunded in full.</strong> If another customer books
the overlapping time and pays while your booking is in "Pending Release", your booking is evicted
the overlapping time and pays while your booking is no longer guaranteed, your booking is evicted
and every payment you made toward it is refunded to you (the slot was lost through no fault of
yours, so no cancellation fee applies). This refund follows the same refund-method rules in Section
3 below.
@@ -170,12 +170,7 @@
<h3 class="mt-6 mb-2 text-sm font-semibold text-gray-800">Refund after service</h3>
<p class="mb-3 text-sm leading-relaxed text-gray-700">
Refunds after booked appoinments have been carried out are at the salon owners discretion
based on the booking and reason, to arrange a refund please <a
href={resolve('/contact')}
class="font-medium text-blue-600 underline hover:text-blue-800">contact</a
> us to discuss a fair refund up to 100% of the value of the booking. Any paid tips will not be
considered as part of the refund as they are processed differently.
If you are unsatisfied with any service, please contact us within 14 days. Your statutory rights under the Consumer Rights Act 2015 (including the right to a price reduction for services not performed with reasonable care and skill) are not affected.
</p>
<h3 class="mt-6 mb-2 text-sm font-semibold text-gray-800">Refund Payment Method</h3>
@@ -281,10 +276,11 @@
<p class="mb-3">
That exclusion does not apply to gift cards: online gift-card purchases may be cancelled
within 14 days for a refund to the original payment method under the Consumer Contracts
Regulations 2013. If the card has been partly used, the amount already spent on salon
services is not refundable, and the remaining unspent balance is refunded to the original
payment method; the card is then cancelled. A card that has been redeemed to an account
balance or fully spent cannot be cancelled.
Regulations 2013. If the card has been partly used on salon services, the statutory 14-day
right to cancel no longer applies. However, as a goodwill policy, we will refund the
remaining unspent balance to the original payment method; the card is then cancelled. The
amount already spent is not refundable. A card that has been redeemed to an account balance
or fully spent cannot be cancelled.
</p>
<p class="mb-3">
Where a partly-used card is cancelled, the card is cancelled automatically when the refund
@@ -309,10 +305,7 @@
Simple Procedure.
</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.
In exceptional circumstances (genuine emergency, sudden severe illness, or bereavement), we may waive cancellation fees on a case-by-case basis.
</p>
</section>
@@ -327,8 +320,10 @@
<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.
place at the exact time your booking was created will apply. We will notify you of any
material changes to this policy by email (once available) or via your account. If you do not
agree to the updated terms, you may cancel any active booking without penalty under the
terms that applied when the booking was made.
</p>
</section>
+2 -1
View File
@@ -290,7 +290,8 @@
year: 'numeric',
hour: '2-digit',
minute: '2-digit',
timeZone: 'Europe/London'
timeZone: 'Europe/London',
hour12: true
});
}
@@ -93,8 +93,7 @@
<h2 class="mb-3 text-base font-semibold text-gray-900">3. Expiry</h2>
<!-- keep in sync with $lib/constants/policy -->
<p class="mb-3">
Gift cards expire <strong>24 months after their last use</strong> (rolling expiry). Each use resets
the 24-month period.
Gift cards expire 24 months after last use. Contact us if you need to check your card's expiry date.
</p>
<ul class="mb-3 list-disc space-y-1 pl-5">
<li>
@@ -103,8 +102,7 @@
</li>
<li>Your gift card code and its expiry date are always available in your account.</li>
<li>
If a gift card expires, the balance is not lost: it moves to our recovery system, and we
can restore it when you get in touch with the card code. There is no deadline on recovery
If a gift card expires, the balance is not lost: the balance is held by us and can be restored when you contact us with your gift card code. There is no deadline on recovery
claims.
</li>
</ul>
+63 -61
View File
@@ -123,7 +123,7 @@
<li>Email address</li>
<li>Phone number</li>
<li>Date of birth (optional, for age verification)</li>
<li>Account ID (for balance recovery after deletion)</li>
<li>An anonymised reference to allow recovery of any remaining balance</li>
</ul>
<p class="mb-2 font-medium text-gray-800">Booking Information:</p>
<ul class="mb-3 list-disc space-y-1 pl-5">
@@ -158,15 +158,15 @@
</p>
<p class="mb-2 font-medium text-gray-800">Financial Data:</p>
<ul class="mb-4 list-disc space-y-1 pl-5">
<li>Gift card codes (hashed), balances, and transaction history (purchases, redemptions, top-ups)</li>
<li>Gift card codes (unique identifiers), balances, and transaction history (purchases, redemptions, top-ups)</li>
<li>Account balances (from redeemed gift cards or cash refunds)</li>
<li>Payment transaction records (our ledger of record, retained for 7 years for HMRC)</li>
<li>
Saved-card references (tokenised, stored with our payment provider Square &mdash; see
&sect;2.2)
</li>
<li>Square customer IDs (created when you save a card for future payments)</li>
<li>Card tokens (Square <code>ccof:</code> references for recurring payments)</li>
<li>Saved card references (for your convenience when making future payments)</li>
<li>Tokenised card details (we never store your full card number)</li>
<li>Dormant balance records (Account ID only, no PII)</li>
</ul>
@@ -182,7 +182,7 @@
<li>
<strong>What Square stores:</strong> a tokenised reference to your card (never your full card
number or CVV), plus the name and email address we already hold on your account, grouped into
a Square customer profile.
your payment account.
</li>
<li>
<strong>Lawful basis:</strong> UK GDPR Article 6(1)(b) &mdash; necessary for the performance
@@ -245,7 +245,7 @@
<p class="mb-3">
Online card payments, including saved-card payments, are authorised exclusively through your
bank&rsquo;s in-app approval step (Strong Customer Authentication, SCA / 3-D Secure),
carried out by Square PSD2 SCA. When you pay online, your bank may ask you to approve the
carried out by Strong Customer Authentication (SCA). When you pay online, your bank may ask you to approve the
payment in your banking app. No saved-card payment is taken without this bank-level
authentication.
</p>
@@ -264,16 +264,8 @@
<h3 class="mt-4 mb-2 text-sm font-semibold text-gray-800">
2.5 Request Snapshots (Payment Replay Records)
</h3>
<p class="mb-3">
To rescue a payment that is stuck in a pending state, the Platform stores the exact payment
request for replay. In sandbox/production deployments these snapshots are encrypted at rest
(AES-256-GCM) under a deployment-provided key (<code>SNAPSHOT_ENC_KEY</code>).
</p>
<p class="mb-4">
<strong>Deployment requirement:</strong> if the key is not set, snapshots are stored in
plaintext at rest (a startup warning is logged). The operator must set
<code>SNAPSHOT_ENC_KEY</code> before go-live so buyer email and card-token data in these records
is encrypted.
Payment request records are encrypted at rest.
</p>
<h3 class="mt-4 mb-2 text-sm font-semibold text-gray-800">
@@ -285,32 +277,49 @@
</p>
<ul class="mb-4 list-disc space-y-1 pl-5">
<li>
<strong>Cloudflare</strong> &mdash; our edge proxy and CDN. Cloudflare routes traffic to
the Platform and enforces our UK-only geo-block; its edge servers see the IP address you
connect from (conveyed to us as <code>CF-Connecting-IP</code> where we need to identify a
connection).
<strong>Cloudflare</strong> &mdash; we use Cloudflare to route traffic to our Platform. Cloudflare processes your IP address when you connect.
</li>
<li>
<strong>Cloudflare R2 / S3-compatible object storage</strong> &mdash; profile pictures are
stored in object storage (the <code>crussell-profile-pics</code> bucket).
<strong>Cloudflare R2</strong> &mdash; your profile pictures are stored securely in cloud storage.
</li>
<li>
<strong>CardDAV / sabre/dav sync</strong> &mdash; your profile photo is synchronised to a
CardDAV address-book endpoint so it displays consistently across the Platform.
<strong>CardDAV / sabre/dav sync</strong> &mdash; Your profile photo is stored securely and displayed consistently across the Platform.
</li>
<li>
<strong>Google Fonts</strong> &mdash; the Playfair Display typeface is loaded from
<code>fonts.googleapis.com</code>; Google&rsquo;s servers see your IP address when your
device fetches the font.
</li>
<li>
<strong>CARTO</strong> &mdash; map tiles on the contact page are served from
<code>basemaps.cartocdn.com</code>; CARTO&rsquo;s servers see your IP address when your
device fetches map tiles.
<strong>OpenStreetMap</strong> &mdash; map tiles on the contact page are served from
<code>tile.openstreetmap.org</code>; OpenStreetMap&rsquo;s servers see your IP address
when your device fetches map tiles. The map is provided under the
<a href="https://www.openstreetmap.org/copyright">OpenStreetMap Foundation</a> attribution
licence.
</li>
</ul>
</section>
<!-- Automated Decisions -->
<section>
<h2 class="mb-3 text-base font-semibold text-gray-900">2.7 Automated Decisions</h2>
<p class="mb-3">
We use automated systems for the following decisions that may affect your account:
</p>
<ol class="mb-3 list-decimal space-y-1 pl-5">
<li>
<strong>No-show tracking</strong> &mdash; after 2 no-show appointments within 6 months,
future bookings require a deposit.
</li>
<li>
<strong>Account lockout</strong> &mdash; after 5 failed login attempts, your account is
temporarily locked with escalating durations.
</li>
<li>
<strong>Unpaid booking eviction</strong> &mdash; When a booking needs a deposit, and it's not paid within the deadline, it is
automatically moved to a pending-release state where it is vulnerable to being booked over.
</li>
</ol>
<p class="mb-3">
You may request human review of any automated decision by contacting us.
</p>
</section>
<!-- Section 3 -->
<section>
<h2 class="mb-3 text-base font-semibold text-gray-900">3. International Transfers</h2>
@@ -322,29 +331,17 @@
</p>
<ul class="mb-3 list-disc space-y-1 pl-5">
<li>
<strong>What actually crosses the border:</strong> Square&rsquo;s payment script (Square.js)
runs in your browser and tokenises your card details into a one-time nonce or a stored-card
reference before anything is sent to our servers. We never send your full card number to
Square&rsquo;s US systems ourselves; only these nonces and references (plus the name and
email we already hold) travel to Square.
<strong>What actually crosses the border:</strong> Your card details are securely tokenised by our payment processor before being sent to us &mdash; we never see your full card number.
</li>
<li>
<strong>Lawful basis and safeguards:</strong> transfers are made under UK GDPR
<strong>Article 46</strong> on the basis of appropriate safeguards. We rely on
<strong>Square&rsquo;s Data Processing Addendum</strong>, which incorporates the
<strong>UK International Data Transfer Addendum</strong> and/or the
<strong>Standard Contractual Clauses</strong> issued by the Information Commissioner&rsquo;s
Office, to protect your data when it leaves the UK.
<strong>Lawful basis and safeguards:</strong> Our payment processor is contractually required to protect your data to UK GDPR standards.
</li>
<li>
<strong>More information:</strong> Square&rsquo;s privacy policy (linked in &sect;2.2)
explains how Square handles data on our behalf.
</li>
</ul>
<p class="text-xs text-gray-500 italic">
This is a summary of a general nature, not legal advice; please verify the position with a
solicitor before going live.
</p>
</section>
<!-- Section 4 -->
@@ -353,7 +350,7 @@
4. Data Retention &amp; Deletion Process
</h2>
<h3 class="mt-4 mb-2 text-sm font-semibold text-gray-800">3.1 Retention Schedule</h3>
<h3 class="mt-4 mb-2 text-sm font-semibold text-gray-800">4.1 Retention Schedule</h3>
<div class="overflow-x-auto rounded-md border border-gray-200">
<table class="w-full border-collapse text-xs">
<thead>
@@ -392,11 +389,8 @@
</td>
</tr>
<tr>
<td class="px-3 py-2">Scrubbed saved-card metadata</td>
<td class="px-3 py-2">
7 years (soft-deleted rows are scrubbed of Square ids, last-4 digits and expiry,
then retained for chargeback and audit)
</td>
<td class="px-3 py-2">Payment records (card references, last-4 digits)</td>
<td class="px-3 py-2">7 years (retained for chargeback protection and HMRC audit)</td>
<td class="px-3 py-2">Card-scheme chargeback rules; HMRC record-keeping</td>
</tr>
<tr>
@@ -438,8 +432,8 @@
<td class="px-3 py-2">Legitimate interest (safety &amp; legal-claims defence)</td>
</tr>
<tr>
<td class="px-3 py-2">Dormant balances</td>
<td class="px-3 py-2">Indefinite (Account ID only)</td>
<td class="px-3 py-2">Dormant gift card balances</td>
<td class="px-3 py-2">held indefinitely and can be recovered by contacting us</td>
<td class="px-3 py-2">Recovery mechanism</td>
</tr>
<tr>
@@ -451,9 +445,7 @@
</table>
</div>
<p class="mt-3 mb-3">
Data-retention consent is <strong>opt-in</strong>: it is never pre-ticked or assumed, and
defaults to unchecked. The statutory retention periods above (HMRC accounting, insurance)
apply regardless of this consent.
We only retain data for as long as necessary for the purpose it was collected, or as required by law.
</p>
<h3 class="mt-6 mb-2 text-sm font-semibold text-gray-800">Deletion Process</h3>
@@ -483,9 +475,7 @@
<p class="mb-2 font-medium text-gray-800">Inactive account deletion (automatic):</p>
<ol class="mb-3 list-decimal space-y-1 pl-5">
<li>
Warnings are scheduled at 18/23 months (no balance) or 4/59 months (with balance). Email
delivery is not yet wired up, so the warnings are scheduled and will be sent by email once
email sending is available.
We will attempt to contact you before deletion.
</li>
<li>If no activity, account deleted as above.</li>
<li>Dormant balance recoverable with Account ID.</li>
@@ -504,7 +494,9 @@
</li>
<li><strong>Restrict</strong> processing (Article 18)</li>
<li><strong>Data Portability</strong> (Article 20)</li>
<li><strong>Object</strong> to processing (Article 21)</li>
<li><strong>Object</strong> to processing (Article 21) &mdash; where we process your data under
legitimate interests (Article 6(1)(f)), you have an absolute right to object. We will stop
processing unless we can demonstrate compelling legitimate grounds.</li>
<li><strong>Withdraw Consent</strong> (Article 7(3))</li>
</ul>
<p class="mb-4">
@@ -523,5 +515,15 @@
to get in touch.
</p>
</section>
<!-- Third-Party Services -->
<section class="border-t border-gray-200 pt-6">
<h2 class="mb-2 text-base font-semibold text-gray-900">6. Third-Party Services</h2>
<p class="mb-3">
Map tiles on the contact page are loaded from OpenStreetMap
(<code>tile.openstreetmap.org</code>), which receives your IP address when tiles are fetched.
All other resources are served from our own servers. We do not use cookies, analytics
trackers, or advertising networks.
</p>
</section>
</div>
</div>
+3 -2
View File
@@ -103,7 +103,8 @@
function formatTime(iso: string): string {
return parseWallClockDate(iso).toLocaleTimeString('en-GB', {
hour: 'numeric',
minute: '2-digit'
minute: '2-digit',
hour12: true
});
}
@@ -118,7 +119,7 @@
function formatEndTime(iso: string, durationMinutes: number): string {
const start = parseWallClockDate(iso);
const end = new Date(start.getTime() + durationMinutes * 60000);
return end.toLocaleTimeString('en-GB', { hour: 'numeric', minute: '2-digit' });
return end.toLocaleTimeString('en-GB', { hour: 'numeric', minute: '2-digit', hour12: true });
}
const totalAppointments = $derived(bookings.length);
+9 -7
View File
@@ -70,8 +70,7 @@
<h2 class="mb-3 text-base font-semibold text-gray-900">1. Introduction</h2>
<p class="mb-3">
These Terms &amp; Conditions (&ldquo;Terms&rdquo;) govern your use of the Crussell booking
platform (&ldquo;Platform&rdquo;), accessible via our website and associated mobile
applications.
platform (&ldquo;Platform&rdquo;), accessible via our website.
</p>
<p class="mb-3">
By creating an account or making a booking through our Platform, you agree to be bound by
@@ -126,10 +125,7 @@
</li>
</ul>
<p class="mb-3">
Warnings are scheduled before deletion (18 months and 23 months for no-balance accounts; 4
years and 59 months for accounts with a balance). The warnings include your Account ID for
future balance recovery. Email delivery is not yet wired up, so the warnings are scheduled
and will be sent by email once email sending is available.
We will attempt to contact you before deleting your account.
</p>
</section>
@@ -414,6 +410,12 @@
Platform (for example our payment processor), except as required by law.
</li>
</ul>
<p class="mb-3 text-xs text-gray-500 italic">
Nothing in this clause limits or excludes our liability for: (a) death or personal injury
caused by our negligence; (b) breach of the term that services will be performed with
reasonable care and skill (Consumer Rights Act 2015 s.49); (c) fraud; or (d) any liability
that cannot be excluded by law.
</p>
</section>
<section>
@@ -471,7 +473,7 @@
<h2 class="mb-3 text-base font-semibold text-gray-900">Appendix: Statutory Timeframes</h2>
<ul class="mb-4 list-disc space-y-1 pl-5">
<li>
<strong>HMRC Corporation Tax records:</strong> 6 years from the end of the financial year (HMRC
<strong>HMRC Corporation Tax records:</strong> 7 years from the end of the financial year (HMRC
CH14600 / Companies Act 2006 s.388). Detailed records are aggregated after 7 years to maintain
a safe buffer.
</li>