linting and bugfixing
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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: '^_'
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user