style: fix prefer-const and prettier formatting issues
Backend CI / Tests (push) Failing after 1m41s
Backend CI / Lint & vulns (push) Failing after 2m26s
Backend CI / Race detector (push) Failing after 3m45s

This commit is contained in:
2026-06-25 13:48:03 +01:00
parent e0236144c7
commit 4af2b8dfb4
64 changed files with 234 additions and 229 deletions
@@ -89,57 +89,57 @@
);
let placeholderDate = $state<CalendarDate>(minDate);
let hoursUntilAppointment = $derived(
const hoursUntilAppointment = $derived(
(new SvelteDate(booking.start_time).getTime() - new SvelteDate().getTime()) / (1000 * 60 * 60)
);
let hasPayments = $derived((booking.amount_paid ?? 0) > 0);
let noticePeriodBlocked = $derived(
const hasPayments = $derived((booking.amount_paid ?? 0) > 0);
const noticePeriodBlocked = $derived(
hasPayments ? hoursUntilAppointment < 72 : hoursUntilAppointment < 24
);
let noticePeriodWarning = $derived(
const noticePeriodWarning = $derived(
!hasPayments && hoursUntilAppointment < 24 && hoursUntilAppointment >= 0
);
let noticeBlockedMessage = $derived(
const noticeBlockedMessage = $derived(
hasPayments
? 'This booking has payments and is too close to the original appointment to reschedule online.'
: 'This booking is too close to the original appointment time to reschedule online.'
);
let discountTotal = $derived(
const discountTotal = $derived(
booking.discounts?.reduce((sum, d) => sum + d.discount_amount, 0) ?? 0
);
let bookingTotalDuration = $derived(
const bookingTotalDuration = $derived(
booking.services?.reduce(
(sum, s) => sum + (s.override_duration_minutes ?? s.duration_minutes ?? 0),
0
) || 0
);
let selectedServicesDuration = $derived(
const selectedServicesDuration = $derived(
selectedServices.reduce((sum, s) => sum + (s.duration_minutes ?? 0), 0)
);
let hasOverrides = $derived(
const hasOverrides = $derived(
booking?.services?.some(
(s) => s.override_price != null || s.override_duration_minutes != null
) ?? false
);
let slotDuration = $derived(
const slotDuration = $derived(
editMode === 'time' ? bookingTotalDuration : selectedServicesDuration
);
let canSubmit = $derived(
const canSubmit = $derived(
submitting === false &&
((editMode === 'time' && !!newDate && newTime.length >= 4) ||
(editMode === 'services' && selectedServices.length > 0) ||
(editMode === 'both-time' && !!newDate && newTime.length >= 4))
);
let notesChanged = $derived(notes.trim() !== originalNotes.trim());
const notesChanged = $derived(notes.trim() !== originalNotes.trim());
let modalTitle = $derived(() => {
const modalTitle = $derived(() => {
switch (editMode) {
case 'select':
return 'Edit/Reschedule';
@@ -193,7 +193,7 @@
function calculateEndTime(startTime: string, durationMinutes: number): string {
const [hours, minutes] = startTime.split(':').map(Number);
let total = hours * 60 + minutes + durationMinutes;
const total = hours * 60 + minutes + durationMinutes;
const h = Math.floor(total / 60);
const m = total % 60;
return `${String(h).padStart(2, '0')}:${String(m).padStart(2, '0')}`;
@@ -201,7 +201,7 @@
function calculatePreviousTime(time: string): string {
const [h, m] = time.split(':').map(Number);
let total = h * 60 + m - 15;
const total = h * 60 + m - 15;
return `${String(Math.floor(total / 60)).padStart(2, '0')}:${String(total % 60).padStart(2, '0')}`;
}
@@ -654,7 +654,7 @@
return Math.max(0, Math.min(nextBookingStart, workingEndMinutes) - bookingEndMinutes);
}
let availableAdditionalServices = $derived(() => {
const availableAdditionalServices = $derived(() => {
if (editMode !== 'services') return [];
const remaining = calculateRemainingTime();
if (remaining <= 0) return [];
@@ -21,7 +21,7 @@
let { open = $bindable(), bookingId }: Props = $props();
let selectedBooking = $state<Booking | null>(null);
let businessSettings = $derived(getBusinessInfo());
const businessSettings = $derived(getBusinessInfo());
let loading = $state(false);
let hasPendingEditRequest = $state(false);
let pendingEditRequest = $state<{
@@ -44,80 +44,80 @@
// IMPORTANT: Use override_duration_minutes when present — services may have been
// customised at booking time. Showing base values misleads users about what was booked.
let totalDuration = $derived(
const totalDuration = $derived(
selectedBooking?.services?.reduce(
(sum, service) => sum + (service.override_duration_minutes ?? service.duration_minutes ?? 0),
0
) || 0
);
let isFutureBooking = $derived(
const isFutureBooking = $derived(
selectedBooking ? new SvelteDate(selectedBooking.start_time) > new SvelteDate() : false
);
let hasPayments = $derived(
const hasPayments = $derived(
selectedBooking && selectedBooking.payments && selectedBooking.payments.length > 0
);
let isCancellable = $derived(
const isCancellable = $derived(
selectedBooking && isFutureBooking && ['pending', 'confirmed'].includes(selectedBooking.status)
);
let canEditBooking = $derived(isCancellable);
const canEditBooking = $derived(isCancellable);
let totalPaid = $derived(
const totalPaid = $derived(
selectedBooking?.payments
?.filter((p) => p.status === 'completed')
.reduce((sum, p) => sum + p.amount, 0) || 0
);
let totalRefunds = $derived(
const totalRefunds = $derived(
(selectedBooking?.refunds ?? [])
.filter((r) => r.status === 'completed')
.reduce((sum, r) => sum + r.amount, 0)
);
let balanceDue = $derived(selectedBooking ? computeBalanceDue(selectedBooking) : 0);
const balanceDue = $derived(selectedBooking ? computeBalanceDue(selectedBooking) : 0);
let totalVAT = $derived(
const totalVAT = $derived(
selectedBooking?.payments
?.filter((p) => p.status === 'completed' && p.vat_amount)
.reduce((sum, p) => sum + (p.vat_amount ?? 0), 0) || 0
);
let totalNet = $derived(
const totalNet = $derived(
selectedBooking?.payments
?.filter((p) => p.status === 'completed' && p.net_amount)
.reduce((sum, p) => sum + (p.net_amount ?? 0), 0) || 0
);
let hasVAT = $derived(totalVAT > 0);
const hasVAT = $derived(totalVAT > 0);
let depositOutstanding = $derived(
const depositOutstanding = $derived(
selectedBooking?.deposit_required && !selectedBooking?.deposit_paid
);
let canPayEarly = $derived(
const canPayEarly = $derived(
selectedBooking &&
!depositOutstanding &&
totalPaid < selectedBooking.total_amount &&
['confirmed', 'in_progress'].includes(selectedBooking.status)
);
let isCompleted = $derived(selectedBooking?.status === 'completed');
const isCompleted = $derived(selectedBooking?.status === 'completed');
let hoursUntilAppointment = $derived(
const hoursUntilAppointment = $derived(
selectedBooking
? (new SvelteDate(selectedBooking.start_time).getTime() - new SvelteDate().getTime()) /
(1000 * 60 * 60)
: Infinity
);
let protectedDeposit = $derived(
const protectedDeposit = $derived(
selectedBooking
? Math.min(totalPaid, selectedBooking.total_amount * POLICY.PROTECTED_DEPOSIT_MAX_PCT)
: 0
);
let estimatedRefund = $derived(
const estimatedRefund = $derived(
hoursUntilAppointment > POLICY.FULL_REFUND_THRESHOLD_HOURS
? totalPaid
: hoursUntilAppointment >= POLICY.PARTIAL_REFUND_THRESHOLD_HOURS
@@ -133,11 +133,11 @@
let customTipInput = $state('');
let tipProcessing = $state(false);
let canSaveCards = $derived(
const canSaveCards = $derived(
authStore.currentUser?.role === 'verified_email' || authStore.currentUser?.role === 'affiliate'
);
let tipPresets = $derived(
const tipPresets = $derived(
selectedBooking
? [
{ pct: 10, amount: Math.round(selectedBooking.total_amount * 0.1 * 100) / 100 },
@@ -138,7 +138,7 @@
minimum_age_required: validateCsMinimumAge(newCustomService.minimum_age_required)
};
}
let isCustomFormValid = $derived(
const isCustomFormValid = $derived(
(newCustomService.name ?? '').trim() !== '' &&
!customServiceErrors.name &&
!customServiceErrors.price &&
@@ -32,29 +32,29 @@
let cancelling = $state(false);
// Derived values for cancel confirmation
let totalPaid = $derived(
const totalPaid = $derived(
(selectedBooking?.payments ?? [])
.filter((p) => p.payment_method !== 'discount' && p.status === 'completed')
.reduce((sum, p) => sum + p.amount, 0)
);
let totalRefunds = $derived(
const totalRefunds = $derived(
(selectedBooking?.refunds ?? [])
.filter((r) => r.status === 'completed')
.reduce((sum, r) => sum + r.amount, 0)
);
let balanceDue = $derived(selectedBooking ? computeBalanceDue(selectedBooking) : 0);
let hoursUntilAppt = $derived(
const balanceDue = $derived(selectedBooking ? computeBalanceDue(selectedBooking) : 0);
const hoursUntilAppt = $derived(
selectedBooking
? (new SvelteDate(selectedBooking.start_time).getTime() - new SvelteDate().getTime()) /
(1000 * 60 * 60)
: Infinity
);
let protectedDeposit = $derived(
const protectedDeposit = $derived(
selectedBooking
? Math.min(totalPaid, selectedBooking.total_amount * POLICY.PROTECTED_DEPOSIT_MAX_PCT)
: 0
);
let estimatedRefund = $derived(
const estimatedRefund = $derived(
forgiveFeesCancel
? totalPaid
: hoursUntilAppt > POLICY.FULL_REFUND_THRESHOLD_HOURS
@@ -97,7 +97,7 @@
// IMPORTANT: Use override_duration_minutes when present — services may have been
// customised at booking time (discounts, extended sessions). Showing base values
// misleads admins about what was actually booked.
let totalDuration = $derived(
const totalDuration = $derived(
selectedBooking?.services?.reduce(
(sum, service) => sum + (service.override_duration_minutes ?? service.duration_minutes ?? 0),
0
@@ -12,7 +12,7 @@
import { formatUserName } from '$lib/utils/nameDisplay';
// Props
let { openBookingModal }: { openBookingModal: (bookingId: string) => void } = $props();
const { openBookingModal }: { openBookingModal: (bookingId: string) => void } = $props();
// State
let bookings = $state<Booking[]>([]);
@@ -25,7 +25,7 @@
let services = $state<CustomService[]>([]);
let total = $state(0);
let page = $state(1);
let perPage = $state(10);
const perPage = $state(10);
let searchQuery = $state('');
let loading = $state(true);
let creating = $state(false);
@@ -95,7 +95,7 @@
serviceErrors.minimum_age_required = validateMinimumAge(newService.minimum_age_required);
}
let isFormValid = $derived(
const isFormValid = $derived(
(newService.name ?? '').trim() !== '' &&
!serviceErrors.name &&
!serviceErrors.price &&
@@ -92,7 +92,7 @@
}
});
let isFormValid = $derived.by(() => {
const isFormValid = $derived.by(() => {
if (!form.name.trim()) return false;
if (form.discount_percent <= 0 || form.discount_percent > 100) return false;
if (form.campaign_type === 'time_based') {
@@ -149,11 +149,11 @@
}
}
let totalDuration = $derived(
const totalDuration = $derived(
services.reduce((sum, s) => sum + (s.override_duration_minutes ?? s.duration_minutes ?? 0), 0)
);
let totalPrice = $derived(
const totalPrice = $derived(
services.reduce((sum, s) => sum + (s.override_price ?? s.price ?? 0), 0)
);
@@ -280,7 +280,7 @@
return services.some((s) => s.service_id === serviceId);
}
let maxAvailableDuration = $derived.by(() => {
const maxAvailableDuration = $derived.by(() => {
if (!booking?.start_time || !nextAppointmentStart) return null;
const bookingStart = new SvelteDate(booking.start_time).getTime();
@@ -292,7 +292,7 @@
return Math.max(0, Math.floor(availableMs / (60 * 1000)));
});
let filteredServices = $derived.by(() => {
const filteredServices = $derived.by(() => {
return availableServices.filter((s) => {
if (isServiceAdded(s.id)) return false;
if (maxAvailableDuration !== null && s.duration_minutes > maxAvailableDuration) return false;
@@ -101,7 +101,7 @@
return false;
}
let serviceDiff = $derived.by(() => {
const serviceDiff = $derived.by(() => {
const orig = editRequest.original.services;
const prop = editRequest.proposed.services;
const origIds = new Set(orig.map((s) => s.id));
@@ -37,7 +37,7 @@
user_balances: UserBalance[];
}
let summary = $state<GiftCardSummary>({
const summary = $state<GiftCardSummary>({
total_unclaimed: 0,
total_user_balances: 0,
gift_cards: [],
@@ -196,32 +196,32 @@
let topUpMode = $state<'giveaway' | 'purchase'>('giveaway');
// Validation
let generateError = $derived(
const generateError = $derived(
generateAmount && (isNaN(Number(generateAmount)) || Number(generateAmount) <= 0)
? 'Must be a valid positive number'
: ''
);
let topUpError = $derived(
const topUpError = $derived(
topUpAmount && (isNaN(Number(topUpAmount)) || Number(topUpAmount) <= 0)
? 'Must be a valid positive number'
: ''
);
let transferAmountError = $derived(
const transferAmountError = $derived(
transferAmount && (isNaN(Number(transferAmount)) || Number(transferAmount) <= 0)
? 'Must be a valid positive number'
: ''
);
let transferCodeError = $derived(
const transferCodeError = $derived(
transferToCode && transferToCode.replace(/[^a-zA-Z0-9]/g, '').length !== 12
? 'Code must be exactly 12 characters'
: ''
);
let isAmountValid = $derived(
const isAmountValid = $derived(
generateType === 'stock' ? true : !!(generateAmount && !generateError)
);
let isEmailValid = $derived.by(() => {
const isEmailValid = $derived.by(() => {
if (generateType !== 'code') return true;
const trimmed = generateEmail.trim();
if (isGuestSelected) {
@@ -233,20 +233,20 @@
return trimmed.includes('@') && trimmed.includes('.');
});
let isGenerateValid = $derived(generateType === 'stock' ? true : isAmountValid && isEmailValid);
const isGenerateValid = $derived(generateType === 'stock' ? true : isAmountValid && isEmailValid);
let isTopUpValid = $derived(topUpAmount && !topUpError);
let isTransferValid = $derived(
const isTopUpValid = $derived(topUpAmount && !topUpError);
const isTransferValid = $derived(
transferAmount && !transferAmountError && transferToCode && !transferCodeError
);
// TillPayment state
let showTillPayment = $state(false);
let tillAmount = $state(0);
let tillAction = $state<'create' | 'topup'>('create');
let tillGiftCardId = $state<string | undefined>(undefined);
let tillUserId = $state<string | undefined>(undefined);
let tillDelivery = $state<'account' | 'code'>('code');
const showTillPayment = $state(false);
const tillAmount = $state(0);
const tillAction = $state<'create' | 'topup'>('create');
const tillGiftCardId = $state<string | undefined>(undefined);
const tillUserId = $state<string | undefined>(undefined);
const tillDelivery = $state<'account' | 'code'>('code');
async function fetchGiftCards(page: number = 1, search: string = '') {
loading = true;
@@ -512,7 +512,7 @@
// =============== Embedded Payment Handlers ===============
let isEphemeralCardValid = $derived(
const isEphemeralCardValid = $derived(
ephemeralCardNumber.replace(/\s/g, '').length >= 13 &&
ephemeralCardExpiry.includes('/') &&
ephemeralCardExpiry.length === 5 &&
@@ -811,8 +811,11 @@
| 'balance'
| 'updated';
let cardSort = $state<{ key: SortKey; dir: 'asc' | 'desc' }>({ key: 'created', dir: 'desc' });
let balanceSort = $state<{ key: SortKey; dir: 'asc' | 'desc' }>({ key: 'updated', dir: 'desc' });
const cardSort = $state<{ key: SortKey; dir: 'asc' | 'desc' }>({ key: 'created', dir: 'desc' });
const balanceSort = $state<{ key: SortKey; dir: 'asc' | 'desc' }>({
key: 'updated',
dir: 'desc'
});
function toggleCardSort(key: SortKey) {
if (cardSort.key === key) {
@@ -832,7 +835,7 @@
}
}
let sortedCards = $derived.by(() => {
const sortedCards = $derived.by(() => {
let filtered = [...cards];
if (activeSection === 'cards') {
filtered = cards.filter((gc) => !isExpired(gc.last_used_at));
@@ -864,10 +867,10 @@
return sorted;
});
let activeCardsCount = $derived(cards.filter((gc) => !isExpired(gc.last_used_at)).length);
let expiredCardsCount = $derived(cards.filter((gc) => isExpired(gc.last_used_at)).length);
const activeCardsCount = $derived(cards.filter((gc) => !isExpired(gc.last_used_at)).length);
const expiredCardsCount = $derived(cards.filter((gc) => isExpired(gc.last_used_at)).length);
let sortedBalances = $derived.by(() => {
const sortedBalances = $derived.by(() => {
const bals = [...balances];
const { key, dir } = balanceSort;
const mul = dir === 'asc' ? 1 : -1;
@@ -37,7 +37,7 @@
let exceptionGroupsLoading = $state(true);
let savingHours = $state(false);
let formErrors = $state({
const formErrors = $state({
name: '',
weeks: ''
});
@@ -59,7 +59,7 @@
]
});
let isFormValid = $derived(
const isFormValid = $derived(
exceptionDraft.name.trim() !== '' && exceptionDraft.weekStarts.length > 0
);
@@ -26,7 +26,7 @@
return file.size > MAX_FILE_SIZE;
}
let hasOversizedFiles = $derived(uploadFiles.some(isFileTooBig));
const hasOversizedFiles = $derived(uploadFiles.some(isFileTooBig));
function isHeicFile(file: File): boolean {
const name = file.name.toLowerCase();
@@ -83,7 +83,7 @@
validateExpiryField();
}
let isFormValid = $derived(
const isFormValid = $derived(
formData.name.trim() !== '' &&
!formErrors.name &&
!formErrors.notice_duration &&
@@ -65,20 +65,20 @@
maxDate.getDate()
);
let hoursUntilAppointment = $derived(
const hoursUntilAppointment = $derived(
(new SvelteDate(booking.start_time).getTime() - new SvelteDate().getTime()) / (1000 * 60 * 60)
);
let hasPayments = $derived((booking.amount_paid ?? 0) > 0);
let showNoticeWarning = $derived(
const hasPayments = $derived((booking.amount_paid ?? 0) > 0);
const showNoticeWarning = $derived(
hasPayments ? hoursUntilAppointment < 72 : hoursUntilAppointment < 24
);
let showNoShowWarning = $derived(
const showNoShowWarning = $derived(
!hasPayments && hoursUntilAppointment < 24 && hoursUntilAppointment >= 0
);
let forgiveFees = $state(false);
let forgiveNoShow = $state(false);
let bookingDuration = $derived(
const bookingDuration = $derived(
booking.services?.reduce(
(sum, s) => sum + (s.override_duration_minutes ?? s.duration_minutes ?? 0),
0
@@ -36,9 +36,9 @@
is_active: true,
minimum_age_required: 0
});
let editingService = $state<Service | null>(null);
const editingService = $state<Service | null>(null);
let servicesLoading = $state(true);
let servicesUpdating = $state<Record<string, boolean>>({});
const servicesUpdating = $state<Record<string, boolean>>({});
let showServiceModal = $state(false);
let creatingService = $state(false);
let serviceErrors = $state<Record<string, string>>({});
@@ -129,7 +129,7 @@
);
}
let isFormValid = $derived(
const isFormValid = $derived(
newService.name !== '' &&
!serviceErrors.name &&
!serviceErrors.price &&
@@ -15,8 +15,8 @@
let giftCardAmount = $state('25');
let showGiftCardInput = $state(false);
let subtotal = $derived(cart.reduce((sum, item) => sum + item.price * item.qty, 0));
let itemCount = $derived(cart.reduce((sum, item) => sum + item.qty, 0));
const subtotal = $derived(cart.reduce((sum, item) => sum + item.price * item.qty, 0));
const itemCount = $derived(cart.reduce((sum, item) => sum + item.qty, 0));
function formatCurrency(n: number): string {
return new Intl.NumberFormat('en-GB', { style: 'currency', currency: 'GBP' }).format(n);
@@ -55,7 +55,7 @@
rescheduleVersion?: number;
}
let { openUserModal, openBookingModal, rescheduleVersion = 0 }: Props = $props();
const { openUserModal, openBookingModal, rescheduleVersion = 0 }: Props = $props();
const PAGE_SIZE = 5;
@@ -84,7 +84,7 @@
let overlappingBookings = $state<OverlappingBooking[]>([]);
let hasOverlap = $state(false);
let hasDayConflicts = $derived(overlappingBookings.length > 0);
const hasDayConflicts = $derived(overlappingBookings.length > 0);
let showDeleteAlert = $state(false);
let blockerToDelete = $state<TimeBlocker | null>(null);
@@ -128,9 +128,9 @@
return `${row.startTime}-${row.endTime}-${row.isOpen}`;
}
let selectedWorkingHours = $derived.by(() => getWorkingHoursForDate(newStartDate));
const selectedWorkingHours = $derived.by(() => getWorkingHoursForDate(newStartDate));
let availableStartOptions = $derived.by(() => {
const availableStartOptions = $derived.by(() => {
const wh = selectedWorkingHours;
if (!wh || !wh.isOpen) return [];
const startMin = timeToMinutes(wh.startTime);
@@ -154,7 +154,7 @@
return options;
});
let availableEndOptions = $derived.by(() => {
const availableEndOptions = $derived.by(() => {
const wh = selectedWorkingHours;
if (!wh || !wh.isOpen) return [];
const startMin = timeToMinutes(wh.startTime);
@@ -197,14 +197,14 @@
return `in ${Math.floor(hr / 24)}d`;
}
let sortedBlockers = $derived.by(() => {
const sortedBlockers = $derived.by(() => {
return [...blockers].sort(
(a, b) => new SvelteDate(a.start_time).getTime() - new SvelteDate(b.start_time).getTime()
);
});
let totalPages = $derived.by(() => Math.max(1, Math.ceil(sortedBlockers.length / PAGE_SIZE)));
let pagedBlockers = $derived.by(() => {
const totalPages = $derived.by(() => Math.max(1, Math.ceil(sortedBlockers.length / PAGE_SIZE)));
const pagedBlockers = $derived.by(() => {
const start = (currentPage - 1) * PAGE_SIZE;
return sortedBlockers.slice(start, start + PAGE_SIZE);
});
@@ -275,13 +275,13 @@
return formatLocalDateTime(dt);
}
let formComplete = $derived.by(() => {
const formComplete = $derived.by(() => {
return (
newDescription.trim() !== '' && newStartDate !== '' && startHour !== '' && endHour !== ''
);
});
let canCreate = $derived.by(() => {
const canCreate = $derived.by(() => {
return formComplete && !hasOverlap && !hasDayConflicts && !checkingOverlap;
});
@@ -11,7 +11,7 @@
openUserModal: (userId: string) => void;
}
let { openUserModal }: Props = $props();
const { openUserModal }: Props = $props();
type UserListItem = {
id: string;
@@ -322,13 +322,13 @@
window.__walkInCountdownInterval = setInterval(updateCountdown, 1000);
}
let availableMinutes = $derived.by(() => {
const availableMinutes = $derived.by(() => {
if (!slotInfo) return null;
if (slotInfo.isAvailableNow) return getLiveRemainingMinutes();
return slotInfo.durationMinutes;
});
let tooShortForService = $derived.by(() => {
const tooShortForService = $derived.by(() => {
if (shortestServiceMinutes === null || availableMinutes === null) return false;
return availableMinutes < shortestServiceMinutes;
});
@@ -124,7 +124,7 @@
minimum_age_required: validateCsMinimumAge(newCustomService.minimum_age_required)
};
}
let isCustomFormValid = $derived(
const isCustomFormValid = $derived(
(newCustomService.name ?? '').trim() !== '' &&
!customServiceErrors.name &&
!customServiceErrors.price &&
@@ -72,7 +72,7 @@
let selectedServices = $state<Service[]>([]);
let selectedDate = $state<CalendarDate | undefined>(undefined);
let selectedTime = $state<string | null>(null);
let customerInfo = $state<CustomerInfo>({
const customerInfo = $state<CustomerInfo>({
firstName: '',
lastName: '',
email: '',
@@ -98,13 +98,13 @@
let newCardNumber = $state('');
let newCardExpiry = $state('');
let newCardCVC = $state('');
let saveCardForFuture = $state(false);
const saveCardForFuture = $state(false);
// Payment flow state
let depositPaid = $state(false);
let showPaymentForm = $state(false);
let depositCardFormValid = $derived(
const depositCardFormValid = $derived(
selectedPaymentMethod !== null ||
(showNewCardForm &&
newCardNumber.replace(/\s/g, '').length >= 13 &&
@@ -113,17 +113,17 @@
);
// VAT registration status from public business info (via shared store)
let vatRegistered = $derived(getBusinessInfo()?.is_vat_registered ?? false);
const vatRegistered = $derived(getBusinessInfo()?.is_vat_registered ?? false);
// Email existence check (guest flow only)
let emailChecking = $state(false);
let emailSuggestion = $state<string | null>(null);
let emailError = $state('');
let emailFormatValid = $derived(
const emailFormatValid = $derived(
!customerInfo.email ||
/^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/.test(customerInfo.email)
);
let allGuestFieldsValid = $derived(
const allGuestFieldsValid = $derived(
customerInfo.firstName &&
customerInfo.lastName &&
customerInfo.email &&
@@ -571,12 +571,12 @@
let loadingWorkingHours = $state<boolean>(false);
let loadingAvailableHours = $state<boolean>(false);
let workingHoursCache: Record<
const workingHoursCache: Record<
string,
Record<string, { isOpen: boolean; startTime: string; endTime: string }>
> = {};
let availableHoursCache: Record<
const availableHoursCache: Record<
string,
Record<string, { isOpen: boolean; slots: Array<{ startTime: string; endTime: string }> }>
> = {};
@@ -602,7 +602,7 @@
});
// Track which months are currently being fetched (prevents duplicate requests)
let loadingMonths: Record<string, boolean> = {};
const loadingMonths: Record<string, boolean> = {};
// Preload current + next month on first render; subsequent months fetched individually
let initialLoadDone = $state(false);
@@ -1304,13 +1304,13 @@
selectedDate ? getDayWithOrdinal(selectedDate) : undefined
);
let depositRequired = $derived(calculateDepositRequired());
let totalSteps = $derived(authStore.isAuthenticated ? 4 : 5);
const depositRequired = $derived(calculateDepositRequired());
const totalSteps = $derived(authStore.isAuthenticated ? 4 : 5);
// StepIndicator uses displayNumber = startAt + index. currentStep aligns with displayNumber,
// not the array index. For auth: startAt=1, totalSteps=4 → last displayNumber=4.
// For guest: startAt=0, totalSteps=5 → last displayNumber=4. Always evaluates to 4.
let finalStep = $derived(totalSteps - 1 + (authStore.isAuthenticated ? 1 : 0));
let stepLabels = $derived(
const finalStep = $derived(totalSteps - 1 + (authStore.isAuthenticated ? 1 : 0));
const stepLabels = $derived(
authStore.isAuthenticated
? ['Service', 'Date & Time', 'Details', 'Payment']
: userDepositsRequired > 0
@@ -4,7 +4,7 @@
import { getLocalTimeZone } from '@internationalized/date';
import { Separator } from '$lib/components/ui/separator';
let {
const {
services = [],
date = undefined,
time = null,
@@ -2,7 +2,7 @@
import type { CalendarDate, DateValue } from '@internationalized/date';
import Calendar from '$lib/components/ui/calendar/calendar.svelte';
let {
const {
date,
placeholder,
minValue,
@@ -11,7 +11,7 @@
outOfHours?: boolean;
}
let {
const {
selectedDate,
selectedTime,
endTime,
@@ -2,7 +2,7 @@
import { resolve } from '$app/paths';
import type { Service } from '$lib/types/booking';
let {
const {
service,
selected = false,
onclick,
@@ -2,7 +2,7 @@
import ServiceCard from './ServiceCard.svelte';
import type { Service } from '$lib/types/booking';
let {
const {
services = [],
selected = [],
loading = true,
@@ -11,7 +11,7 @@
onSelect: (time: string) => void;
}
let { slots, selectedTime, duration, protection, onSelect }: Props = $props();
const { slots, selectedTime, duration, protection, onSelect }: Props = $props();
</script>
{#if slots.length > 0}
@@ -2,7 +2,7 @@
import type { CalendarDate } from '@internationalized/date';
import { Button } from '$lib/components/ui/button';
let {
const {
date,
groupedTimeSlots = [],
selectedTime = null,
@@ -52,8 +52,8 @@
};
// Derived values for auth state - ensures reactivity
let isAuthenticated = $derived(authStore.isAuthenticated);
let isLoading = $derived(authStore.isLoading);
const isAuthenticated = $derived(authStore.isAuthenticated);
const isLoading = $derived(authStore.isLoading);
// Notification bell state
let unreadCount = $state(0);
@@ -15,7 +15,7 @@
onComplete: (payment: PaymentResult) => void;
}
let { booking, onClose, onComplete }: Props = $props();
const { booking, onClose, onComplete }: Props = $props();
type PaymentStatus =
| 'idle'
@@ -47,17 +47,17 @@
let paymentResult = $state<PaymentResult | null>(null);
let error = $state<string | null>(null);
let stamps = $derived(booking.user?.loyalty_stamps ?? 0);
const stamps = $derived(booking.user?.loyalty_stamps ?? 0);
let useLoyalty = $state(false);
let loyaltyEligible = $derived(
const loyaltyEligible = $derived(
stamps >= 10 &&
!(booking.discounts ?? []).some((d: BookingDiscount) => d.discount_source === 'loyalty') &&
booking.total_amount > 0 &&
booking.amount_paid === 0
);
let loyaltyDiscount = $derived(
const loyaltyDiscount = $derived(
useLoyalty ? Math.round(booking.total_amount * 100 * LOYALTY_DISCOUNT_RATE) : 0
);
@@ -203,13 +203,15 @@
return service.override_price ?? service.price ?? 0;
}
let subtotal = $derived((booking.services ?? []).reduce((sum, s) => sum + getServicePrice(s), 0));
let discountSum = $derived(
const subtotal = $derived(
(booking.services ?? []).reduce((sum, s) => sum + getServicePrice(s), 0)
);
const discountSum = $derived(
(booking.discounts ?? []).reduce((sum, d) => sum + d.discount_amount, 0)
);
let netTotal = $derived(Math.max(0, subtotal - discountSum));
const netTotal = $derived(Math.max(0, subtotal - discountSum));
let tipPercentages = $derived.by(() => {
const tipPercentages = $derived.by(() => {
if (netTotal <= 0) return [];
return [
{ pct: 10, amount: Math.round(netTotal * 0.1 * 100) / 100 },
@@ -218,7 +220,7 @@
];
});
let tipMultiplier = $derived(
const tipMultiplier = $derived(
selectedTipPercent !== null
? 1 + selectedTipPercent / 100
: customTipAmount && parseFloat(customTipAmount) > 0
@@ -226,8 +228,8 @@
: 1
);
let totalWithTip = $derived(tipEnabled ? netTotal * tipMultiplier : netTotal);
let tipDisplay = $derived(
const totalWithTip = $derived(tipEnabled ? netTotal * tipMultiplier : netTotal);
const tipDisplay = $derived(
selectedTipPercent !== null
? `${selectedTipPercent}%`
: customTipAmount && parseFloat(customTipAmount) > 0
@@ -235,7 +237,7 @@
: ''
);
let totalDue = $derived(tipEnabled ? totalWithTip : netTotal);
const totalDue = $derived(tipEnabled ? totalWithTip : netTotal);
function formatCurrency(value: number): string {
return new Intl.NumberFormat('en-GB', {
@@ -378,8 +380,8 @@
});
let cashAmount = $state<string>('');
let cashAmountNum = $derived(cashAmount === '' ? 0 : parseFloat(cashAmount));
let changeDue = $derived(cashAmountNum > totalDue ? cashAmountNum - totalDue : 0);
const cashAmountNum = $derived(cashAmount === '' ? 0 : parseFloat(cashAmount));
const changeDue = $derived(cashAmountNum > totalDue ? cashAmountNum - totalDue : 0);
let extraAsTip = $state(false);
function handleCashInput(e: Event) {
@@ -511,7 +513,7 @@
giftCardId = formatted;
}
let giftCardValid = $derived(useAccountBalance || giftCardId.replace(/-/g, '').length === 12);
const giftCardValid = $derived(useAccountBalance || giftCardId.replace(/-/g, '').length === 12);
async function handleGiftCardPayment() {
if (!giftCardValid) {
@@ -21,7 +21,7 @@
onComplete: (result: TillSaleResult) => void;
}
let {
const {
amount,
itemType,
action,
@@ -100,9 +100,9 @@
// Cash
let cashAmount = $state('');
let cashAmountNum = $derived(Number(cashAmount) || 0);
let changeDue = $derived(Math.max(0, cashAmountNum - amount));
let cashEntryComplete = $derived(cashAmountNum >= amount);
const cashAmountNum = $derived(Number(cashAmount) || 0);
const changeDue = $derived(Math.max(0, cashAmountNum - amount));
const cashEntryComplete = $derived(cashAmountNum >= amount);
// Remember cash received for success screen display
let cashTendered = $state(0);
@@ -224,13 +224,13 @@
ephemeralCardCVC = input.value.replace(/\D/g, '').substring(0, 4);
}
let isEphemeralCardValid = $derived(
const isEphemeralCardValid = $derived(
isValidLuhn(ephemeralCardNumber) &&
/^\d{2}\/\d{2}$/.test(ephemeralCardExpiry) &&
ephemeralCardCVC.length >= 3
);
let ephemeralCardError = $derived(
const ephemeralCardError = $derived(
ephemeralCardNumber.length > 0 && !isValidLuhn(ephemeralCardNumber)
? 'Invalid card number'
: /^\d{2}\/\d{2}$/.test(ephemeralCardExpiry) &&
@@ -22,7 +22,7 @@
defaultPaymentType?: 'full' | 'partial' | 'deposit';
}
let { booking, onClose, onComplete, canSaveCards = true, defaultPaymentType }: Props = $props();
const { booking, onClose, onComplete, canSaveCards = true, defaultPaymentType }: Props = $props();
type PaymentStatus = 'idle' | 'processing' | 'polling' | 'success' | 'error';
@@ -140,9 +140,9 @@
return { month, year };
}
let expiryParts = $derived(parseExpiryParts(newCardExpiry));
const expiryParts = $derived(parseExpiryParts(newCardExpiry));
let isExpiryInPast = $derived(
const isExpiryInPast = $derived(
expiryParts !== null &&
(() => {
const expiryDate = new SvelteDate(expiryParts.year, expiryParts.month);
@@ -150,7 +150,7 @@
})()
);
let hasInvalidMonth = $derived(/^\d{2}\/\d{2}$/.test(newCardExpiry) && expiryParts === null);
const hasInvalidMonth = $derived(/^\d{2}\/\d{2}$/.test(newCardExpiry) && expiryParts === null);
function isValidLuhn(cardNumber: string): boolean {
const s = cardNumber.replace(/\D/g, '');
@@ -168,17 +168,17 @@
return sum % 10 === 0 && s.length >= 13 && s.length <= 19;
}
let cardFormValid = $derived(
const cardFormValid = $derived(
isValidLuhn(newCardNumber) && expiryParts !== null && newCardCVC.length >= 3 && !isExpiryInPast
);
let cardSelected = $derived(
const cardSelected = $derived(
(selectedPaymentMethod !== null && paymentMethods.length > 0) ||
(showNewCardForm && cardFormValid) ||
(paymentMethods.length === 0 && cardFormValid)
);
let cardValidationError = $derived(
const cardValidationError = $derived(
!cardSelected
? selectedPaymentMethod === null && paymentMethods.length > 0 && !showNewCardForm
? 'Please select a card'
@@ -211,12 +211,12 @@
} | null>(null);
// Derived values
let depositOutstanding = $derived(booking.deposit_required && !booking.deposit_paid);
const depositOutstanding = $derived(booking.deposit_required && !booking.deposit_paid);
// Auto-select a sensible default payment type based on the booking's deposit
// state. The backend will split the charge into deposit + non-deposit records
// when appropriate, so this choice mainly controls the button label and amount.
let defaultType = $derived(defaultPaymentType ?? (depositOutstanding ? 'deposit' : 'full'));
const defaultType = $derived(defaultPaymentType ?? (depositOutstanding ? 'deposit' : 'full'));
let paymentType = $state<'full' | 'partial' | 'deposit'>('full');
$effect(() => {
paymentType = defaultType as 'full' | 'partial' | 'deposit';
@@ -230,35 +230,35 @@
let lockInterval: ReturnType<typeof setInterval> | null = null;
let countdownInterval: ReturnType<typeof setInterval> | null = null;
let servicesSubtotal = $derived(
const servicesSubtotal = $derived(
(booking.services ?? []).reduce((sum, s) => sum + (s.price || 0), 0)
);
let discountSum = $derived(
const discountSum = $derived(
(booking.discounts ?? []).reduce((sum, d) => sum + d.discount_amount, 0)
);
let totalPaid = $derived(
const totalPaid = $derived(
booking.payments
?.filter((p) => p.status === 'completed')
.reduce((sum, p) => sum + p.amount, 0) || 0
);
let amountRemaining = $derived(booking.total_amount - totalPaid);
const amountRemaining = $derived(booking.total_amount - totalPaid);
let loyaltyEligible = $derived(
const loyaltyEligible = $derived(
stamps >= 10 &&
!(booking.discounts ?? []).some((d) => d.discount_source === 'loyalty') &&
booking.total_amount > 0 &&
booking.amount_paid === 0
);
let loyaltyDiscount = $derived(
const loyaltyDiscount = $derived(
useLoyalty ? Math.round(booking.total_amount * 100 * LOYALTY_DISCOUNT_RATE) : 0
);
// Deposit policy warning text — dynamic based on booking state
let expectedDepositPercent = $derived(booking.deposit_required ? 20 : 0);
let depositPolicyWarning = $derived<string | null>(
const expectedDepositPercent = $derived(booking.deposit_required ? 20 : 0);
const depositPolicyWarning = $derived<string | null>(
{
get text(): string | null {
if (!booking.deposit_required && totalPaid === 0 && booking.amount_due <= 0) return null;
@@ -273,12 +273,12 @@
}.text
);
let isScenarioA = $derived(depositOutstanding);
const isScenarioA = $derived(depositOutstanding);
let isScenarioB = $derived(!depositOutstanding && totalPaid < booking.total_amount);
const isScenarioB = $derived(!depositOutstanding && totalPaid < booking.total_amount);
let partialAmountNum = $derived(partialAmount === '' ? 0 : parseFloat(partialAmount));
let partialAmountValid = $derived(
const partialAmountNum = $derived(partialAmount === '' ? 0 : parseFloat(partialAmount));
const partialAmountValid = $derived(
paymentType === 'partial' &&
partialAmount !== '' &&
!isNaN(partialAmountNum) &&
@@ -287,7 +287,7 @@
/^\d+(\.\d{0,2})?$/.test(partialAmount)
);
let partialValidationError = $derived(
const partialValidationError = $derived(
paymentType === 'partial' && !partialAmountValid
? partialAmount === ''
? 'Enter an amount'
@@ -301,14 +301,14 @@
: null
);
let payButtonDisabled = $derived(
const payButtonDisabled = $derived(
status === 'processing' ||
!cardSelected ||
(paymentType === 'partial' && !partialAmountValid) ||
(booking.status === 'pending_release' && (lockTimer <= 0 || !lockAcquired))
);
let payButtonError = $derived(
const payButtonError = $derived(
status === 'processing'
? null
: !cardSelected
@@ -18,7 +18,7 @@
openUserModal: (userId: string) => void;
}
let { _openBookingModal, openEditBookingModal, openUserModal }: Props = $props();
const { _openBookingModal, openEditBookingModal, openUserModal }: Props = $props();
type Booking = {
id: string;
@@ -90,7 +90,7 @@
let weekSummary = $state<DailySummary | null>(null);
// VAT registration status from public business info (via shared store)
let vatRegistered = $derived(getBusinessInfo()?.is_vat_registered ?? false);
const vatRegistered = $derived(getBusinessInfo()?.is_vat_registered ?? false);
// Data diffing — only update UI when payload actually changes
let prevDataJson = $state('');
@@ -87,17 +87,17 @@
};
let pendingApprovals = $state<PendingApproval[]>([]);
let visibleApprovals = $derived(pendingApprovals.slice(0, 3));
const visibleApprovals = $derived(pendingApprovals.slice(0, 3));
let loading = $state(true);
let showApprovalModal = $state(false);
let selectedBooking = $state<PendingBooking | null>(null);
let pendingEditRequests = $state<EditRequest[]>([]);
let visibleEditRequests = $derived(pendingEditRequests.slice(0, 3));
const visibleEditRequests = $derived(pendingEditRequests.slice(0, 3));
let showEditRequestModal = $state(false);
let selectedEditRequest = $state<EditRequest | null>(null);
let _hasItems = $derived(pendingApprovals.length > 0 || pendingEditRequests.length > 0);
const _hasItems = $derived(pendingApprovals.length > 0 || pendingEditRequests.length > 0);
$effect(() => {
hasItems = _hasItems;
});
@@ -28,7 +28,7 @@
openUserModal: (userId: string) => void;
}
let { openBookingModal, openUserModal }: Props = $props();
const { openBookingModal, openUserModal }: Props = $props();
type TodayAppointment = {
id: string;
@@ -155,7 +155,7 @@
return blocker.description?.startsWith('RESERVATION:') ?? false;
}
let availableStartOptions = $derived.by(() => {
const availableStartOptions = $derived.by(() => {
const wh = workingHours;
if (!wh || !wh.isOpen) return [];
const startMin = timeToMinutes(wh.startTime);
@@ -174,7 +174,7 @@
return options;
});
let availableEndOptions = $derived.by(() => {
const availableEndOptions = $derived.by(() => {
const wh = workingHours;
if (!wh || !wh.isOpen) return [];
const endMin = timeToMinutes(wh.endTime);
@@ -197,7 +197,7 @@
return options;
});
let suggestedLunch = $derived.by(() => {
const suggestedLunch = $derived.by(() => {
if (!workingHours || !availableHours || !workingHours.isOpen) return null;
const wh = workingHours;
const ah = availableHours;
@@ -284,7 +284,7 @@
| LunchTimelineItem
| ClosingTimeTimelineItem;
let timeline = $derived.by(() => {
const timeline = $derived.by(() => {
const items: TimelineItem[] = [];
for (const apt of appointments) {
@@ -512,7 +512,7 @@
return formatLocalDateTime(dt);
}
let canCreate = $derived.by(() => !hasOverlap && !checkingOverlap);
const canCreate = $derived.by(() => !hasOverlap && !checkingOverlap);
async function createBlocker() {
if (!canCreate) return;
@@ -4,7 +4,7 @@
maxChars?: number;
threshold?: number; // when to start showing counter
};
let { text, maxChars = 1000000, threshold = 750000 }: Props = $props();
const { text, maxChars = 1000000, threshold = 750000 }: Props = $props();
const graphemeCount = $derived.by(() => {
const segmenter = new Intl.Segmenter('en', { granularity: 'grapheme' });
@@ -12,7 +12,7 @@
jxl?: string;
}
let {
const {
urls,
type = 'thumb',
alt = '',
@@ -1,5 +1,5 @@
<script lang="ts">
let { status }: { status: string } = $props();
const { status }: { status: string } = $props();
const variantMap: Record<string, { label: string; variantClass: string }> = {
draft: { label: 'Draft', variantClass: 'bg-gray-100 text-gray-800' },
@@ -1,7 +1,7 @@
<script lang="ts">
import * as Button from '$lib/components/ui/button/index.js';
let { ...restProps }: Record<string, unknown> = $props();
const { ...restProps }: Record<string, unknown> = $props();
let ref = $state<HTMLElement | null>(null);
</script>
@@ -3,7 +3,7 @@
import { useImageCropperCropper } from './image-cropper.svelte.js';
import type { ImageCropperCropperProps } from './types.js';
let {
const {
cropShape = 'round',
aspect = 1,
showGrid = false,
@@ -4,7 +4,7 @@
import { useImageCropperDialog } from './image-cropper.svelte.js';
import type { ImageCropperDialogProps } from './types';
let { children, class: className, ...rest }: ImageCropperDialogProps = $props();
const { children, class: className, ...rest }: ImageCropperDialogProps = $props();
const dialogState = useImageCropperDialog();
</script>
@@ -5,7 +5,7 @@
import UploadIcon from '@lucide/svelte/icons/upload';
import { cn } from '$lib/utils.js';
let { child, class: className }: ImageCropperPreviewProps = $props();
const { child, class: className }: ImageCropperPreviewProps = $props();
const previewState = useImageCropperPreview();
</script>
@@ -52,7 +52,7 @@
<script lang="ts" generics="T extends MapArcDatum = MapArcDatum">
import { useMap } from '$lib/hooks/use-map.svelte.js';
let {
const {
data,
id: propId,
curvature = 0.2,
@@ -80,7 +80,7 @@
const ARC_HIT_MIN_WIDTH = 12;
const ARC_HIT_PADDING = 6;
let autoId = $state(Math.random().toString(36).slice(2));
const autoId = $state(Math.random().toString(36).slice(2));
const id = $derived(propId ?? autoId);
const sourceId = $derived(`arc-source-${id}`);
const layerId = $derived(`arc-layer-${id}`);
@@ -25,7 +25,7 @@
onclusterclick?: (clusterId: number, coordinates: [number, number], pointCount: number) => void;
}
let {
const {
data,
clusterMaxZoom = 14,
clusterRadius = 50,
@@ -18,7 +18,7 @@
onlocate?: (coords: { longitude: number; latitude: number }) => void;
}
let {
const {
position = 'bottom-right',
showZoom = true,
showCompass = false,
@@ -31,7 +31,7 @@
rotationAlignment?: MarkerOptions['rotationAlignment'];
}
let {
const {
longitude,
latitude,
children,
@@ -19,7 +19,7 @@
maxWidth?: string;
}
let {
const {
longitude,
latitude,
children,
@@ -26,7 +26,7 @@
interactive?: boolean;
}
let {
const {
coordinates,
color = '#4285F4',
width = 3,
@@ -8,7 +8,7 @@
class?: string;
}
let { children, class: className }: Props = $props();
const { children, class: className }: Props = $props();
const markerCtx = getContext<{
getMarker: () => MapLibreGL.Marker | null;
@@ -7,7 +7,7 @@
position?: 'top' | 'bottom';
}
let { children, class: className, position = 'top' }: Props = $props();
const { children, class: className, position = 'top' }: Props = $props();
const positionClasses = {
top: 'bottom-full mb-1',
@@ -16,7 +16,7 @@
maxWidth?: string;
}
let {
const {
children,
class: className,
closeButton = false,
@@ -10,7 +10,7 @@
anchor?: PopupOptions['anchor'];
}
let { children, class: className, offset = 16, anchor }: Props = $props();
const { children, class: className, offset = 16, anchor }: Props = $props();
const markerCtx = getContext<{
getMarker: () => MapLibreGL.Marker | null;
@@ -1,7 +1,7 @@
<script lang="ts">
import type { Snippet } from 'svelte';
let {
const {
trigger
}: {
trigger?: Snippet;
@@ -2,7 +2,7 @@
import { Toaster as Sonner, type ToasterProps as SonnerProps } from 'svelte-sonner';
import { mode } from 'mode-watcher';
let { ...restProps }: SonnerProps = $props();
const { ...restProps }: SonnerProps = $props();
</script>
<Sonner
+2 -2
View File
@@ -120,7 +120,7 @@ export function timeToMinutes(time: string): number {
export function calculatePreviousTime(time: string): string {
const [hours, minutes] = time.split(':').map(Number);
let totalMinutes = hours * 60 + minutes - 15;
const totalMinutes = hours * 60 + minutes - 15;
return `${String(Math.floor(totalMinutes / 60)).padStart(2, '0')}:${String(totalMinutes % 60).padStart(2, '0')}`;
}
@@ -208,7 +208,7 @@ export function generateAvailableTimeSlots(
});
const [londonHours, londonMinutes] = londonTimeStr.split(':').map(Number);
const currentMinutes = londonHours * 60 + londonMinutes;
let minimumStart = Math.ceil((currentMinutes + 60) / 15) * 15;
const minimumStart = Math.ceil((currentMinutes + 60) / 15) * 15;
startTotalMinutes = Math.max(startTotalMinutes, minimumStart);
}