linting and bugfixing

This commit is contained in:
2025-11-09 01:49:21 +00:00
parent e734ec8f37
commit e8f53f4282
5 changed files with 149 additions and 170 deletions
+6 -6
View File
@@ -109,13 +109,13 @@ func CreateServiceHandler(w http.ResponseWriter, r *http.Request) {
// Insert new service
query := `
INSERT INTO services (
name, description, price, duration_minutes,
name, description, price, duration_minutes,
patch_test_duration_hours, minimum_age_required, created_by
)
)
VALUES ($1, $2, $3, $4, $5, $6, $7)
RETURNING
RETURNING
id, name, description, price, duration_minutes, is_active,
patch_test_duration_hours, minimum_age_required, created_at,
patch_test_duration_hours, minimum_age_required, created_at,
created_by
`
@@ -200,7 +200,7 @@ func DeleteServiceHandler(w http.ResponseWriter, r *http.Request) {
func ServicesHandler(w http.ResponseWriter, r *http.Request) {
// Query all active services
query := `
SELECT id, name, description, price, duration_minutes,
SELECT id, name, description, price, duration_minutes,
patch_test_duration_hours, minimum_age_required
FROM services
WHERE is_active = TRUE
@@ -261,7 +261,7 @@ func ServicesHandler(w http.ResponseWriter, r *http.Request) {
func AllServicesHandler(w http.ResponseWriter, r *http.Request) {
// Query all services including inactive ones
query := `
SELECT id, name, description, price, duration_minutes, is_active,
SELECT id, name, description, price, duration_minutes, is_active,
patch_test_duration_hours, minimum_age_required, created_at, created_by
FROM services
ORDER BY is_active DESC, name
+10 -1
View File
@@ -24,7 +24,16 @@ export default defineConfig(
rules: {
// typescript-eslint strongly recommend that you do not use the no-undef lint rule on TypeScript projects.
// see: https://typescript-eslint.io/troubleshooting/faqs/eslint/#i-get-errors-from-the-no-undef-rule-about-global-variables-not-being-defined-even-though-there-are-no-typescript-errors
'no-undef': 'off'
'no-undef': 'off',
// Allow underscore prefix for unused variables
'@typescript-eslint/no-unused-vars': [
'error',
{
argsIgnorePattern: '^_',
varsIgnorePattern: '^_',
caughtErrorsIgnorePattern: '^_'
}
]
}
},
{
+92 -71
View File
@@ -1,6 +1,8 @@
<script lang="ts">
import { goto } from '$app/navigation';
import { authStore } from '$lib/stores/auth.svelte';
import { SvelteDate } from 'svelte/reactivity';
// shadcn-svelte components
import { Button } from '$lib/components/ui/button';
import * as Card from '$lib/components/ui/card';
@@ -72,7 +74,7 @@
} else {
uploadResults.push({ name: file.name, url: `/images/${file.name}` });
}
} catch (err: any) {
} catch (err: unknown) {
uploadResults.push({ name: file.name, error: err?.message || 'Network error' });
}
@@ -85,7 +87,6 @@
// =============== Working Hours ===============
type WorkingHourRow = {
id?: number;
weekday: number;
start_time: string;
end_time: string;
@@ -148,7 +149,7 @@
if (response.ok) {
const data = await response.json();
defaultHours = data.map((hour: any) => ({
defaultHours = data.map((hour) => ({
weekday: hour.weekday,
start_time: formatTime(hour.startTime),
end_time: formatTime(hour.endTime),
@@ -223,13 +224,13 @@
if (data === null || data.length === 0) {
return;
}
exceptionGroups = data.map((group: any) => ({
exceptionGroups = data.map((group) => ({
id: group.id,
name: group.name,
description: group.description,
weekStarts: group.weekStarts || [],
hours:
group.hours?.map((h: any) => ({
group.hours?.map((h) => ({
id: h.id,
weekday: h.weekday,
start_time: formatTime(h.startTime),
@@ -590,7 +591,7 @@
}
// Map the response correctly - the backend returns the full Booking objects
bookings = data.bookings.map((b: any) => ({
bookings = data.bookings.map((b) => ({
id: b.id,
start_time: b.start_time,
status: b.status,
@@ -655,7 +656,7 @@
console.log('Search API response:', data); // Debug log
// Map the search response correctly (same structure as fetchBookings)
bookings = data.bookings.map((b: any) => ({
bookings = data.bookings.map((b) => ({
id: b.id,
start_time: b.start_time,
status: b.status,
@@ -725,7 +726,7 @@
notes: data.user.notes
}
: undefined,
services: (data.services || []).map((s: any) => ({
services: (data.services || []).map((s) => ({
booking_id: s.booking_id,
service_id: s.service_id,
service_name: s.service_name,
@@ -733,7 +734,7 @@
price: s.price,
duration_minutes: s.duration_minutes
})),
payments: (data.payments || []).map((p: any) => ({
payments: (data.payments || []).map((p) => ({
id: p.id,
booking_id: p.booking_id,
payment_type: p.payment_type,
@@ -813,7 +814,9 @@
// Filter demo bookings for this user
bookingUserHistory = bookings
.filter((b) => b?.user?.id === userId)
.sort((a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime());
.sort(
(a, b) => new SvelteDate(b.created_at).getTime() - new SvelteDate(a.created_at).getTime()
);
showUserModal = true;
}
@@ -828,9 +831,9 @@
}
function addWeeksToException(fromISO: string, toISO: string, dest: string[]) {
const from = new Date(fromISO + 'T00:00:00');
const to = new Date(toISO + 'T00:00:00');
const first = new Date(from);
const from = new SvelteDate(fromISO + 'T00:00:00');
const to = new SvelteDate(toISO + 'T00:00:00');
const first = new SvelteDate(from);
const day = first.getDay();
const daysToMonday = day === 0 ? -6 : 1 - day;
@@ -838,8 +841,8 @@
first.setDate(first.getDate() + daysToMonday);
// Add all Mondays in the range
for (let d = new Date(first); d <= to; d.setDate(d.getDate() + 7)) {
dest.push(isoDateOf(new Date(d)));
for (let d = new SvelteDate(first); d <= to; d.setDate(d.getDate() + 7)) {
dest.push(isoDateOf(new SvelteDate(d)));
}
}
@@ -854,7 +857,7 @@
patch_test_duration_hours: number;
minimum_age_required: number;
created_at: string;
updated_at: string;
updated_at?: string;
created_by?: string;
updated_by?: string;
};
@@ -879,7 +882,11 @@
if (response.ok) {
const data = await response.json();
services = data;
// Ensure each service has an ID
services = data.filter((s: Service) => s.id);
if (data.length !== services.length) {
console.warn('Some services missing IDs were filtered out');
}
} else {
console.error('Failed to fetch services:', response.status);
toast.error('Failed to load services');
@@ -1130,7 +1137,7 @@
});
if (response.ok) {
const createdService = await response.json();
await response.json();
toast.success('Service created successfully!', { id: loadingToast });
// Reset form and close modal
@@ -1255,7 +1262,7 @@
<Skeleton class="h-10 w-20" />
</div>
<div class="space-y-2">
{#each Array(3) as _, i}
{#each Array(3) as _, i (i)}
<Skeleton class="h-16 w-full" />
{/each}
</div>
@@ -1274,7 +1281,7 @@
<Skeleton class="h-10 w-20" />
</div>
<div class="space-y-2">
{#each Array(3) as _, i}
{#each Array(3) as _, i (i)}
<Skeleton class="h-16 w-full" />
{/each}
</div>
@@ -1293,7 +1300,7 @@
<Skeleton class="h-10 w-32" />
</div>
<div class="grid grid-cols-1 gap-4 md:grid-cols-2">
{#each Array(2) as _, i}
{#each Array(2) as _, i (i)}
<Skeleton class="h-32 w-full" />
{/each}
</div>
@@ -1322,7 +1329,7 @@
</tr>
</thead>
<tbody>
{#each Array(7) as _, i}
{#each Array(7) as _, i (i)}
<tr class="border-t">
<td class="py-2"><Skeleton class="h-4 w-20" /></td>
<td class="py-2"><Skeleton class="h-4 w-12" /></td>
@@ -1359,7 +1366,7 @@
</tr>
</thead>
<tbody>
{#each Array(3) as _, i}
{#each Array(3) as _, i (i)}
<tr class="border-b">
<td class="py-3"><Skeleton class="h-4 w-32" /></td>
<td class="py-3"><Skeleton class="h-4 w-48" /></td>
@@ -1422,7 +1429,7 @@
<div class="mt-4">
<div class="text-sm text-gray-600">Selected files ({uploadFiles.length})</div>
<div class="mt-2 max-h-40 space-y-2 overflow-y-auto">
{#each uploadFiles as f}
{#each uploadFiles as f (f.name)}
<div class="flex items-center justify-between rounded bg-gray-50 p-2">
<div>{f.name}{Math.round(f.size / 1024)}KB</div>
<button
@@ -1437,7 +1444,7 @@
{#if uploadResults.length > 0}
<div class="mt-4 text-sm text-gray-600">Upload Results</div>
<div class="mt-2 max-h-40 space-y-2 overflow-y-auto">
{#each uploadResults as result}
{#each uploadResults as result (result.url || result.name)}
<div
class="rounded p-2 text-xs {result.error
? 'bg-red-100 text-red-800'
@@ -1503,7 +1510,7 @@
</div>
<div class="mt-3 max-h-60 space-y-2 overflow-y-auto">
{#each users as u}
{#each users as u, i (i)}
<div class="flex items-center justify-between rounded bg-gray-50 p-2">
<div>
<div class="font-medium">{u.fn || `${u.n_first_name} ${u.n_last_name}`}</div>
@@ -1566,15 +1573,19 @@
{:else if bookings.length === 0}
<div class="text-center text-sm text-gray-500">No bookings found.</div>
{:else}
{#each bookings as b}
{#each bookings as b, i (i)}
<div class="flex items-center justify-between rounded bg-gray-50 p-2">
<div class="flex-1">
<div class="font-medium">
{(() => {
const date = new Date(b.start_time);
const now = new Date();
const today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
const bookingDate = new Date(
const date = new SvelteDate(b.start_time);
const now = new SvelteDate();
const today = new SvelteDate(
now.getFullYear(),
now.getMonth(),
now.getDate()
);
const bookingDate = new SvelteDate(
date.getFullYear(),
date.getMonth(),
date.getDate()
@@ -1694,11 +1705,17 @@
{b.status}
</span>
<span>• {b.user?.full_name || 'Unknown User'}</span>
<span
>• {(b.services || [])
.map((s) => s.service_name || 'Unknown Service')
.join(', ')}</span
>
<span>
- {(() => {
const services = (b.services || []).map(
(s) => s.service_name || 'Unknown Service'
);
if (services.length === 0) return 'No services';
if (services.length === 1) return services[0];
if (services.length === 2) return services.join(' and ');
return `${services[0]} and ${services.length - 1} other${services.length - 1 > 1 ? 's' : ''}`;
})()}
</span>
</div>
</div>
<Button variant="outline" onclick={() => openBookingModal(b.id)}>View</Button>
@@ -1740,7 +1757,7 @@
<Card.Content class="space-y-4">
{#if exceptionGroupsLoading}
<div class="grid grid-cols-1 gap-4 md:grid-cols-2">
{#each Array(2) as _, i}
{#each Array(2) as _, i (i)}
<Skeleton class="h-32 w-full" />
{/each}
</div>
@@ -1750,7 +1767,7 @@
<p class="col-span-2 text-sm text-gray-500">No exception groups found.</p>
{/if}
{#each exceptionGroups as g}
{#each exceptionGroups as g (g.weekStarts)}
<div class="group relative h-full rounded-lg border p-4 transition-all">
<div class="flex h-full flex-col gap-3">
<div class="flex-1">
@@ -1786,7 +1803,7 @@
{g.weekStarts
?.slice(0, 3)
.map((w) =>
new Date(w).toLocaleDateString('en-GB', {
new SvelteDate(w).toLocaleDateString('en-GB', {
day: 'numeric',
month: 'short'
})
@@ -1891,7 +1908,7 @@
</tr>
</thead>
<tbody class="divide-y divide-gray-200">
{#each defaultHours as row}
{#each defaultHours as row (row.weekday)}
<tr class="transition-colors hover:bg-gray-50">
<td class="p-3">
<div class="flex items-center gap-2">
@@ -1986,7 +2003,7 @@
<!-- Mobile Cards -->
<div class="space-y-3 sm:hidden">
{#each defaultHours as row}
{#each defaultHours as row (row.weekday)}
<div class="rounded-lg border p-4 transition-colors hover:bg-gray-50">
<div class="mb-3 flex items-center justify-between">
<div class="flex items-center gap-2">
@@ -2051,7 +2068,7 @@
<table class="w-full table-auto border-collapse">
<thead>
<tr
class="border-b bg-gray-50 text-left text-xs font-medium uppercase tracking-wider text-gray-600"
class="border-b bg-gray-50 text-left text-xs font-medium tracking-wider text-gray-600 uppercase"
>
<th class="px-4 py-3">Day</th>
<th class="px-4 py-3 text-center">Status</th>
@@ -2061,7 +2078,7 @@
</tr>
</thead>
<tbody class="divide-y divide-gray-200">
{#each Array(7) as _, i}
{#each Array(7) as _, i (i)}
<tr>
<td class="px-4 py-4">
<div class="flex items-center gap-2">
@@ -2089,7 +2106,7 @@
<!-- Mobile Skeleton -->
<div class="space-y-3 md:hidden">
{#each Array(7) as _, i}
{#each Array(7) as _, i (i)}
<div class="rounded-lg border p-4">
<div class="mb-3 flex items-center justify-between">
<div class="flex items-center gap-2">
@@ -2151,7 +2168,7 @@
</thead>
<tbody>
{#if servicesLoading}
{#each Array(3) as _, i}
{#each Array(3) as _, i (i)}
<tr class="border-b">
<td class="py-3"><Skeleton class="h-4 w-32" /></td>
<td class="py-3"><Skeleton class="h-4 w-48" /></td>
@@ -2167,7 +2184,7 @@
</tr>
{/each}
{:else}
{#each services as service}
{#each services as service (service.id)}
<tr class="border-b hover:bg-gray-50">
<td class="py-3 font-medium">{service.name}</td>
<td class="py-3 text-gray-600">
@@ -2224,7 +2241,7 @@
<!-- Mobile Cards (keep the same as before) -->
<div class="space-y-4 md:hidden">
{#if servicesLoading}
{#each Array(3) as _, i}
{#each Array(3) as _, i (i)}
<div class="rounded-lg border p-4">
<div class="space-y-3">
<Skeleton class="h-5 w-32" />
@@ -2241,7 +2258,7 @@
</div>
{/each}
{:else}
{#each services as service}
{#each services as service (service.id)}
<div class="rounded-lg border p-4 hover:bg-gray-50">
<div class="space-y-3">
<div class="flex items-start justify-between">
@@ -2330,14 +2347,14 @@
</tr>
</thead>
<tbody>
{#each defaultHoursDraft as row}
{#each defaultHoursDraft as row (row.weekday)}
<tr class="border-t">
<td class="py-2 text-sm">{weekdayLabel(row.weekday)}</td>
<td class="py-2">
<input
type="checkbox"
bind:checked={row.is_open}
class="text-primary focus:ring-primary h-4 w-4 rounded border-gray-300 bg-gray-100"
class="h-4 w-4 rounded border-gray-300 bg-gray-100 text-primary focus:ring-primary"
/>
</td>
<td class="py-2">
@@ -2461,7 +2478,7 @@
Selected weeks ({exceptionDraft.weekStarts.length}):
</div>
<div class="max-h-32 space-y-1 overflow-y-auto rounded border p-2">
{#each exceptionDraft.weekStarts as week, index}
{#each exceptionDraft.weekStarts as week, index (week)}
<div class="flex items-center justify-between text-sm">
<span>Week starting: {week}</span>
<button
@@ -2493,7 +2510,7 @@
</tr>
</thead>
<tbody>
{#each exceptionDraft.hours as row}
{#each exceptionDraft.hours as row (row.weekday)}
<tr class="border-t">
<td class="py-2">{weekdayLabel(row.weekday)}</td>
<td class="py-2">
@@ -2586,9 +2603,9 @@
<div class="max-h-48 space-y-1 overflow-y-auto rounded border bg-gray-50 p-3">
{#if viewingException.weekStarts && viewingException.weekStarts.length > 0}
<div class="grid grid-cols-2 gap-2 md:grid-cols-3">
{#each viewingException.weekStarts as week}
{#each viewingException.weekStarts as week (week)}
<div class="text-sm">
Week of {new Date(week).toLocaleDateString('en-GB', {
Week of {new SvelteDate(week).toLocaleDateString('en-GB', {
day: 'numeric',
month: 'short',
year: 'numeric'
@@ -2618,7 +2635,7 @@
</tr>
</thead>
<tbody>
{#each viewingException.hours as row}
{#each viewingException.hours as row (row.weekday)}
<tr class="border-t">
<td class="py-2 text-sm">{weekdayLabel(row.weekday)}</td>
<td class="py-2">
@@ -2669,7 +2686,7 @@
<div class="font-medium">{selectedUser.phone}</div>
<div class="mt-2 text-sm text-gray-500">Joined</div>
<div class="font-medium">
{new Date(selectedUser.created_at || '').toLocaleString()}
{new SvelteDate(selectedUser.created_at || '').toLocaleString()}
</div>
</div>
@@ -2693,10 +2710,10 @@
<div class="text-sm text-gray-500">No recent bookings</div>
{/if}
<div class="space-y-2">
{#each bookingUserHistory as hb}
{#each bookingUserHistory as hb (hb.id)}
<div class="flex items-center justify-between rounded bg-gray-50 p-2">
<div>
<div class="font-medium">{new Date(hb.start_time).toLocaleString()}</div>
<div class="font-medium">{new SvelteDate(hb.start_time).toLocaleString()}</div>
<div class="text-xs text-gray-500">
{hb.status}{hb.services.map((s) => s.service_name).join(', ')}
</div>
@@ -2751,7 +2768,7 @@
<!-- Customer Information -->
<div class="rounded-lg border border-gray-200 bg-gray-50 p-4">
<h3 class="mb-3 text-sm font-semibold uppercase tracking-wide text-gray-600">
<h3 class="mb-3 text-sm font-semibold tracking-wide text-gray-600 uppercase">
Customer Information
</h3>
<div class="grid gap-3 md:grid-cols-2">
@@ -2761,7 +2778,7 @@
</div>
<div>
<div class="text-xs text-gray-500">Email</div>
<div class="break-all font-medium">{selectedBooking.user?.email || '—'}</div>
<div class="font-medium break-all">{selectedBooking.user?.email || '—'}</div>
</div>
<div>
<div class="text-xs text-gray-500">Phone</div>
@@ -2800,14 +2817,14 @@
<!-- Appointment Details -->
<div class="rounded-lg border border-gray-200 bg-gray-50 p-4">
<h3 class="mb-3 text-sm font-semibold uppercase tracking-wide text-gray-600">
<h3 class="mb-3 text-sm font-semibold tracking-wide text-gray-600 uppercase">
Appointment Details
</h3>
<div class="grid gap-3 md:grid-cols-2">
<div>
<div class="text-xs text-gray-500">Scheduled Date & Time</div>
<div class="font-medium">
{new Date(selectedBooking.start_time).toLocaleString()}
{new SvelteDate(selectedBooking.start_time).toLocaleString()}
</div>
</div>
<div>
@@ -2816,11 +2833,15 @@
</div>
<div>
<div class="text-xs text-gray-500">Created</div>
<div class="text-sm">{new Date(selectedBooking.created_at).toLocaleString()}</div>
<div class="text-sm">
{new SvelteDate(selectedBooking.created_at).toLocaleString()}
</div>
</div>
<div>
<div class="text-xs text-gray-500">Last Updated</div>
<div class="text-sm">{new Date(selectedBooking.updated_at).toLocaleString()}</div>
<div class="text-sm">
{new SvelteDate(selectedBooking.updated_at).toLocaleString()}
</div>
</div>
{#if selectedBooking.created_by}
<div class="md:col-span-2">
@@ -2840,11 +2861,11 @@
<!-- Services -->
{#if selectedBooking.services && selectedBooking.services.length > 0}
<div class="rounded-lg border border-gray-200 bg-gray-50 p-4">
<h3 class="mb-3 text-sm font-semibold uppercase tracking-wide text-gray-600">
<h3 class="mb-3 text-sm font-semibold tracking-wide text-gray-600 uppercase">
Services
</h3>
<div class="space-y-3">
{#each selectedBooking.services as service}
{#each selectedBooking.services as service (service.service_id)}
<div class="rounded-md border border-gray-300 bg-white p-3">
<div class="font-medium">{service.service_name || '—'}</div>
{#if service.service_description}
@@ -2862,7 +2883,7 @@
<!-- Financial Summary -->
<div class="rounded-lg border border-gray-200 bg-gray-50 p-4">
<h3 class="mb-3 text-sm font-semibold uppercase tracking-wide text-gray-600">
<h3 class="mb-3 text-sm font-semibold tracking-wide text-gray-600 uppercase">
Financial Summary
</h3>
<div class="space-y-2">
@@ -2892,11 +2913,11 @@
<!-- Payments -->
{#if selectedBooking.payments && selectedBooking.payments.length > 0}
<div class="rounded-lg border border-gray-200 bg-gray-50 p-4">
<h3 class="mb-3 text-sm font-semibold uppercase tracking-wide text-gray-600">
<h3 class="mb-3 text-sm font-semibold tracking-wide text-gray-600 uppercase">
Payment History
</h3>
<div class="space-y-3">
{#each selectedBooking.payments as payment}
{#each selectedBooking.payments as payment (payment.id)}
<div class="rounded-md border border-gray-300 bg-white p-3">
<div class="flex items-start justify-between">
<div class="flex-1">
@@ -2941,7 +2962,7 @@
</div>
{/if}
<div class="mt-1 text-xs text-gray-400">
{new Date(payment.created_at).toLocaleString()}
{new SvelteDate(payment.created_at).toLocaleString()}
</div>
</div>
<div class="text-right font-semibold">
@@ -3007,7 +3028,7 @@
<div class="space-y-2">
<label for="service-price" class="text-sm font-medium">Price (£) *</label>
<div class="relative">
<span class="absolute left-3 top-1/2 -translate-y-1/2 text-sm text-gray-500">£</span
<span class="absolute top-1/2 left-3 -translate-y-1/2 text-sm text-gray-500">£</span
>
<Input
id="service-price"
+30 -86
View File
@@ -9,6 +9,7 @@
import { CalendarDate, getLocalTimeZone, type DateValue } from '@internationalized/date';
import { authStore } from '$lib/stores/auth.svelte';
import { toast } from 'svelte-sonner';
import { SvelteMap, SvelteDate } from 'svelte/reactivity';
// Booking state
let currentStep = $state<number>(1);
@@ -79,21 +80,21 @@
let loadingWorkingHours = $state<boolean>(false);
let loadingAvailableHours = $state<boolean>(false);
const workingHoursCache = new Map<
const workingHoursCache = new SvelteMap<
string,
Record<string, { isOpen: boolean; startTime: string; endTime: string }>
>();
const availableHoursCache = new Map<
const availableHoursCache = new SvelteMap<
string,
Record<string, { isOpen: boolean; slots: Array<{ startTime: string; endTime: string }> }>
>();
// Initialize date boundaries
const today = new Date();
const tomorrow = new Date(today);
const today = new SvelteDate();
const tomorrow = new SvelteDate(today);
tomorrow.setDate(today.getDate() + 1);
const maxDate = new Date();
const maxDate = new SvelteDate();
maxDate.setMonth(today.getMonth() + 6);
// Create CalendarDate objects
@@ -228,11 +229,11 @@
function setDefaultSelectedDate(
hoursMap: Record<string, { isOpen: boolean; startTime: string; endTime: string }>
) {
const currentDate = new Date();
let nextDate = new Date(currentDate);
const currentDate = new SvelteDate();
let nextDate = new SvelteDate(currentDate);
for (let i = 1; i < 30; i++) {
nextDate = new Date(currentDate);
nextDate = new SvelteDate(currentDate);
nextDate.setDate(currentDate.getDate() + i);
const dateStr = nextDate.toISOString().split('T')[0];
@@ -247,7 +248,7 @@
}
if (!selectedDate) {
const tomorrow = new Date();
const tomorrow = new SvelteDate();
tomorrow.setDate(tomorrow.getDate() + 1);
selectedDate = new CalendarDate(
tomorrow.getFullYear(),
@@ -262,7 +263,7 @@
*/
function calculateEndTime(startTime: string, durationMinutes: number): string {
const [hours, minutes] = startTime.split(':').map(Number);
const date = new Date();
const date = new SvelteDate();
date.setHours(hours, minutes, 0, 0);
date.setMinutes(date.getMinutes() + durationMinutes);
const endHours = date.getHours().toString().padStart(2, '0');
@@ -363,7 +364,7 @@
let startTotalMinutes = startHour * 60 + startMinute;
const endTotalMinutes = endHour * 60 + endMinute;
const now = new Date();
const now = new SvelteDate();
const today = new CalendarDate(now.getFullYear(), now.getMonth() + 1, now.getDate());
const isToday = date.compare(today) === 0;
@@ -483,7 +484,7 @@
}
const slots: string[] = [];
const now = new Date();
const now = new SvelteDate();
const today = new CalendarDate(now.getFullYear(), now.getMonth() + 1, now.getDate());
const isToday = date.compare(today) === 0;
@@ -529,7 +530,7 @@
return selectedServices.reduce((total, service: Service) => total + service.price, 0);
}
function toggleService(service: any) {
function toggleService(service: Service) {
const index = selectedServices.findIndex((s) => s.id === service.id);
const wasSelected = index >= 0;
@@ -553,70 +554,10 @@
}
}
function isServiceSelected(service: any) {
function isServiceSelected(service: Service) {
return selectedServices.some((s) => s.id === service.id);
}
const allTimeSlots = $derived(
selectedServices.length > 0 && selectedDate
? generateAllTimeSlots(getTotalDuration(), selectedDate)
: []
);
function generateAllTimeSlots(
duration: number,
date: CalendarDate | undefined
): Array<{ time: string; type: 'available' | 'unavailable' }> {
if (!date || !workingHours) {
return [];
}
const dateStr = date.toString();
const dayWorkingHours = workingHours[dateStr];
if (!dayWorkingHours || !dayWorkingHours.isOpen) {
return [];
}
const allSlots: Array<{ time: string; type: 'available' | 'unavailable' }> = [];
const [startHour, startMinute] = dayWorkingHours.startTime.split(':').map(Number);
const [endHour, endMinute] = dayWorkingHours.endTime.split(':').map(Number);
let startTotalMinutes = startHour * 60 + startMinute;
const endTotalMinutes = endHour * 60 + endMinute;
const now = new Date();
const today = new CalendarDate(now.getFullYear(), now.getMonth() + 1, now.getDate());
const isToday = date.compare(today) === 0;
// Apply 2-hour buffer for today's appointments
if (isToday) {
const currentMinutes = now.getHours() * 60 + now.getMinutes();
const minimumStartMinutes = currentMinutes + 120;
startTotalMinutes = Math.max(startTotalMinutes, minimumStartMinutes);
}
// Get available time slots that fit our duration
const availableSlots = generateAvailableTimeSlots(duration, date);
// Generate all 15-minute increments within working hours
for (let minutes = startTotalMinutes; minutes < endTotalMinutes; minutes += 15) {
const hour = Math.floor(minutes / 60);
const minute = minutes % 60;
const timeStr = `${String(hour).padStart(2, '0')}:${String(minute).padStart(2, '0')}`;
// Check if this time slot is available (fits duration and within available hours)
const isAvailable = availableSlots.includes(timeStr);
allSlots.push({
time: timeStr,
type: isAvailable ? 'available' : 'unavailable'
});
}
return allSlots;
}
function formatDuration(minutes: number): string {
const hours = Math.floor(minutes / 60);
const remainingMinutes = minutes % 60;
@@ -631,9 +572,12 @@
}
function getDayWithOrdinal(date: CalendarDate): string {
const monthName = new Date(date.year, date.month - 1, date.day).toLocaleDateString('en-GB', {
month: 'long'
});
const monthName = new SvelteDate(date.year, date.month - 1, date.day).toLocaleDateString(
'en-GB',
{
month: 'long'
}
);
const day = date.day;
if (day > 3 && day < 21) return monthName + ' ' + day + 'th';
switch (day % 10) {
@@ -695,7 +639,7 @@
<!-- Progress Indicator -->
<div class="mb-8 grid grid-cols-2 gap-4 md:flex md:items-center md:justify-center md:space-x-4">
{#each ['Service', 'Date & Time', 'Details', 'Payment'] as step, index}
{#each ['Service', 'Date & Time', 'Details', 'Payment'] as step, index (step)}
<div class="flex items-center justify-start md:justify-center">
<div
class="flex h-8 w-8 items-center justify-center rounded-full text-sm font-medium {index +
@@ -738,10 +682,10 @@
{:else if services.length === 0}
<p>No services available at the moment.</p>
{:else}
{#each services as service}
{#each services as service (service.id)}
<button
type="button"
class="focus:ring-primary cursor-pointer rounded-lg p-4 text-left shadow-sm transition-colors hover:bg-fuchsia-50 {isServiceSelected(
class="cursor-pointer rounded-lg p-4 text-left shadow-sm transition-colors hover:bg-fuchsia-50 focus:ring-primary {isServiceSelected(
service
)
? 'bg-fuchsia-100'
@@ -785,7 +729,7 @@
<div class="rounded-lg bg-gray-50 p-4">
<h4 class="mb-2 font-semibold">Selected Services</h4>
<div class="space-y-2">
{#each selectedServices as service}
{#each selectedServices as service (service.id)}
<div class="flex justify-between text-sm">
<span>{service.name}</span>
<span>{service.duration_minutes} mins • £{service.price}</span>
@@ -828,7 +772,7 @@
bind:value={selectedDate}
bind:placeholder
{isDateUnavailable}
class="data-unavailable:line-through data-unavailable:opacity-100 bg-transparent p-0 [--cell-size:--spacing(10)] md:[--cell-size:--spacing(12)] [&_[data-outside-month]]:pointer-events-none [&_[data-outside-month]]:opacity-0"
class="bg-transparent p-0 [--cell-size:--spacing(10)] data-unavailable:line-through data-unavailable:opacity-100 md:[--cell-size:--spacing(12)] [&_[data-outside-month]]:pointer-events-none [&_[data-outside-month]]:opacity-0"
weekdayFormat="short"
minValue={minDate}
maxValue={maxCalendarDate}
@@ -836,7 +780,7 @@
/>
</div>
<div
class="no-scrollbar inset-y-0 right-0 flex max-h-48 w-full scroll-pb-6 flex-col gap-4 overflow-y-auto border-t p-6 md:absolute md:max-h-none md:w-56 md:border-l md:border-t-0"
class="no-scrollbar inset-y-0 right-0 flex max-h-48 w-full scroll-pb-6 flex-col gap-4 overflow-y-auto border-t p-6 md:absolute md:max-h-none md:w-56 md:border-t-0 md:border-l"
>
{#if (loadingWorkingHours || loadingAvailableHours) && selectedDate}
<div class="text-center text-sm text-gray-500">Loading available times...</div>
@@ -903,7 +847,7 @@
{/if}
</div>
<Card.Footer class="flex justify-between border-t !py-5 px-6">
<Card.Footer class="flex justify-between border-t px-6 !py-5">
<Button variant="outline" onclick={prevStep}>Back</Button>
<div class="flex items-center space-x-4">
<!-- Desktop appointment summary - hidden on mobile -->
@@ -942,8 +886,8 @@
<div class="space-y-1 text-sm">
<div>
<span class="font-medium">Services:</span>
<div class="ml-4 mt-1 space-y-1">
{#each selectedServices as service}
<div class="mt-1 ml-4 space-y-1">
{#each selectedServices as service (service.id)}
<div class="flex justify-between">
<span>{service.name}</span>
<span>£{service.price}</span>
+11 -6
View File
@@ -5,6 +5,7 @@
import { Separator } from '$lib/components/ui/separator/index.js';
import { Checkbox } from '$lib/components/ui/checkbox/index.js';
import RequiredLabel from '$lib/components/layout/RequiredLabel.svelte';
import { SvelteDate } from 'svelte/reactivity';
// zxcvbn-ts imports
import { zxcvbn, zxcvbnOptions } from '@zxcvbn-ts/core';
@@ -64,7 +65,7 @@
// Email validation
function validateEmail(email: string): boolean {
// ASCII-only email regex (safe with most providers)
const asciiRegex = /^[a-zA-Z0-9._%+\-]+@[a-zA-Z0-9.\-]+\.[a-zA-Z]{2,}$/;
const asciiRegex = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/;
// Unicode-friendly regex (valid per RFC 6531)
const unicodeRegex = /^[\p{L}\p{M}0-9._%+-]+@[\p{L}\p{M}0-9.-]+\.[\p{L}\p{M}]{2,}$/u;
@@ -155,9 +156,13 @@
return true;
}
const dob = new Date(dateStr);
const today = new Date();
const sixteenYearsAgo = new Date(today.getFullYear() - 16, today.getMonth(), today.getDate());
const dob = new SvelteDate(dateStr);
const today = new SvelteDate();
const sixteenYearsAgo = new SvelteDate(
today.getFullYear() - 16,
today.getMonth(),
today.getDate()
);
const isValid = dob <= sixteenYearsAgo;
validationErrors.dateOfBirth = isValid
@@ -406,7 +411,7 @@
type="date"
bind:value={formData.dateOfBirth}
onblur={() => validateAge(formData.dateOfBirth)}
max={new Date(new Date().setFullYear(new Date().getFullYear() - 16))
max={new SvelteDate(new SvelteDate().setFullYear(new SvelteDate().getFullYear() - 16))
.toISOString()
.split('T')[0]}
required
@@ -467,7 +472,7 @@
</p>
{#if passwordStrength.feedback.suggestions.length > 0}
<ul class="mt-1 ml-4 list-disc text-xs text-muted-foreground">
{#each passwordStrength.feedback.suggestions as suggestion}
{#each passwordStrength.feedback.suggestions as suggestion (suggestion)}
<li>{suggestion}</li>
{/each}
</ul>