Gift-card rolling expiry, SvelteDate→Date purge, strict DST tests, UTC scan-location + settings legal floor
Gift-card rolling expiry (setting-driven, was dead config): - GetGiftCardExpiryMonths(): single source of truth (business_settings gift_card_expiry_months, fallback 24) shared by payment handlers and the CleanupExpiredGiftCards job (was hardcoded 24). - expiry_date now maintained on ALL 9 gift-card write sites (buy, topup, transfer, redeem, terminal payment, refund credit, till) so the refund-time guard at refunds.go actually fires. Schema default 12->24 + migration note; test-DB seed aligned. Stale "expiry_date IS NULL" test rewritten; new expired-card-rejected regression test. Frontend SvelteDate purge (docs' stated convention, wide): - All 180+ raw `new SvelteDate(...)` uses across routes/components replaced with parseWallClockDate (backend UTC ISO) or new Date (wall-clock constructors). SvelteDate imports removed. timeSlots.ts getDayWithOrdinal fixed. Zero SvelteDate references remain; svelte-check clean. Strict timezone/DST testing + QA fixes: - 8 new hermetic boundary tests: clock.DST transitions (both 2026 folds), closing-hours GMT vs BST, booking date-window midnight, refund-tier elapsed-time independence, deposit-window UTC-instant, scheduling LondonDateString midnight, today AT TIME ZONE window + UTC round-trip. - today.go summary date labels fixed to London wall-clock (were showing the previous UTC day during BST) + regression test. - pgx ScanLocation fixed to UTC via AfterConnect (was host-local -> JSON offsets depended on deployment TZ, contradicting the documented UTC invariant) + regression test. Registered as a new *Type to avoid a data race on the shared type map (caught by -race). Admin Business Settings (setting now functional => legal floor): - gift_card_expiry_months validation floor raised 1 -> 12 months (CMA/ Consumer Rights Act 2015 unfair-contract-term guidance) in endpoint + UI, with rolling-expiry semantics shown in both display and edit form. - 3 new expiry validation tests; 2 pre-existing message assertions updated. Full suite 25/25 + race clean via run-tests.sh lockfile; svelte-check 0 errors/warnings; production build succeeds.
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { apiFetch } from '$lib/utils/api';
|
||||
import { SvelteDate, SvelteMap } from 'svelte/reactivity';
|
||||
import { SvelteMap } from 'svelte/reactivity';
|
||||
import { CalendarDate, getLocalTimeZone, type DateValue } from '@internationalized/date';
|
||||
import { toast } from 'svelte-sonner';
|
||||
import { extractErrorMessage } from '$lib/utils/toast-safe';
|
||||
@@ -16,7 +16,7 @@
|
||||
getLunchProtectionForSlots,
|
||||
timeToMinutes
|
||||
} from '$lib/lunchProtection';
|
||||
import { formatLocalDateTime, getLondonTodayCalendarDate } from '$lib/utils/timeSlots';
|
||||
import { formatLocalDateTime, getLondonTodayCalendarDate, parseWallClockDate } from '$lib/utils/timeSlots';
|
||||
import ClockIcon from '@lucide/svelte/icons/clock';
|
||||
import RefreshCwIcon from '@lucide/svelte/icons/refresh-cw';
|
||||
import ArrowLeftIcon from '@lucide/svelte/icons/arrow-left';
|
||||
@@ -71,7 +71,7 @@
|
||||
// ─── Date constants ─────────────────────────────────────
|
||||
const todayCalendarDate = getLondonTodayCalendarDate();
|
||||
const minDate = todayCalendarDate;
|
||||
const maxDate = new SvelteDate(
|
||||
const maxDate = new Date(
|
||||
todayCalendarDate.year,
|
||||
todayCalendarDate.month - 1,
|
||||
todayCalendarDate.day
|
||||
@@ -85,7 +85,7 @@
|
||||
let placeholderDate = $state<CalendarDate>(minDate);
|
||||
|
||||
const hoursUntilAppointment = $derived(
|
||||
(new SvelteDate(booking.start_time).getTime() - new SvelteDate().getTime()) / (1000 * 60 * 60)
|
||||
(parseWallClockDate(booking.start_time).getTime() - new Date().getTime()) / (1000 * 60 * 60)
|
||||
);
|
||||
const hasPayments = $derived((booking.amount_paid ?? 0) > 0);
|
||||
const noticePeriodBlocked = $derived(
|
||||
@@ -420,7 +420,7 @@
|
||||
$effect(() => {
|
||||
if (editMode === 'services' && !servicesHoursFetched) {
|
||||
servicesHoursFetched = true;
|
||||
const bookingDate = new SvelteDate(booking.start_time);
|
||||
const bookingDate = parseWallClockDate(booking.start_time);
|
||||
const calDate = new CalendarDate(
|
||||
bookingDate.getFullYear(),
|
||||
bookingDate.getMonth() + 1,
|
||||
@@ -445,8 +445,8 @@
|
||||
return;
|
||||
editRequestAutoSelectDone = true;
|
||||
|
||||
const currentDate = new SvelteDate(getLondonTodayCalendarDate().toString() + 'T00:00:00');
|
||||
const maxDateJs = new SvelteDate(
|
||||
const currentDate = new Date(getLondonTodayCalendarDate().toString() + 'T00:00:00');
|
||||
const maxDateJs = new Date(
|
||||
maxCalendarDate.year,
|
||||
maxCalendarDate.month - 1,
|
||||
maxCalendarDate.day
|
||||
@@ -458,7 +458,7 @@
|
||||
const daysToCheck = Math.min(daysDifference, 180);
|
||||
|
||||
for (let i = 0; i <= daysToCheck; i++) {
|
||||
const nextDate = new SvelteDate(currentDate);
|
||||
const nextDate = new Date(currentDate);
|
||||
nextDate.setDate(currentDate.getDate() + i);
|
||||
const dateStr = nextDate.toLocaleDateString('en-CA', { timeZone: 'Europe/London' });
|
||||
|
||||
@@ -637,7 +637,7 @@
|
||||
function calculateRemainingTime(): number {
|
||||
if (!workingHours || !availableHours) return 0;
|
||||
|
||||
const bookingDate = new SvelteDate(booking.start_time);
|
||||
const bookingDate = parseWallClockDate(booking.start_time);
|
||||
const dateStr = bookingDate.toLocaleDateString('en-CA', { timeZone: 'Europe/London' });
|
||||
|
||||
const dayWH = workingHours[dateStr];
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
import { authStore } from '$lib/stores/auth.svelte';
|
||||
import { apiFetch } from '$lib/utils/api';
|
||||
import { ensureBusinessInfo, getBusinessInfo } from '$lib/stores/businessInfo.svelte';
|
||||
import { SvelteDate } from 'svelte/reactivity';
|
||||
import { toast } from 'svelte-sonner';
|
||||
import { extractErrorMessage } from '$lib/utils/toast-safe';
|
||||
import * as Modal from '$lib/components/ui/dialog';
|
||||
@@ -56,7 +55,7 @@
|
||||
);
|
||||
|
||||
const isFutureBooking = $derived(
|
||||
selectedBooking ? new SvelteDate(selectedBooking.start_time) > new SvelteDate() : false
|
||||
selectedBooking ? parseWallClockDate(selectedBooking.start_time) > new Date() : false
|
||||
);
|
||||
|
||||
const hasPayments = $derived(
|
||||
@@ -112,7 +111,7 @@
|
||||
|
||||
const hoursUntilAppointment = $derived(
|
||||
selectedBooking
|
||||
? (new SvelteDate(selectedBooking.start_time).getTime() - new SvelteDate().getTime()) /
|
||||
? (parseWallClockDate(selectedBooking.start_time).getTime() - new Date().getTime()) /
|
||||
(1000 * 60 * 60)
|
||||
: Infinity
|
||||
);
|
||||
@@ -555,7 +554,7 @@ ${hasVAT ? `<p class="warning">VAT is included at ${biz?.default_vat_rate ?? 20}
|
||||
</div>
|
||||
|
||||
{#if selectedBooking}
|
||||
{@const isPastBooking = new SvelteDate(selectedBooking.start_time) < new SvelteDate()}
|
||||
{@const isPastBooking = parseWallClockDate(selectedBooking.start_time) < new Date()}
|
||||
{@const isUnpaid = selectedBooking.amount_due > 0}
|
||||
{@const showChip = !isPastBooking || isUnpaid}
|
||||
{@const isConfirmedOrLater = ['confirmed', 'in_progress', 'completed'].includes(
|
||||
@@ -849,7 +848,7 @@ ${hasVAT ? `<p class="warning">VAT is included at ${biz?.default_vat_rate ?? 20}
|
||||
</div>
|
||||
{/if}
|
||||
<div class="mt-1 text-xs text-gray-400">
|
||||
{new SvelteDate(payment.created_at).toLocaleString()}
|
||||
{parseWallClockDate(payment.created_at).toLocaleString()}
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-right font-semibold">
|
||||
@@ -875,7 +874,7 @@ ${hasVAT ? `<p class="warning">VAT is included at ${biz?.default_vat_rate ?? 20}
|
||||
{refund.reason || 'Refund processed'}
|
||||
</div>
|
||||
<div class="mt-1 text-xs text-gray-400">
|
||||
{new SvelteDate(refund.created_at).toLocaleString()}
|
||||
{parseWallClockDate(refund.created_at).toLocaleString()}
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-right font-semibold text-red-600">
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
<script lang="ts">
|
||||
import { SvelteDate } from 'svelte/reactivity';
|
||||
import { apiFetch } from '$lib/utils/api';
|
||||
import { toast } from 'svelte-sonner';
|
||||
import { extractErrorMessage, sanitizeText } from '$lib/utils/toast-safe';
|
||||
@@ -161,12 +160,12 @@
|
||||
function findOldestBooking(): { id: string; isCurrent: boolean } | null {
|
||||
if (overlappingBookings.length === 0) return null;
|
||||
|
||||
const currentCreatedAt = new SvelteDate(booking.created_at).getTime();
|
||||
const currentCreatedAt = parseWallClockDate(booking.created_at).getTime();
|
||||
let oldestId = booking.id;
|
||||
let oldestTime = currentCreatedAt;
|
||||
|
||||
for (const ob of overlappingBookings) {
|
||||
const obTime = new SvelteDate(ob.created_at).getTime();
|
||||
const obTime = parseWallClockDate(ob.created_at).getTime();
|
||||
if (obTime < oldestTime) {
|
||||
oldestTime = obTime;
|
||||
oldestId = ob.id;
|
||||
@@ -450,7 +449,7 @@
|
||||
{ob.status}
|
||||
</span>
|
||||
<div class="mt-1 text-xs text-gray-400">
|
||||
{new SvelteDate(ob.created_at).toLocaleDateString('en-GB', {
|
||||
{parseWallClockDate(ob.created_at).toLocaleDateString('en-GB', {
|
||||
day: 'numeric',
|
||||
month: 'short',
|
||||
hour: 'numeric',
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import { apiFetch } from '$lib/utils/api';
|
||||
import { toast } from 'svelte-sonner';
|
||||
import { extractErrorMessage } from '$lib/utils/toast-safe';
|
||||
import { SvelteDate, SvelteSet, SvelteURLSearchParams } from 'svelte/reactivity';
|
||||
import { SvelteSet, SvelteURLSearchParams } from 'svelte/reactivity';
|
||||
import { CalendarDate, getLocalTimeZone, type DateValue } from '@internationalized/date';
|
||||
import { isValidUKPhone, toE164UK } from '$lib/utils/phone';
|
||||
import { formatUserName } from '$lib/utils/nameDisplay';
|
||||
@@ -217,7 +217,7 @@
|
||||
// Date Boundaries
|
||||
const today = getLondonTodayCalendarDate();
|
||||
const minDate = today;
|
||||
const maxDate = new SvelteDate(today.year, today.month - 1, today.day);
|
||||
const maxDate = new Date(today.year, today.month - 1, today.day);
|
||||
maxDate.setMonth(today.month - 1 + 6);
|
||||
const maxCalendarDate = new CalendarDate(
|
||||
maxDate.getFullYear(),
|
||||
@@ -397,8 +397,8 @@
|
||||
!bookingCreateAutoSelectDone
|
||||
) {
|
||||
bookingCreateAutoSelectDone = true;
|
||||
const now = new SvelteDate(getLondonTodayCalendarDate().toString() + 'T00:00:00');
|
||||
const maxDateJs = new SvelteDate(
|
||||
const now = new Date(getLondonTodayCalendarDate().toString() + 'T00:00:00');
|
||||
const maxDateJs = new Date(
|
||||
maxCalendarDate.year,
|
||||
maxCalendarDate.month - 1,
|
||||
maxCalendarDate.day
|
||||
@@ -408,7 +408,7 @@
|
||||
);
|
||||
const daysToCheck = Math.min(daysDifference, 180);
|
||||
for (let i = 0; i <= daysToCheck; i++) {
|
||||
const checkDate = new SvelteDate(now);
|
||||
const checkDate = new Date(now);
|
||||
checkDate.setDate(now.getDate() + i);
|
||||
const dateStr = checkDate.toLocaleDateString('en-CA', { timeZone: 'Europe/London' });
|
||||
const calDate = new CalendarDate(
|
||||
@@ -757,7 +757,7 @@
|
||||
return;
|
||||
}
|
||||
|
||||
const now = new SvelteDate();
|
||||
const now = new Date();
|
||||
const diff = reservationExpiresAt.getTime() - now.getTime();
|
||||
|
||||
if (diff <= 0) {
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import { apiFetch } from '$lib/utils/api';
|
||||
import { toast } from 'svelte-sonner';
|
||||
import { extractErrorMessage } from '$lib/utils/toast-safe';
|
||||
import { SvelteDate } from 'svelte/reactivity';
|
||||
import { parseWallClockDate } from '$lib/utils/timeSlots';
|
||||
import * as Modal from '$lib/components/ui/dialog';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import { Checkbox } from '$lib/components/ui/checkbox';
|
||||
@@ -48,7 +48,7 @@
|
||||
const balanceDue = $derived(selectedBooking ? computeBalanceDue(selectedBooking) : 0);
|
||||
const hoursUntilAppt = $derived(
|
||||
selectedBooking
|
||||
? (new SvelteDate(selectedBooking.start_time).getTime() - new SvelteDate().getTime()) /
|
||||
? (parseWallClockDate(selectedBooking.start_time).getTime() - new Date().getTime()) /
|
||||
(1000 * 60 * 60)
|
||||
: Infinity
|
||||
);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { apiFetch } from '$lib/utils/api';
|
||||
import { SvelteDate, SvelteURLSearchParams } from 'svelte/reactivity';
|
||||
import { SvelteURLSearchParams } from 'svelte/reactivity';
|
||||
import { parseWallClockDate } from '$lib/utils/timeSlots';
|
||||
import { toast } from 'svelte-sonner';
|
||||
import { extractErrorMessage } from '$lib/utils/toast-safe';
|
||||
|
||||
@@ -117,10 +118,10 @@
|
||||
|
||||
// Format booking date/time
|
||||
function formatBookingDateTime(startTime: string): string {
|
||||
const date = new SvelteDate(startTime);
|
||||
const now = new SvelteDate();
|
||||
const today = new SvelteDate(now.getFullYear(), now.getMonth(), now.getDate());
|
||||
const bookingDate = new SvelteDate(date.getFullYear(), date.getMonth(), date.getDate());
|
||||
const date = parseWallClockDate(startTime);
|
||||
const now = new Date();
|
||||
const today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
|
||||
const bookingDate = new Date(date.getFullYear(), date.getMonth(), date.getDate());
|
||||
const daysDiff = Math.floor((bookingDate.getTime() - today.getTime()) / (1000 * 60 * 60 * 24));
|
||||
|
||||
const days = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];
|
||||
|
||||
@@ -143,7 +143,9 @@
|
||||
if (value == null || value === '') return 'Required';
|
||||
const num = Number(value);
|
||||
if (isNaN(num)) return 'Must be a number';
|
||||
if (num < 1) return 'Must be at least 1';
|
||||
// Legal floor: CMA guidance flags sub-12-month expiry windows as an
|
||||
// unfair contract term under the Consumer Rights Act 2015.
|
||||
if (num < 12) return 'Must be at least 12 months';
|
||||
return '';
|
||||
}
|
||||
|
||||
@@ -385,6 +387,10 @@
|
||||
<div>
|
||||
<span class="text-xs text-muted-foreground">Expiry Period</span>
|
||||
<p class="text-sm font-medium">{settings.gift_card_expiry_months} months</p>
|
||||
<p class="text-xs text-muted-foreground">
|
||||
Rolling from last use — each balance check, top-up, redemption or payment
|
||||
resets the timer.
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<span class="text-xs text-muted-foreground">Voucher Type</span>
|
||||
@@ -609,11 +615,16 @@
|
||||
<Input
|
||||
id="gift_card_expiry_months"
|
||||
type="number"
|
||||
min="1"
|
||||
min="12"
|
||||
bind:value={form.gift_card_expiry_months}
|
||||
oninput={() => validateField('gift_card_expiry_months')}
|
||||
onblur={() => validateField('gift_card_expiry_months')}
|
||||
/>
|
||||
<p class="text-xs text-muted-foreground">
|
||||
Rolling expiry: the timer resets on every use (balance check, top-up, redeem,
|
||||
payment). 24 months is the recommended default; CMA guidance flags sub-12-month
|
||||
windows as an unfair contract term.
|
||||
</p>
|
||||
{#if formErrors.gift_card_expiry_months}
|
||||
<p class="text-xs text-destructive">{formErrors.gift_card_expiry_months}</p>
|
||||
{/if}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
<script lang="ts">
|
||||
import { SvelteDate } from 'svelte/reactivity';
|
||||
import { apiFetch } from '$lib/utils/api';
|
||||
import { range } from '$lib/utils/format';
|
||||
import { toast } from 'svelte-sonner';
|
||||
@@ -92,7 +91,7 @@
|
||||
if (form.discount_percent <= 0 || form.discount_percent > 100) return false;
|
||||
if (form.campaign_type === 'time_based') {
|
||||
if (!form.start_date || !form.end_date) return false;
|
||||
if (new SvelteDate(form.end_date) < new SvelteDate(form.start_date)) return false;
|
||||
if (new Date(form.end_date) < new Date(form.start_date)) return false;
|
||||
}
|
||||
if (form.campaign_type === 'milestone') {
|
||||
if (form.milestone_value <= 0) return false;
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { apiFetch } from '$lib/utils/api';
|
||||
import { SvelteDate } from 'svelte/reactivity';
|
||||
import { toast } from 'svelte-sonner';
|
||||
import { extractErrorMessage } from '$lib/utils/toast-safe';
|
||||
import { formatDuration } from '$lib/utils/format';
|
||||
@@ -280,10 +279,10 @@
|
||||
const maxAvailableDuration = $derived.by(() => {
|
||||
if (!booking?.start_time || !nextAppointmentStart) return null;
|
||||
|
||||
const bookingStart = new SvelteDate(booking.start_time).getTime();
|
||||
const bookingStart = parseWallClockDate(booking.start_time).getTime();
|
||||
const currentDurationMs = totalDuration * 60 * 1000;
|
||||
const bookingEnd = bookingStart + currentDurationMs;
|
||||
const nextStart = new SvelteDate(nextAppointmentStart).getTime();
|
||||
const nextStart = parseWallClockDate(nextAppointmentStart).getTime();
|
||||
|
||||
const availableMs = nextStart - bookingEnd;
|
||||
return Math.max(0, Math.floor(availableMs / (60 * 1000)));
|
||||
@@ -600,7 +599,7 @@
|
||||
{/if}
|
||||
</div>
|
||||
<div class="mt-1 text-xs text-gray-400">
|
||||
{new SvelteDate(payment.created_at).toLocaleDateString('en-GB', {
|
||||
{parseWallClockDate(payment.created_at).toLocaleDateString('en-GB', {
|
||||
day: 'numeric',
|
||||
month: 'short',
|
||||
year: 'numeric'
|
||||
|
||||
@@ -13,7 +13,8 @@
|
||||
import { isSquareConfigured } from '$lib/square/square';
|
||||
import { range } from '$lib/utils/format';
|
||||
import { formatUserName } from '$lib/utils/nameDisplay';
|
||||
import { SvelteDate, SvelteURLSearchParams } from 'svelte/reactivity';
|
||||
import { parseWallClockDate } from '$lib/utils/timeSlots';
|
||||
import { SvelteURLSearchParams } from 'svelte/reactivity';
|
||||
|
||||
interface GiftCard {
|
||||
id: string;
|
||||
@@ -726,7 +727,7 @@
|
||||
}
|
||||
|
||||
function formatDate(dateStr: string): string {
|
||||
return new SvelteDate(dateStr).toLocaleDateString('en-GB', {
|
||||
return parseWallClockDate(dateStr).toLocaleDateString('en-GB', {
|
||||
day: 'numeric',
|
||||
month: 'short',
|
||||
year: 'numeric',
|
||||
@@ -736,14 +737,14 @@
|
||||
|
||||
function getExpiryDate(lastUsedAt?: string): Date | null {
|
||||
if (!lastUsedAt) return null;
|
||||
const date = new SvelteDate(lastUsedAt);
|
||||
const date = parseWallClockDate(lastUsedAt);
|
||||
date.setMonth(date.getMonth() + 24);
|
||||
return date;
|
||||
}
|
||||
|
||||
function isExpired(lastUsedAt?: string): boolean {
|
||||
const expiry = getExpiryDate(lastUsedAt);
|
||||
return expiry !== null && expiry < new SvelteDate();
|
||||
return expiry !== null && expiry < new Date();
|
||||
}
|
||||
|
||||
// =============== Sorting ===============
|
||||
@@ -802,7 +803,7 @@
|
||||
return (a.amount_remaining - b.amount_remaining) * mul;
|
||||
case 'created':
|
||||
return (
|
||||
(new SvelteDate(a.created_at).getTime() - new SvelteDate(b.created_at).getTime()) * mul
|
||||
(parseWallClockDate(a.created_at).getTime() - parseWallClockDate(b.created_at).getTime()) * mul
|
||||
);
|
||||
case 'status': {
|
||||
const aVal = a.redeemed_by ? 2 : a.amount_remaining === 0 ? 1 : 0;
|
||||
@@ -833,7 +834,7 @@
|
||||
return (a.balance - b.balance) * mul;
|
||||
case 'updated':
|
||||
return (
|
||||
(new SvelteDate(a.updated_at).getTime() - new SvelteDate(b.updated_at).getTime()) * mul
|
||||
(parseWallClockDate(a.updated_at).getTime() - parseWallClockDate(b.updated_at).getTime()) * mul
|
||||
);
|
||||
default:
|
||||
return 0;
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
<script lang="ts">
|
||||
import { SvelteDate } from 'svelte/reactivity';
|
||||
import { toast } from 'svelte-sonner';
|
||||
import { extractErrorMessage, sanitizeText } from '$lib/utils/toast-safe';
|
||||
import { apiFetch } from '$lib/utils/api';
|
||||
@@ -178,9 +177,9 @@
|
||||
}
|
||||
|
||||
function addWeeksToException(fromISO: string, toISO: string, dest: string[]) {
|
||||
const from = new SvelteDate(fromISO + 'T00:00:00Z');
|
||||
const to = new SvelteDate(toISO + 'T00:00:00Z');
|
||||
const first = new SvelteDate(from);
|
||||
const from = new Date(fromISO + 'T00:00:00Z');
|
||||
const to = new Date(toISO + 'T00:00:00Z');
|
||||
const first = new Date(from);
|
||||
const day = first.getDay();
|
||||
const daysToMonday = day === 0 ? -6 : 1 - day;
|
||||
|
||||
@@ -188,8 +187,8 @@
|
||||
first.setDate(first.getDate() + daysToMonday);
|
||||
|
||||
// Add all Mondays in the range
|
||||
for (let d = new SvelteDate(first); d <= to; d.setDate(d.getDate() + 7)) {
|
||||
dest.push(isoDateOf(new SvelteDate(d)));
|
||||
for (let d = new Date(first); d <= to; d.setDate(d.getDate() + 7)) {
|
||||
dest.push(isoDateOf(new Date(d)));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -487,7 +486,7 @@
|
||||
{g.weekStarts
|
||||
?.slice(0, 3)
|
||||
.map((w) =>
|
||||
new SvelteDate(w).toLocaleDateString('en-GB', {
|
||||
new Date(w).toLocaleDateString('en-GB', {
|
||||
day: 'numeric',
|
||||
month: 'short'
|
||||
})
|
||||
@@ -869,7 +868,7 @@
|
||||
<div class="grid grid-cols-2 gap-2 md:grid-cols-3">
|
||||
{#each viewingException.weekStarts as week (week)}
|
||||
<div class="text-sm">
|
||||
Week of {new SvelteDate(week).toLocaleDateString('en-GB', {
|
||||
Week of {new Date(week).toLocaleDateString('en-GB', {
|
||||
day: 'numeric',
|
||||
month: 'short',
|
||||
year: 'numeric'
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { SvelteDate, SvelteSet } from 'svelte/reactivity';
|
||||
import { SvelteSet } from 'svelte/reactivity';
|
||||
import { toast } from 'svelte-sonner';
|
||||
import { extractErrorMessage } from '$lib/utils/toast-safe';
|
||||
import { apiFetch } from '$lib/utils/api';
|
||||
@@ -58,7 +58,7 @@
|
||||
|
||||
const today = getLondonTodayCalendarDate();
|
||||
const minDate = today;
|
||||
const maxDate = new SvelteDate(today.year, today.month - 1, today.day);
|
||||
const maxDate = new Date(today.year, today.month - 1, today.day);
|
||||
maxDate.setMonth(today.month - 1 + 6);
|
||||
const maxCalendarDate = new CalendarDate(
|
||||
maxDate.getFullYear(),
|
||||
@@ -67,7 +67,7 @@
|
||||
);
|
||||
|
||||
const hoursUntilAppointment = $derived(
|
||||
(new SvelteDate(booking.start_time).getTime() - new SvelteDate().getTime()) / (1000 * 60 * 60)
|
||||
(parseWallClockDate(booking.start_time).getTime() - new Date().getTime()) / (1000 * 60 * 60)
|
||||
);
|
||||
const hasPayments = $derived((booking.amount_paid ?? 0) > 0);
|
||||
const showNoticeWarning = $derived(
|
||||
@@ -380,8 +380,8 @@
|
||||
!rescheduleAutoSelectDone
|
||||
) {
|
||||
rescheduleAutoSelectDone = true;
|
||||
const now = new SvelteDate(getLondonTodayCalendarDate().toString() + 'T00:00:00');
|
||||
const maxDateJs = new SvelteDate(
|
||||
const now = new Date(getLondonTodayCalendarDate().toString() + 'T00:00:00');
|
||||
const maxDateJs = new Date(
|
||||
maxCalendarDate.year,
|
||||
maxCalendarDate.month - 1,
|
||||
maxCalendarDate.day
|
||||
@@ -391,7 +391,7 @@
|
||||
);
|
||||
const daysToCheck = Math.min(daysDifference, 180);
|
||||
for (let i = 1; i <= daysToCheck; i++) {
|
||||
const checkDate = new SvelteDate(now);
|
||||
const checkDate = new Date(now);
|
||||
checkDate.setDate(now.getDate() + i);
|
||||
const dateStr = checkDate.toLocaleDateString('en-CA', { timeZone: 'Europe/London' });
|
||||
const calDate = new CalendarDate(
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
<script lang="ts">
|
||||
import { SvelteDate } from 'svelte/reactivity';
|
||||
import { apiFetch } from '$lib/utils/api';
|
||||
import { CalendarDate } from '@internationalized/date';
|
||||
import { toast } from 'svelte-sonner';
|
||||
@@ -123,7 +122,7 @@
|
||||
|
||||
function getWorkingHoursForDate(dateStr: string): WorkingHourRow | null {
|
||||
if (!dateStr || defaultHours.length === 0) return null;
|
||||
const d = new SvelteDate(dateStr + 'T00:00:00Z');
|
||||
const d = new Date(dateStr + 'T00:00:00Z');
|
||||
const jsDay = d.getDay();
|
||||
const weekday = jsDay === 0 ? 6 : jsDay - 1;
|
||||
return defaultHours.find((h) => h.weekday === weekday) ?? null;
|
||||
@@ -186,8 +185,8 @@
|
||||
});
|
||||
|
||||
function formatRelativeTime(iso: string): string {
|
||||
const d = new SvelteDate(iso);
|
||||
const now = new SvelteDate();
|
||||
const d = parseWallClockDate(iso);
|
||||
const now = new Date();
|
||||
const diffMs = d.getTime() - now.getTime();
|
||||
if (diffMs < 0) {
|
||||
const absMin = Math.floor(Math.abs(diffMs) / 60000);
|
||||
@@ -205,7 +204,7 @@
|
||||
|
||||
const sortedBlockers = $derived.by(() => {
|
||||
return [...blockers].sort(
|
||||
(a, b) => new SvelteDate(a.start_time).getTime() - new SvelteDate(b.start_time).getTime()
|
||||
(a, b) => parseWallClockDate(a.start_time).getTime() - parseWallClockDate(b.start_time).getTime()
|
||||
);
|
||||
});
|
||||
|
||||
@@ -297,8 +296,8 @@
|
||||
return;
|
||||
}
|
||||
|
||||
const blockerStart = new SvelteDate(startIso);
|
||||
const blockerEnd = new SvelteDate(endIso);
|
||||
const blockerStart = new Date(startIso);
|
||||
const blockerEnd = new Date(endIso);
|
||||
if (blockerEnd.getTime() <= blockerStart.getTime()) {
|
||||
overlappingBookings = [];
|
||||
hasOverlap = false;
|
||||
@@ -322,8 +321,8 @@
|
||||
const allBookings: OverlappingBooking[] = data.bookings || [];
|
||||
// Client-side filter: only bookings that overlap with the proposed blocker timespan
|
||||
const filtered = allBookings.filter((b) => {
|
||||
const bStart = new SvelteDate(b.start_time);
|
||||
const bEnd = new SvelteDate(bStart.getTime() + b.duration_minutes * 60000);
|
||||
const bStart = parseWallClockDate(b.start_time);
|
||||
const bEnd = new Date(bStart.getTime() + b.duration_minutes * 60000);
|
||||
return bStart < blockerEnd && bEnd > blockerStart;
|
||||
});
|
||||
overlappingBookings = filtered;
|
||||
@@ -774,7 +773,7 @@
|
||||
<p class="text-sm text-gray-400">Select a date first</p>
|
||||
{:else if !selectedWorkingHours.isOpen}
|
||||
<p class="text-sm text-red-500">
|
||||
Closed on {new SvelteDate(newStartDate + 'T00:00:00Z').toLocaleDateString('en-GB', {
|
||||
Closed on {new Date(newStartDate + 'T00:00:00Z').toLocaleDateString('en-GB', {
|
||||
weekday: 'long'
|
||||
})}
|
||||
</p>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { SvelteDate, SvelteURLSearchParams } from 'svelte/reactivity';
|
||||
import { SvelteURLSearchParams } from 'svelte/reactivity';
|
||||
import { toast } from 'svelte-sonner';
|
||||
import { extractErrorMessage } from '$lib/utils/toast-safe';
|
||||
import { apiFetch } from '$lib/utils/api';
|
||||
@@ -325,13 +325,13 @@
|
||||
<div class="font-medium">
|
||||
{#if selectedUser.dateOfBirth}
|
||||
{(() => {
|
||||
const dob = new SvelteDate(selectedUser.dateOfBirth);
|
||||
const dob = new Date(selectedUser.dateOfBirth);
|
||||
const dateStr = dob.toLocaleDateString('en-US', {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric'
|
||||
});
|
||||
const today = new SvelteDate();
|
||||
const today = new Date();
|
||||
let age = today.getFullYear() - dob.getFullYear();
|
||||
const m = today.getMonth() - dob.getMonth();
|
||||
if (m < 0 || (m === 0 && today.getDate() < dob.getDate())) age--;
|
||||
@@ -346,7 +346,7 @@
|
||||
<div class="text-xs text-gray-500">First Visit</div>
|
||||
<div class="font-medium">
|
||||
{#if customerRelationship?.firstVisitDate}
|
||||
{new SvelteDate(customerRelationship.firstVisitDate).toLocaleDateString('en-US', {
|
||||
{new Date(customerRelationship.firstVisitDate).toLocaleDateString('en-US', {
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
day: 'numeric'
|
||||
@@ -360,7 +360,7 @@
|
||||
<div class="text-xs text-gray-500">Last Visit</div>
|
||||
<div class="font-medium">
|
||||
{#if customerRelationship?.lastVisitDate}
|
||||
{new SvelteDate(customerRelationship.lastVisitDate).toLocaleDateString('en-US', {
|
||||
{new Date(customerRelationship.lastVisitDate).toLocaleDateString('en-US', {
|
||||
weekday: 'short',
|
||||
day: 'numeric',
|
||||
month: 'short',
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
import WalkInCreateModal from '$lib/components/admin/WalkInCreateModal.svelte';
|
||||
import { CalendarDate } from '@internationalized/date';
|
||||
import { apiFetch } from '$lib/utils/api';
|
||||
import { SvelteDate } from 'svelte/reactivity';
|
||||
import { onMount, onDestroy } from 'svelte';
|
||||
import { toast } from 'svelte-sonner';
|
||||
import { extractErrorMessage } from '$lib/utils/toast-safe';
|
||||
@@ -28,7 +27,7 @@
|
||||
} | null>(null);
|
||||
let loading = $state(true);
|
||||
let noSlotsToday = $state(false);
|
||||
let currentTime = new SvelteDate();
|
||||
let currentTime = new Date();
|
||||
|
||||
let _reservationId = $state<string | null>(null);
|
||||
let reservationExpiresAt = $state<Date | null>(null);
|
||||
@@ -58,7 +57,7 @@
|
||||
fetchShortestService();
|
||||
|
||||
const interval = setInterval(() => {
|
||||
currentTime = new SvelteDate();
|
||||
currentTime = new Date();
|
||||
}, 60000);
|
||||
|
||||
return () => clearInterval(interval);
|
||||
@@ -176,7 +175,7 @@
|
||||
noSlotsToday = false;
|
||||
|
||||
try {
|
||||
const now = new SvelteDate();
|
||||
const now = new Date();
|
||||
const londonDateStr = new Date().toLocaleDateString('en-CA', { timeZone: 'Europe/London' });
|
||||
const [y, m, d] = londonDateStr.split('-').map(Number);
|
||||
const today = new CalendarDate(y, m, d);
|
||||
@@ -275,7 +274,7 @@
|
||||
const londonDateStr = new Date().toLocaleDateString('en-CA', { timeZone: 'Europe/London' });
|
||||
const [y, m, d] = londonDateStr.split('-').map(Number);
|
||||
const [hours, minutes] = startTime.split(':').map(Number);
|
||||
const start = new SvelteDate(y, m - 1, d, hours, minutes, 0, 0);
|
||||
const start = new Date(y, m - 1, d, hours, minutes, 0, 0);
|
||||
const startTimeISO = formatLocalDateTime(start);
|
||||
|
||||
const response = await apiFetch('/api/admin/bookings/reserve', {
|
||||
@@ -329,7 +328,7 @@
|
||||
return;
|
||||
}
|
||||
|
||||
const now = new SvelteDate();
|
||||
const now = new Date();
|
||||
const diff = reservationExpiresAt.getTime() - now.getTime();
|
||||
|
||||
if (diff <= 0) {
|
||||
@@ -369,7 +368,7 @@
|
||||
const liveRemaining = getLiveRemainingMinutes() ?? 0;
|
||||
if (liveRemaining > RESERVATION_TTL) {
|
||||
// Available now with >15min remaining — reserve from now to slot end
|
||||
const now = new SvelteDate();
|
||||
const now = new Date();
|
||||
const currentMin = now.getHours() * 60 + now.getMinutes();
|
||||
reserveTime = `${String(now.getHours()).padStart(2, '0')}:${String(now.getMinutes()).padStart(2, '0')}`;
|
||||
reserveDuration = slotInfo.slotEndMinutes! - currentMin;
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import { apiFetch } from '$lib/utils/api';
|
||||
import { toast } from 'svelte-sonner';
|
||||
import { extractErrorMessage } from '$lib/utils/toast-safe';
|
||||
import { SvelteDate, SvelteURLSearchParams } from 'svelte/reactivity';
|
||||
import { SvelteURLSearchParams } from 'svelte/reactivity';
|
||||
import { isValidUKPhone, toE164UK } from '$lib/utils/phone';
|
||||
import { formatUserName } from '$lib/utils/nameDisplay';
|
||||
import { formatLocalDateTime } from '$lib/utils/timeSlots';
|
||||
@@ -243,7 +243,7 @@
|
||||
return;
|
||||
}
|
||||
|
||||
const now = new SvelteDate();
|
||||
const now = new Date();
|
||||
const diff = reservationExpiresAt.getTime() - now.getTime();
|
||||
|
||||
if (diff <= 0) {
|
||||
@@ -479,7 +479,7 @@
|
||||
const [hours, minutes] = availableStartTime.split(':').map(Number);
|
||||
const londonDateStr = new Date().toLocaleDateString('en-CA', { timeZone: 'Europe/London' });
|
||||
const [y, m, d] = londonDateStr.split('-').map(Number);
|
||||
start = new SvelteDate(y, m - 1, d, hours, minutes, 0, 0);
|
||||
start = new Date(y, m - 1, d, hours, minutes, 0, 0);
|
||||
} else {
|
||||
// Fallback: Calculate immediate start time (rounded to next 15 min)
|
||||
const londonDateStr = new Date().toLocaleDateString('en-CA', { timeZone: 'Europe/London' });
|
||||
@@ -491,8 +491,8 @@
|
||||
});
|
||||
const [y, m, d] = londonDateStr.split('-').map(Number);
|
||||
const [h, min] = londonTimeStr.split(':').map(Number);
|
||||
const now = new SvelteDate(y, m - 1, d, h, min, 0, 0);
|
||||
start = new SvelteDate(now);
|
||||
const now = new Date(y, m - 1, d, h, min, 0, 0);
|
||||
start = new Date(now);
|
||||
const minutes = start.getMinutes();
|
||||
const remainder = 15 - (minutes % 15);
|
||||
if (remainder !== 15 && remainder !== 0) {
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
<script lang="ts">
|
||||
import { SvelteDate } from 'svelte/reactivity';
|
||||
import { toast } from 'svelte-sonner';
|
||||
import { extractErrorMessage } from '$lib/utils/toast-safe';
|
||||
import { browser } from '$app/environment';
|
||||
@@ -229,7 +228,7 @@
|
||||
}
|
||||
|
||||
function getDefaultEffectiveDate(): string {
|
||||
const d = new SvelteDate();
|
||||
const d = new Date();
|
||||
d.setDate(d.getDate() + 1);
|
||||
return d.toISOString().slice(0, 10);
|
||||
}
|
||||
|
||||
@@ -27,7 +27,6 @@
|
||||
import { toast } from 'svelte-sonner';
|
||||
import { extractErrorMessage } from '$lib/utils/toast-safe';
|
||||
import { onDestroy } from 'svelte';
|
||||
import { SvelteDate } from 'svelte/reactivity';
|
||||
|
||||
// Components
|
||||
import BookingActions from '$lib/components/booking/BookingActions.svelte';
|
||||
@@ -41,7 +40,7 @@
|
||||
import { POLICY } from '$lib/constants/policy';
|
||||
import UserPaymentModal from '$lib/components/payments/UserPaymentModal.svelte';
|
||||
import { extractBookedSlots, getLunchProtectionForSlots } from '$lib/lunchProtection';
|
||||
import { formatLocalDateTime, getLondonTodayCalendarDate } from '$lib/utils/timeSlots';
|
||||
import { formatLocalDateTime, getLondonTodayCalendarDate, parseWallClockDate } from '$lib/utils/timeSlots';
|
||||
import { generateUUID } from '$lib/utils/uuid';
|
||||
|
||||
import type {
|
||||
@@ -575,7 +574,7 @@
|
||||
return;
|
||||
}
|
||||
|
||||
const now = new SvelteDate();
|
||||
const now = new Date();
|
||||
const diff = reservationExpiresAt.getTime() - now.getTime();
|
||||
|
||||
if (diff <= 0) {
|
||||
@@ -685,7 +684,7 @@
|
||||
// Initialize date boundaries
|
||||
const today = getLondonTodayCalendarDate();
|
||||
const minDate = today;
|
||||
const maxDate = new SvelteDate(today.year, today.month - 1, today.day);
|
||||
const maxDate = new Date(today.year, today.month - 1, today.day);
|
||||
maxDate.setMonth(today.month - 1 + 6);
|
||||
const maxCalendarDate = new CalendarDate(
|
||||
maxDate.getFullYear(),
|
||||
@@ -756,8 +755,8 @@
|
||||
) {
|
||||
bookingFlowAutoSelectDone = true;
|
||||
|
||||
const currentDate = new SvelteDate(getLondonTodayCalendarDate().toString() + 'T00:00:00');
|
||||
const maxDateJs = new SvelteDate(
|
||||
const currentDate = new Date(getLondonTodayCalendarDate().toString() + 'T00:00:00');
|
||||
const maxDateJs = new Date(
|
||||
maxCalendarDate.year,
|
||||
maxCalendarDate.month - 1,
|
||||
maxCalendarDate.day
|
||||
@@ -769,7 +768,7 @@
|
||||
const daysToCheck = Math.min(daysDifference, 180);
|
||||
|
||||
for (let i = 1; i <= daysToCheck; i++) {
|
||||
const nextDate = new SvelteDate(currentDate);
|
||||
const nextDate = new Date(currentDate);
|
||||
nextDate.setDate(currentDate.getDate() + i);
|
||||
const dateStr = nextDate.toLocaleDateString('en-CA', { timeZone: 'Europe/London' });
|
||||
|
||||
@@ -992,7 +991,7 @@
|
||||
// =============== Time Slot Generation ===============
|
||||
function calculateEndTime(startTime: string, durationMinutes: number): string {
|
||||
const [hours, minutes] = startTime.split(':').map(Number);
|
||||
const date = new SvelteDate();
|
||||
const date = new Date();
|
||||
date.setHours(hours, minutes, 0, 0);
|
||||
date.setMinutes(date.getMinutes() + durationMinutes);
|
||||
const endHours = date.getHours().toString().padStart(2, '0');
|
||||
@@ -1253,7 +1252,7 @@
|
||||
15,
|
||||
false
|
||||
);
|
||||
const now = new SvelteDate();
|
||||
const now = new Date();
|
||||
const validSlots = availableSlots.filter((t) => {
|
||||
if (lunchProtection.get(t)?.isBlocked) return false;
|
||||
if (userDepositsRequired > 0) {
|
||||
@@ -1410,7 +1409,7 @@
|
||||
}
|
||||
|
||||
function getDayWithOrdinal(date: CalendarDate): string {
|
||||
const monthName = new SvelteDate(date.year, date.month - 1, date.day).toLocaleDateString(
|
||||
const monthName = new Date(date.year, date.month - 1, date.day).toLocaleDateString(
|
||||
'en-GB',
|
||||
{
|
||||
month: 'long'
|
||||
@@ -2131,7 +2130,7 @@
|
||||
{#if currentStep === finalStep}
|
||||
{#if confirmedBooking}
|
||||
{@const isRequested = confirmedBooking.notes && confirmedBooking.notes.length > 0}
|
||||
{@const bookingDate = new SvelteDate(confirmedBooking.start_time)}
|
||||
{@const bookingDate = parseWallClockDate(confirmedBooking.start_time)}
|
||||
{@const dateStr = bookingDate.toLocaleDateString('en-GB', {
|
||||
weekday: 'long',
|
||||
day: 'numeric',
|
||||
@@ -2446,8 +2445,8 @@
|
||||
deposit_paid: bk.deposit_paid,
|
||||
payments: bk.payments,
|
||||
duration_minutes: bk.duration_minutes,
|
||||
created_at: new SvelteDate().toISOString(),
|
||||
updated_at: new SvelteDate().toISOString()
|
||||
created_at: new Date().toISOString(),
|
||||
updated_at: new Date().toISOString()
|
||||
}}
|
||||
onClose={() => (showPayEarlyModal = false)}
|
||||
onComplete={() => {
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
<script lang="ts">
|
||||
import { SvelteDate } from 'svelte/reactivity';
|
||||
import { onMount, onDestroy } from 'svelte';
|
||||
import { formatTime } from '$lib/utils/timeSlots';
|
||||
|
||||
@@ -81,7 +80,7 @@
|
||||
const today = getLondonDate();
|
||||
const dayOfWeek = today.getDay(); // 0=Sun
|
||||
const offset = dayOfWeek === 0 ? -6 : 1 - dayOfWeek;
|
||||
const monday = new SvelteDate(today);
|
||||
const monday = new Date(today);
|
||||
monday.setDate(today.getDate() + offset);
|
||||
return fmtDate(monday);
|
||||
}
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
import { goto } from '$app/navigation';
|
||||
import { toast } from 'svelte-sonner';
|
||||
import { extractErrorMessage } from '$lib/utils/toast-safe';
|
||||
import { SvelteDate } from 'svelte/reactivity';
|
||||
import { authStore } from '$lib/stores/auth.svelte';
|
||||
import { apiFetch } from '$lib/utils/api';
|
||||
import { parseWallClockDate } from '$lib/utils/timeSlots';
|
||||
import CardSelection from '$lib/components/payments/CardSelection.svelte';
|
||||
import { savedCardsStore, type SavedCard } from '$lib/stores/savedCards.svelte';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
@@ -110,7 +110,7 @@
|
||||
});
|
||||
|
||||
function formatDate(dateStr: string): string {
|
||||
const date = new SvelteDate(dateStr);
|
||||
const date = parseWallClockDate(dateStr);
|
||||
return date.toLocaleDateString('en-GB', {
|
||||
weekday: 'long',
|
||||
day: 'numeric',
|
||||
@@ -127,7 +127,7 @@
|
||||
services: BookingService[],
|
||||
fallbackDuration: number
|
||||
): string {
|
||||
const start = new SvelteDate(startStr);
|
||||
const start = parseWallClockDate(startStr);
|
||||
const totalMinutes =
|
||||
services?.reduce(
|
||||
(sum, s) => sum + (s.override_duration_minutes ?? s.duration_minutes ?? 0),
|
||||
@@ -135,7 +135,7 @@
|
||||
) ??
|
||||
fallbackDuration ??
|
||||
0;
|
||||
const end = new SvelteDate(start.getTime() + totalMinutes * 60000);
|
||||
const end = new Date(start.getTime() + totalMinutes * 60000);
|
||||
|
||||
const formatOpt: Intl.DateTimeFormatOptions = {
|
||||
hour: 'numeric',
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
<script lang="ts">
|
||||
import { apiFetch } from '$lib/utils/api';
|
||||
import { ensureBusinessInfo, getBusinessInfo } from '$lib/stores/businessInfo.svelte';
|
||||
import { SvelteDate } from 'svelte/reactivity';
|
||||
import { toast } from 'svelte-sonner';
|
||||
import { formatDuration } from '$lib/utils/format';
|
||||
import { formatUserName } from '$lib/utils/nameDisplay';
|
||||
import { parseWallClockDate } from '$lib/utils/timeSlots';
|
||||
import * as Card from '$lib/components/ui/card';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import { Badge } from '$lib/components/ui/badge';
|
||||
@@ -106,19 +106,19 @@
|
||||
const minutesUntilClosing = $derived.by(() => {
|
||||
if (!closingTime) return 0;
|
||||
const [ch, cm] = closingTime.split(':').map(Number);
|
||||
const now = new SvelteDate();
|
||||
const closing = new SvelteDate(now.getFullYear(), now.getMonth(), now.getDate(), ch, cm);
|
||||
const now = new Date();
|
||||
const closing = new Date(now.getFullYear(), now.getMonth(), now.getDate(), ch, cm);
|
||||
return Math.max(0, Math.floor((closing.getTime() - now.getTime()) / 60000));
|
||||
});
|
||||
|
||||
function calculateTimes() {
|
||||
const now = new SvelteDate();
|
||||
const now = new Date();
|
||||
|
||||
if (currentAppointment) {
|
||||
isInProgress = currentAppointment.status === 'in_progress';
|
||||
const startTime = new SvelteDate(currentAppointment.start_time);
|
||||
const startTime = parseWallClockDate(currentAppointment.start_time);
|
||||
|
||||
const endTime = new SvelteDate(
|
||||
const endTime = new Date(
|
||||
startTime.getTime() + currentAppointment.duration_minutes * 60 * 1000
|
||||
);
|
||||
|
||||
@@ -137,15 +137,15 @@
|
||||
|
||||
let rawFreeMinutes = 0;
|
||||
if (nextAppointment) {
|
||||
const nextStart = new SvelteDate(nextAppointment.start_time);
|
||||
const nextStart = parseWallClockDate(nextAppointment.start_time);
|
||||
const gapMs = nextStart.getTime() - endTime.getTime();
|
||||
rawFreeMinutes = Math.max(0, Math.floor(gapMs / 60000));
|
||||
}
|
||||
|
||||
if (closingTime) {
|
||||
const [ch, cm] = closingTime.split(':').map(Number);
|
||||
const today = new SvelteDate();
|
||||
const closing = new SvelteDate(
|
||||
const today = new Date();
|
||||
const closing = new Date(
|
||||
today.getFullYear(),
|
||||
today.getMonth(),
|
||||
today.getDate(),
|
||||
@@ -325,14 +325,14 @@
|
||||
</Card.Title>
|
||||
{#if activeAppointment}
|
||||
<Card.Description class="text-base">
|
||||
{new SvelteDate(activeAppointment.start_time).toLocaleTimeString('en-US', {
|
||||
{parseWallClockDate(activeAppointment.start_time).toLocaleTimeString('en-US', {
|
||||
hour: 'numeric',
|
||||
minute: '2-digit',
|
||||
hour12: true
|
||||
})}
|
||||
-
|
||||
{new SvelteDate(
|
||||
new SvelteDate(activeAppointment.start_time).getTime() +
|
||||
{new Date(
|
||||
parseWallClockDate(activeAppointment.start_time).getTime() +
|
||||
activeAppointment.duration_minutes * 60 * 1000
|
||||
).toLocaleTimeString('en-US', {
|
||||
hour: 'numeric',
|
||||
@@ -439,13 +439,13 @@
|
||||
|
||||
{#if summary.summary_scope === 'week'}
|
||||
<div class="text-xs text-gray-500">
|
||||
Summary: {new SvelteDate(summary.summary_start_date).toLocaleDateString('en-US', {
|
||||
Summary: {parseWallClockDate(summary.summary_start_date).toLocaleDateString('en-US', {
|
||||
weekday: 'short',
|
||||
month: 'short',
|
||||
day: 'numeric'
|
||||
})}
|
||||
–
|
||||
{new SvelteDate(summary.summary_end_date).toLocaleDateString('en-US', {
|
||||
{parseWallClockDate(summary.summary_end_date).toLocaleDateString('en-US', {
|
||||
weekday: 'short',
|
||||
month: 'short',
|
||||
day: 'numeric'
|
||||
@@ -552,7 +552,7 @@
|
||||
<div>
|
||||
<h4 class="mb-2 text-sm font-semibold text-gray-700">
|
||||
New bookings made since{summary.summary_scope === 'week'
|
||||
? ` opening ${new SvelteDate(summary.summary_start_date).toLocaleDateString('en-US', { weekday: 'long' })}`
|
||||
? ` opening ${parseWallClockDate(summary.summary_start_date).toLocaleDateString('en-US', { weekday: 'long' })}`
|
||||
: ' closing yesterday'}
|
||||
</h4>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
@@ -573,13 +573,13 @@
|
||||
<div>
|
||||
<h4 class="text-sm font-semibold text-gray-700">This week so far</h4>
|
||||
<div class="text-xs text-gray-500">
|
||||
{new SvelteDate(weekSummary.summary_start_date).toLocaleDateString('en-US', {
|
||||
{parseWallClockDate(weekSummary.summary_start_date).toLocaleDateString('en-US', {
|
||||
weekday: 'short',
|
||||
month: 'short',
|
||||
day: 'numeric'
|
||||
})}
|
||||
–
|
||||
{new SvelteDate(weekSummary.summary_end_date).toLocaleDateString('en-US', {
|
||||
{parseWallClockDate(weekSummary.summary_end_date).toLocaleDateString('en-US', {
|
||||
weekday: 'short',
|
||||
month: 'short',
|
||||
day: 'numeric'
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { apiFetch } from '$lib/utils/api';
|
||||
import { SvelteDate } from 'svelte/reactivity';
|
||||
import { toast } from 'svelte-sonner';
|
||||
import * as Card from '$lib/components/ui/card';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
@@ -10,6 +9,7 @@
|
||||
import EditRequestModal from '$lib/components/admin/EditRequestModal.svelte';
|
||||
import { range } from '$lib/utils/format';
|
||||
import { formatUserName } from '$lib/utils/nameDisplay';
|
||||
import { parseWallClockDate } from '$lib/utils/timeSlots';
|
||||
|
||||
interface Props {
|
||||
openBookingModal?: (bookingId: string) => void;
|
||||
@@ -105,7 +105,7 @@
|
||||
|
||||
// Helper function to format date nicely
|
||||
function formatDateTime(dateTimeString: string): string {
|
||||
const date = new SvelteDate(dateTimeString);
|
||||
const date = parseWallClockDate(dateTimeString);
|
||||
const dateStr = date.toLocaleDateString('en-US', {
|
||||
weekday: 'long',
|
||||
day: 'numeric',
|
||||
@@ -120,8 +120,8 @@
|
||||
}
|
||||
|
||||
function formatRelativeTime(iso: string): string {
|
||||
const d = new SvelteDate(iso);
|
||||
const now = new SvelteDate();
|
||||
const d = parseWallClockDate(iso);
|
||||
const now = new Date();
|
||||
const diffMs = now.getTime() - d.getTime();
|
||||
const diffMin = Math.floor(diffMs / 60000);
|
||||
|
||||
@@ -138,7 +138,7 @@
|
||||
const servicesChanged = areEditServicesChanged(er);
|
||||
|
||||
if (timeChanged) {
|
||||
const d = new SvelteDate(er.proposed.start_time!);
|
||||
const d = parseWallClockDate(er.proposed.start_time!);
|
||||
const dateStr = d.toLocaleDateString('en-US', {
|
||||
weekday: 'long',
|
||||
day: 'numeric',
|
||||
@@ -187,7 +187,8 @@
|
||||
const data = await response.json();
|
||||
const newApprovals = (data.approvals || []).sort(
|
||||
(a: PendingApproval, b: PendingApproval) =>
|
||||
new SvelteDate(a.created_at).getTime() - new SvelteDate(b.created_at).getTime()
|
||||
parseWallClockDate(a.created_at).getTime() -
|
||||
parseWallClockDate(b.created_at).getTime()
|
||||
);
|
||||
const newJson = JSON.stringify(newApprovals);
|
||||
if (newJson !== prevApprovalsJson) {
|
||||
@@ -218,7 +219,8 @@
|
||||
const data = await response.json();
|
||||
const newEditRequests = (data.edit_requests || []).sort(
|
||||
(a: EditRequest, b: EditRequest) =>
|
||||
new SvelteDate(a.requested_at).getTime() - new SvelteDate(b.requested_at).getTime()
|
||||
parseWallClockDate(a.requested_at).getTime() -
|
||||
parseWallClockDate(b.requested_at).getTime()
|
||||
);
|
||||
const newJson = JSON.stringify(newEditRequests);
|
||||
if (newJson !== prevEditRequestsJson) {
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { apiFetch } from '$lib/utils/api';
|
||||
import { SvelteDate } from 'svelte/reactivity';
|
||||
import { CalendarDate } from '@internationalized/date';
|
||||
import { toast } from 'svelte-sonner';
|
||||
import { extractErrorMessage, sanitizeText } from '$lib/utils/toast-safe';
|
||||
@@ -22,7 +21,7 @@
|
||||
timeToMinutes
|
||||
} from '$lib/lunchProtection';
|
||||
import { formatDuration, formatDateISO } from '$lib/utils/format';
|
||||
import { formatLocalDateTime } from '$lib/utils/timeSlots';
|
||||
import { formatLocalDateTime, parseWallClockDate } from '$lib/utils/timeSlots';
|
||||
|
||||
interface Props {
|
||||
openBookingModal: (bookingId: string) => void;
|
||||
@@ -83,8 +82,8 @@
|
||||
};
|
||||
|
||||
function isPastAppointment(startTime: string, durationMinutes: number): boolean {
|
||||
const start = new SvelteDate(startTime);
|
||||
const end = new SvelteDate(start.getTime() + durationMinutes * 60_000);
|
||||
const start = parseWallClockDate(startTime);
|
||||
const end = new Date(start.getTime() + durationMinutes * 60_000);
|
||||
return end.getTime() < Date.now();
|
||||
}
|
||||
|
||||
@@ -118,7 +117,7 @@
|
||||
|
||||
const weekStartStr = $derived.by(() => {
|
||||
const londonDateStr = new Date().toLocaleDateString('en-CA', { timeZone: 'Europe/London' });
|
||||
const d = new SvelteDate(londonDateStr + 'T00:00:00Z');
|
||||
const d = new Date(londonDateStr + 'T00:00:00Z');
|
||||
const day = d.getDay();
|
||||
const diff = day === 0 ? 6 : day - 1;
|
||||
d.setDate(d.getDate() - diff);
|
||||
@@ -126,7 +125,7 @@
|
||||
});
|
||||
|
||||
const weekEndStr = $derived.by(() => {
|
||||
const d = new SvelteDate(weekStartStr + 'T00:00:00Z');
|
||||
const d = new Date(weekStartStr + 'T00:00:00Z');
|
||||
d.setDate(d.getDate() + 6);
|
||||
return formatDateISO(d);
|
||||
});
|
||||
@@ -286,7 +285,7 @@
|
||||
const items: TimelineItem[] = [];
|
||||
|
||||
for (const apt of appointments) {
|
||||
const start = new SvelteDate(apt.start_time);
|
||||
const start = parseWallClockDate(apt.start_time);
|
||||
const startM = start.getHours() * 60 + start.getMinutes();
|
||||
items.push({
|
||||
id: `apt-${apt.id}`,
|
||||
@@ -298,7 +297,7 @@
|
||||
}
|
||||
|
||||
for (const b of blockers) {
|
||||
const start = new SvelteDate(b.start_time);
|
||||
const start = parseWallClockDate(b.start_time);
|
||||
const startM = start.getHours() * 60 + start.getMinutes();
|
||||
items.push({
|
||||
id: `blk-${b.id}`,
|
||||
@@ -440,8 +439,8 @@
|
||||
hasOverlap = false;
|
||||
return;
|
||||
}
|
||||
const blockerStart = new SvelteDate(startIso);
|
||||
const blockerEnd = new SvelteDate(endIso);
|
||||
const blockerStart = new Date(startIso);
|
||||
const blockerEnd = new Date(endIso);
|
||||
if (blockerEnd.getTime() <= blockerStart.getTime()) {
|
||||
overlappingBookings = [];
|
||||
hasOverlap = false;
|
||||
@@ -459,8 +458,8 @@
|
||||
const data = await response.json();
|
||||
const allBookings: OverlappingBooking[] = data.bookings || [];
|
||||
const filtered = allBookings.filter((b) => {
|
||||
const bStart = new SvelteDate(b.start_time);
|
||||
const bEnd = new SvelteDate(bStart.getTime() + b.duration_minutes * 60000);
|
||||
const bStart = parseWallClockDate(b.start_time);
|
||||
const bEnd = new Date(bStart.getTime() + b.duration_minutes * 60000);
|
||||
return bStart < blockerEnd && bEnd > blockerStart;
|
||||
});
|
||||
overlappingBookings = filtered;
|
||||
@@ -627,7 +626,7 @@
|
||||
}
|
||||
|
||||
function formatTime(dateString: string): string {
|
||||
const date = new SvelteDate(dateString);
|
||||
const date = parseWallClockDate(dateString);
|
||||
return date.toLocaleTimeString('en-US', {
|
||||
hour: 'numeric',
|
||||
minute: '2-digit',
|
||||
@@ -1011,7 +1010,7 @@
|
||||
)}
|
||||
</div>
|
||||
<div class="text-xs text-gray-500">
|
||||
{new SvelteDate(booking.start_time).toLocaleTimeString('en-GB', {
|
||||
{parseWallClockDate(booking.start_time).toLocaleTimeString('en-GB', {
|
||||
hour: 'numeric',
|
||||
minute: '2-digit',
|
||||
hour12: true
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
<script lang="ts">
|
||||
import { SvelteDate } from 'svelte/reactivity';
|
||||
import { apiFetch } from '$lib/utils/api';
|
||||
import { Skeleton } from '$lib/components/ui/skeleton';
|
||||
import * as Card from '$lib/components/ui/card';
|
||||
@@ -51,11 +50,11 @@
|
||||
|
||||
async function findLastWorkingDayClose(): Promise<{ cutoff: string; spansClosed: boolean }> {
|
||||
const londonDateStr = new Date().toLocaleDateString('en-CA', { timeZone: 'Europe/London' });
|
||||
const now = new SvelteDate(londonDateStr + 'T00:00:00Z');
|
||||
const now = new Date(londonDateStr + 'T00:00:00Z');
|
||||
let spansClosed = false;
|
||||
|
||||
for (let i = 1; i <= 14; i++) {
|
||||
const d = new SvelteDate(now);
|
||||
const d = new Date(now);
|
||||
d.setDate(d.getDate() - i);
|
||||
const dateStr = `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`;
|
||||
|
||||
@@ -72,7 +71,7 @@
|
||||
if (day?.isOpen) {
|
||||
const closeTime = day.endTime;
|
||||
const [h, m] = closeTime.split(':').map(Number);
|
||||
const closeDate = new SvelteDate(d);
|
||||
const closeDate = new Date(d);
|
||||
closeDate.setHours(h, m, 0, 0);
|
||||
return { cutoff: formatLocalDateTime(closeDate), spansClosed };
|
||||
} else {
|
||||
@@ -81,7 +80,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
const fallback = new SvelteDate(now);
|
||||
const fallback = new Date(now);
|
||||
fallback.setDate(fallback.getDate() - 1);
|
||||
fallback.setHours(17, 0, 0, 0);
|
||||
return { cutoff: formatLocalDateTime(fallback), spansClosed };
|
||||
@@ -144,7 +143,7 @@
|
||||
}
|
||||
|
||||
const { cutoff, spansClosed } = await findLastWorkingDayClose();
|
||||
const nowISO = formatLocalDateTime(new SvelteDate());
|
||||
const nowISO = formatLocalDateTime(new Date());
|
||||
let bookingsMade = 0;
|
||||
try {
|
||||
const bmRes = await apiFetch(
|
||||
|
||||
Reference in New Issue
Block a user