Fix booking count filters, add admin notifications, and improve bookings

UI
This commit is contained in:
2026-01-06 16:45:17 +00:00
parent 08e4e12a73
commit f536a7fd04
10 changed files with 436 additions and 177 deletions
+13 -11
View File
@@ -76,17 +76,19 @@
<PortfolioCarousel />
<section class="bg-gray-50 py-16">
<div class="mx-auto max-w-3xl px-6 text-center">
<h2 class="mb-4 text-2xl font-semibold">Why Choose Us?</h2>
<p class="leading-relaxed text-gray-600">
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed non risus. Suspendisse lectus
tortor, dignissim sit amet, adipiscing nec, ultricies sed, dolor. Cras elementum ultrices
diam. Maecenas ligula massa, varius a, semper congue, euismod non, mi. Proin porttitor, orci
nec nonummy molestie,
</p>
</div>
</section>
{#if !authStore.isAuthenticated}
<section class="bg-gray-50 py-16">
<div class="mx-auto max-w-3xl px-6 text-center">
<h2 class="mb-4 text-2xl font-semibold">Why Choose Us?</h2>
<p class="leading-relaxed text-gray-600">
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed non risus. Suspendisse lectus
tortor, dignissim sit amet, adipiscing nec, ultricies sed, dolor. Cras elementum ultrices
diam. Maecenas ligula massa, varius a, semper congue, euismod non, mi. Proin porttitor, orci
nec nonummy molestie,
</p>
</div>
</section>
{/if}
<section class="py-20 text-center">
<h2 class="mb-4 text-2xl font-semibold">Ready to Treat Yourself?</h2>
+220 -99
View File
@@ -62,9 +62,7 @@
};
let userData = $state<User | null>(null);
let bookings = $state<Booking[]>([]);
let loadingUser = $state(true);
let loadingBookings = $state(true);
let stamps = $state(0);
// =============== Fetch User Data ===============
@@ -97,12 +95,22 @@
}
// =============== Fetch Bookings ===============
async function fetchBookings() {
let upcomingBookings = $state<Booking[]>([]);
let pastBookings = $state<Booking[]>([]);
let pastPage = $state(1);
let pastTotalPages = $state(1);
let loadingUpcoming = $state(false);
let loadingPast = $state(false);
// =============== Fetch Upcoming Bookings (next 3) ===============
async function fetchUpcomingBookings() {
if (pageState !== 'authorized') return;
loadingBookings = true;
loadingUpcoming = true;
try {
const response = await fetch('/api/user/bookings', {
const today = new Date().toISOString().split('T')[0]; // YYYY-MM-DD
const response = await fetch(`/api/bookings?start_date=${today}&per_page=3&page=1`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
@@ -110,24 +118,66 @@
}
});
if (response.ok) {
const data = await response.json();
bookings = data.bookings || [];
} else {
// toast.error('Failed to load booking history');
if (!response.ok) {
const text = await response.text();
toast.error('Failed to load upcoming bookings: ' + text);
return;
}
const data = await response.json();
upcomingBookings = data.bookings || [];
} catch (err) {
console.error('Error fetching bookings:', err);
toast.error('Network error loading bookings');
console.error('Error fetching upcoming bookings:', err);
toast.error('Network error loading upcoming bookings');
} finally {
loadingBookings = false;
loadingUpcoming = false;
}
}
// =============== Fetch Past Bookings (paginated 10 per page) ===============
async function fetchPastBookings(page = 1) {
if (pageState !== 'authorized') return;
loadingPast = true;
try {
const today = new Date().toISOString().split('T')[0]; // YYYY-MM-DD
const response = await fetch(`/api/bookings?end_date=${today}&per_page=10&page=${page}`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${authStore.currentToken}`
}
});
if (!response.ok) {
const text = await response.text();
toast.error('Failed to load past bookings: ' + text);
return;
}
const data = await response.json();
pastBookings = data.bookings || [];
pastPage = data.page || page;
pastTotalPages = Math.ceil((data.total || 0) / (data.per_page || 10));
} catch (err) {
console.error('Error fetching past bookings:', err);
toast.error('Network error loading past bookings');
} finally {
loadingPast = false;
}
}
// =============== Pagination Helpers ===============
function goToPastPage(page: number) {
if (page < 1 || page > pastTotalPages) return;
fetchPastBookings(page);
}
$effect(() => {
if (pageState === 'authorized') {
fetchUserData();
fetchBookings();
fetchUpcomingBookings();
fetchPastBookings();
}
});
@@ -231,13 +281,29 @@
function formatDateTime(dateString: string): string {
const date = new SvelteDate(dateString);
return date.toLocaleString('en-GB', {
day: 'numeric',
month: 'short',
year: 'numeric',
hour: '2-digit',
minute: '2-digit'
});
// Get date parts
const day = date.getDate();
const month = date.toLocaleString('en-GB', { month: 'short' });
const year = date.getFullYear();
// Get time parts
const hours = date.getHours();
const minutes = date.getMinutes();
// Special cases for midnight and noon
let timeStr;
if (hours === 12 && minutes === 0) {
timeStr = 'Noon';
} else if (hours === 0 && minutes === 0) {
timeStr = 'Midnight';
} else {
const period = hours >= 12 ? 'PM' : 'AM';
const displayHours = hours % 12 || 12;
timeStr = `${displayHours}:${minutes.toString().padStart(2, '0')} ${period}`;
}
return `${day} ${month} ${year}, ${timeStr}`;
}
</script>
@@ -424,97 +490,152 @@
</Card.Content>
</Card.Root>
{:else if activeTab === 'history'}
<!-- Booking History -->
<Card.Root>
<!-- Upcoming Bookings -->
<Card.Root class="mb-6">
<Card.Header>
<Card.Title>Booking History</Card.Title>
<Card.Description>Your past and upcoming appointments</Card.Description>
<Card.Title>Upcoming Appointments</Card.Title>
<Card.Description>Next 3 upcoming bookings</Card.Description>
</Card.Header>
<Card.Content class="space-y-4">
{#if loadingBookings}
<Card.Content class="space-y-2">
{#if loadingUpcoming}
{#each Array(3) as _, i (i)}
<Skeleton class="h-32 w-full" />
<Skeleton class="h-16 w-full" />
{/each}
{:else if bookings.length === 0}
<div class="py-8 text-center text-gray-500">No bookings found</div>
{:else if upcomingBookings.length === 0}
<div class="py-4 text-center text-gray-500">No upcoming bookings</div>
{:else}
{#each bookings as booking (booking.id)}
<div class="rounded-lg border p-4 hover:bg-gray-50">
<div class="mb-3 flex items-start justify-between">
<div>
<div class="font-semibold">{formatDateTime(booking.start_time)}</div>
<div class="mt-1 text-xs text-gray-500">
{booking.duration_minutes} minutes
</div>
</div>
<span
class="inline-flex items-center rounded-full px-3 py-1 text-xs font-medium {booking.status ===
'confirmed'
? 'bg-emerald-100 text-emerald-800'
: booking.status === 'pending'
? 'bg-amber-100 text-amber-800'
: booking.status === 'completed'
? 'bg-green-100 text-green-800'
: 'bg-gray-100 text-gray-800'}"
>
{booking.status}
</span>
</div>
<!-- Services -->
{#if booking.services && booking.services.length > 0}
<div class="mb-3 space-y-2">
<div class="text-xs font-medium text-gray-600">Services:</div>
{#each booking.services as service (service.service_name)}
<div class="flex items-center justify-between text-sm">
<span>{service.service_name}</span>
<span class="font-medium">£{service.price.toFixed(2)}</span>
</div>
{/each}
</div>
{/if}
<Separator class="my-3" />
<!-- Payment Summary -->
<div class="space-y-1 text-sm">
<div class="flex justify-between">
<span class="text-gray-600">Total:</span>
<span class="font-semibold">£{booking.total_amount.toFixed(2)}</span>
</div>
<div class="flex justify-between">
<span class="text-gray-600">Paid:</span>
<span class="font-semibold text-green-700"
>£{booking.amount_paid.toFixed(2)}</span
{#each upcomingBookings as b (b.id)}
<div class="flex items-center justify-between rounded bg-gray-50 p-2">
<div class="flex-1">
<div class="font-medium">{formatDateTime(b.start_time)}</div>
<div class="mt-1 flex items-center gap-2 text-xs text-gray-500">
<span
class="inline-flex items-center rounded-full px-2 py-0.5 text-xs font-medium {b.status ===
'confirmed'
? 'bg-emerald-100 text-emerald-800'
: b.status === 'pending'
? 'bg-amber-100 text-amber-800'
: b.status === 'in_progress'
? 'bg-blue-100 text-blue-800'
: b.status === 'completed'
? 'bg-green-100 text-green-800'
: 'bg-gray-100 text-gray-800'}"
>
<span
class="mr-1 h-1.5 w-1.5 rounded-full {b.status === 'confirmed'
? 'bg-emerald-600'
: b.status === 'pending'
? 'bg-amber-600'
: b.status === 'in_progress'
? 'bg-blue-600'
: b.status === 'completed'
? 'bg-green-600'
: 'bg-gray-600'}"
></span>
{b.status}
</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>
{#if booking.amount_due > 0}
<div class="flex justify-between border-t pt-1">
<span class="font-medium text-gray-900">Due:</span>
<span class="font-bold text-red-600">£{booking.amount_due.toFixed(2)}</span>
</div>
{/if}
</div>
<!-- Payments -->
{#if booking.payments && booking.payments.length > 0}
<div class="mt-3 space-y-1 rounded-lg bg-gray-50 p-2">
<div class="text-xs font-medium text-gray-600">Payments:</div>
{#each booking.payments as payment (payment.id)}
<div class="flex items-center justify-between text-xs">
<span class="capitalize"
>{payment.payment_method.replace('_', ' ')} ({payment.payment_type})</span
>
<span class="font-medium">£{payment.amount.toFixed(2)}</span>
</div>
{/each}
</div>
{/if}
<Button variant="outline" onclick={() => goto(`/bookings/${b.id}`)}>View</Button>
</div>
{/each}
{/if}
</Card.Content>
</Card.Root>
<!-- Past Bookings -->
<Card.Root>
<Card.Header>
<Card.Title>Past Appointments</Card.Title>
<Card.Description>Previous bookings</Card.Description>
</Card.Header>
<Card.Content class="space-y-2">
{#if loadingPast}
{#each Array(5) as _, i (i)}
<Skeleton class="h-16 w-full" />
{/each}
{:else if pastBookings.length === 0}
<div class="py-4 text-center text-gray-500">No past bookings</div>
{:else}
{#each pastBookings as b (b.id)}
<div class="flex items-center justify-between rounded bg-gray-50 p-2">
<div class="flex-1">
<div class="font-medium">{formatDateTime(b.start_time)}</div>
<div class="mt-1 flex items-center gap-2 text-xs text-gray-500">
<span
class="inline-flex items-center rounded-full px-2 py-0.5 text-xs font-medium {b.status ===
'confirmed'
? 'bg-emerald-100 text-emerald-800'
: b.status === 'pending'
? 'bg-amber-100 text-amber-800'
: b.status === 'in_progress'
? 'bg-blue-100 text-blue-800'
: b.status === 'completed'
? 'bg-green-100 text-green-800'
: 'bg-gray-100 text-gray-800'}"
>
<span
class="mr-1 h-1.5 w-1.5 rounded-full {b.status === 'confirmed'
? 'bg-emerald-600'
: b.status === 'pending'
? 'bg-amber-600'
: b.status === 'in_progress'
? 'bg-blue-600'
: b.status === 'completed'
? 'bg-green-600'
: 'bg-gray-600'}"
></span>
{b.status}
</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={() => goto(`/bookings/${b.id}`)}>View</Button>
</div>
{/each}
{/if}
<!-- Pagination Controls -->
{#if pastTotalPages > 1}
<div class="mt-2 flex justify-center gap-2">
<Button
variant="outline"
size="sm"
disabled={pastPage === 1}
onclick={() => goToPastPage(pastPage - 1)}>Prev</Button
>
<span class="px-2 py-1 text-sm text-gray-700">{pastPage} / {pastTotalPages}</span>
<Button
variant="outline"
size="sm"
disabled={pastPage === pastTotalPages}
onclick={() => goToPastPage(pastPage + 1)}>Next</Button
>
</div>
{/if}
</Card.Content>
</Card.Root>
{:else if activeTab === 'referral'}
<!-- Referral Program -->
<Card.Root>
+1 -1
View File
@@ -795,7 +795,7 @@
{#if selectedDate}
<div class="grid justify-center gap-2">{getDayWithOrdinal(selectedDate)}</div>
{/if}
<!-- Grouped Time Slots Grid - ORIGINAL STYLING BUT WITH GROUPED UNAVAILABLE SLOTS -->
<!-- Grouped Time Slots Grid -->
<div class="grid gap-2">
{#each groupedTimeSlots as slot (slot.startTime)}
{#if slot.type === 'available'}