added booking approvals

This commit is contained in:
2026-01-19 22:32:35 +00:00
parent 1dd37a8d22
commit 3a8ea4c98f
10 changed files with 5793 additions and 0 deletions
File diff suppressed because it is too large Load Diff
+410
View File
@@ -0,0 +1,410 @@
<script lang="ts">
import { SvelteDate } from 'svelte/reactivity';
import * as Card from '$lib/components/ui/card';
import { Button } from '$lib/components/ui/button';
import { Badge } from '$lib/components/ui/badge';
// Mock data
const currentAppointment = {
id: 'apt1',
customer: {
name: 'Sarah Johnson',
phone: '07700 900123',
profilePic: 'https://i.pravatar.cc/150?img=5'
},
services: ['Gel Polish', 'Nail Art'],
startTime: '2:00 PM',
endTime: '3:00 PM',
status: 'in_progress',
timeElapsed: 25, // minutes
notes: 'Customer requested pink gradient design',
checklist: [
{ item: 'Prep nails', done: true },
{ item: 'Apply base coat', done: true },
{ item: 'Apply color', done: false },
{ item: 'Apply top coat', done: false }
]
};
const todayAppointments = [
{
id: '1',
time: '9:00 AM',
customer: 'Emma Wilson',
service: 'Manicure',
duration: 30,
status: 'completed'
},
{
id: '2',
time: '10:00 AM',
customer: 'Olivia Brown',
service: 'Gel Polish',
duration: 45,
status: 'completed'
},
{
id: '3',
time: '11:30 AM',
customer: 'Ava Davis',
service: 'Pedicure',
duration: 60,
status: 'completed'
},
{
id: '4',
time: '1:00 PM',
customer: 'Isabella Martinez',
service: 'Acrylic Full Set',
duration: 90,
status: 'completed'
},
{
id: '5',
time: '2:00 PM',
customer: 'Sarah Johnson',
service: 'Gel Polish + Art',
duration: 60,
status: 'in_progress'
},
{
id: '6',
time: '3:30 PM',
customer: 'Mia Anderson',
service: 'Manicure',
duration: 30,
status: 'confirmed'
},
{
id: '7',
time: '4:30 PM',
customer: 'Charlotte Thomas',
service: 'Gel Polish',
duration: 45,
status: 'confirmed'
},
{
id: '8',
time: '6:00 PM',
customer: 'Amelia Garcia',
service: 'Pedicure + Gel',
duration: 75,
status: 'pending'
}
];
const upcomingArrivals = [
{ customer: 'Mia Anderson', time: '3:30 PM', service: 'Manicure', minutesUntil: 90 },
{
customer: 'Charlotte Thomas',
time: '4:30 PM',
service: 'Gel Polish',
minutesUntil: 150
}
];
const recentCheckouts = [
{ customer: 'Isabella Martinez', service: 'Acrylic Full Set', amount: 65, paid: true },
{ customer: 'Ava Davis', service: 'Pedicure', amount: 45, paid: true },
{ customer: 'Olivia Brown', service: 'Gel Polish', amount: 35, paid: false }
];
const loyaltyToday = [
{ customer: 'Emma Wilson', stamps: 10, action: 'Redeemed free service' },
{ customer: 'Olivia Brown', stamps: 9, action: 'One away from free!' },
{ customer: 'Ava Davis', stamps: 7, action: 'Earned 1 stamp' }
];
const today = new SvelteDate();
const dateString = today.toLocaleDateString('en-US', {
weekday: 'long',
year: 'numeric',
month: 'long',
day: 'numeric'
});
function getStatusColor(status: string) {
switch (status) {
case 'completed':
return 'bg-green-100 text-green-800';
case 'in_progress':
return 'bg-blue-100 text-blue-800';
case 'confirmed':
return 'bg-yellow-100 text-yellow-800';
case 'pending':
return 'bg-red-100 text-red-800';
default:
return 'bg-gray-100 text-gray-800';
}
}
</script>
<div class="mx-auto max-w-7xl space-y-6 p-6">
<!-- Header -->
<div class="mb-6">
<h1 class="text-3xl font-bold">Today's Schedule</h1>
<p class="text-gray-600">{dateString}</p>
</div>
<!-- Current/Next Appointment Card -->
<Card.Root class="border-2 border-blue-200 bg-gradient-to-r from-blue-50 to-purple-50">
<Card.Header>
<div class="flex items-start justify-between">
<div>
<Card.Title class="text-2xl">Current Appointment</Card.Title>
<Card.Description class="text-base">
{currentAppointment.startTime} - {currentAppointment.endTime}
</Card.Description>
</div>
<Badge class="bg-blue-100 px-3 py-1 text-sm text-blue-800">
In Progress • {currentAppointment.timeElapsed} min elapsed
</Badge>
</div>
</Card.Header>
<Card.Content>
<div class="grid gap-6 md:grid-cols-3">
<!-- Customer Info -->
<div class="flex items-center gap-4">
<img
src={currentAppointment.customer.profilePic}
alt={currentAppointment.customer.name}
class="h-20 w-20 rounded-full object-cover ring-4 ring-blue-200"
/>
<div>
<div class="text-lg font-semibold">{currentAppointment.customer.name}</div>
<div class="text-sm text-gray-600">{currentAppointment.customer.phone}</div>
<div class="mt-1 flex flex-wrap gap-1">
{#each currentAppointment.services as service}
<Badge variant="outline" class="text-xs">{service}</Badge>
{/each}
</div>
</div>
</div>
<!-- Service Checklist -->
<div>
<div class="mb-2 text-sm font-semibold text-gray-700">Service Progress</div>
<div class="space-y-2">
{#each currentAppointment.checklist as item}
<label class="flex items-center gap-2 text-sm">
<input
type="checkbox"
checked={item.done}
class="h-4 w-4 rounded border-gray-300"
/>
<span class={item.done ? 'text-gray-400 line-through' : ''}>{item.item}</span>
</label>
{/each}
</div>
</div>
<!-- Notes & Actions -->
<div class="space-y-3">
{#if currentAppointment.notes}
<div class="rounded-md border border-amber-200 bg-amber-50 p-3">
<div class="text-xs font-semibold text-amber-800">Notes</div>
<div class="text-sm text-amber-900">{currentAppointment.notes}</div>
</div>
{/if}
<div class="grid grid-cols-2 gap-2">
<Button size="sm" variant="outline">Edit</Button>
<Button size="sm" variant="outline">Extend</Button>
<Button size="sm" class="bg-green-600 hover:bg-green-700">Payment</Button>
<Button size="sm" variant="destructive">Cancel</Button>
</div>
</div>
</div>
</Card.Content>
</Card.Root>
<!-- Main Content Grid -->
<div class="grid grid-cols-1 gap-6 lg:grid-cols-3">
<!-- Today's Calendar -->
<div class="lg:col-span-2">
<Card.Root>
<Card.Header>
<Card.Title>Today's Appointments</Card.Title>
<Card.Description>Timeline view of all bookings</Card.Description>
</Card.Header>
<Card.Content>
<div class="space-y-3">
{#each todayAppointments as apt}
<div
class="flex items-center gap-4 rounded-lg border p-3 transition-all hover:shadow-md"
>
<div class="min-w-[80px] text-sm font-semibold text-gray-700">{apt.time}</div>
<div
class="h-10 w-1 rounded {apt.status === 'completed'
? 'bg-green-500'
: apt.status === 'in_progress'
? 'bg-blue-500'
: apt.status === 'confirmed'
? 'bg-yellow-500'
: 'bg-red-500'}"
></div>
<div class="flex-1">
<div class="font-medium">{apt.customer}</div>
<div class="text-sm text-gray-600">{apt.service}{apt.duration} min</div>
</div>
<Badge class={getStatusColor(apt.status)}>
{apt.status.replace('_', ' ')}
</Badge>
<Button size="sm" variant="outline">View</Button>
</div>
{/each}
</div>
</Card.Content>
</Card.Root>
</div>
<!-- Right Column -->
<div class="space-y-6">
<!-- Quick Stats -->
<Card.Root>
<Card.Header>
<Card.Title>Today's Stats</Card.Title>
</Card.Header>
<Card.Content>
<div class="space-y-3">
<div class="flex items-center justify-between">
<span class="text-sm text-gray-600">Total Appointments</span>
<span class="text-2xl font-bold">8</span>
</div>
<div class="flex items-center justify-between">
<span class="text-sm text-gray-600">Completed</span>
<span class="font-semibold text-green-600">4</span>
</div>
<div class="flex items-center justify-between">
<span class="text-sm text-gray-600">In Progress</span>
<span class="font-semibold text-blue-600">1</span>
</div>
<div class="flex items-center justify-between">
<span class="text-sm text-gray-600">Upcoming</span>
<span class="font-semibold text-emerald-600">3</span>
</div>
<div class="border-t pt-3">
<div class="flex items-center justify-between">
<span class="text-sm font-medium">Occupancy</span>
<span class="text-lg font-bold text-purple-600">75%</span>
</div>
</div>
</div>
</Card.Content>
</Card.Root>
<!-- Upcoming Arrivals -->
<Card.Root>
<Card.Header>
<Card.Title>Upcoming Arrivals</Card.Title>
<Card.Description>Next 2 hours</Card.Description>
</Card.Header>
<Card.Content>
<div class="space-y-3">
{#each upcomingArrivals as arrival}
<div class="rounded-lg border bg-gray-50 p-3">
<div class="mb-1 flex items-center justify-between">
<span class="font-medium">{arrival.customer}</span>
<Badge variant="outline" class="text-xs">in {arrival.minutesUntil} min</Badge>
</div>
<div class="text-sm text-gray-600">{arrival.service}</div>
<div class="text-xs text-gray-500">{arrival.time}</div>
<Button size="sm" class="mt-2 w-full" variant="outline">Check In</Button>
</div>
{/each}
</div>
</Card.Content>
</Card.Root>
</div>
</div>
<!-- Bottom Row -->
<div class="grid grid-cols-1 gap-6 md:grid-cols-2 lg:grid-cols-3">
<!-- Recent Checkouts -->
<Card.Root>
<Card.Header>
<Card.Title>Recent Checkouts</Card.Title>
<Card.Description>Last completed today</Card.Description>
</Card.Header>
<Card.Content>
<div class="space-y-3">
{#each recentCheckouts as checkout}
<div class="rounded-lg border p-3">
<div class="mb-1 flex items-center justify-between">
<span class="font-medium">{checkout.customer}</span>
<span class="font-semibold">£{checkout.amount}</span>
</div>
<div class="text-sm text-gray-600">{checkout.service}</div>
<div class="mt-2">
<Badge
class={checkout.paid ? 'bg-green-100 text-green-800' : 'bg-red-100 text-red-800'}
>
{checkout.paid ? 'Paid' : 'Unpaid'}
</Badge>
</div>
</div>
{/each}
</div>
</Card.Content>
</Card.Root>
<!-- Today's Revenue -->
<Card.Root>
<Card.Header>
<Card.Title>Today's Revenue</Card.Title>
</Card.Header>
<Card.Content>
<div class="space-y-4">
<div class="text-center">
<div class="text-3xl font-bold text-green-600">£450</div>
<div class="text-sm text-gray-500">Total collected</div>
</div>
<div class="space-y-2 border-t pt-4">
<div class="flex items-center justify-between text-sm">
<span class="text-gray-600">Cash</span>
<span class="font-semibold">£120</span>
</div>
<div class="flex items-center justify-between text-sm">
<span class="text-gray-600">Card</span>
<span class="font-semibold">£280</span>
</div>
<div class="flex items-center justify-between text-sm">
<span class="text-gray-600">Online</span>
<span class="font-semibold">£50</span>
</div>
</div>
<div class="border-t pt-3">
<div class="flex items-center justify-between">
<span class="text-sm font-medium text-amber-700">Outstanding</span>
<span class="font-semibold text-amber-700">£75</span>
</div>
</div>
</div>
</Card.Content>
</Card.Root>
<!-- Loyalty Activity -->
<Card.Root>
<Card.Header>
<Card.Title>Loyalty Today</Card.Title>
<Card.Description>Stamps & redemptions</Card.Description>
</Card.Header>
<Card.Content>
<div class="space-y-3">
{#each loyaltyToday as loyalty}
<div class="rounded-lg border bg-gray-50 p-3">
<div class="mb-1 flex items-center justify-between">
<span class="font-medium">{loyalty.customer}</span>
<Badge variant="outline">{loyalty.stamps}/10</Badge>
</div>
<div class="text-sm text-gray-600">{loyalty.action}</div>
</div>
{/each}
<div class="mt-4 border-t pt-3 text-center">
<div class="text-2xl font-bold text-purple-600">15</div>
<div class="text-sm text-gray-500">Stamps earned today</div>
</div>
</div>
</Card.Content>
</Card.Root>
</div>
</div>
+153
View File
@@ -0,0 +1,153 @@
<script lang="ts">
import { goto } from '$app/navigation';
import { authStore } from '$lib/stores/auth.svelte';
import { SvelteDate } from 'svelte/reactivity';
import { browser } from '$app/environment';
import { Skeleton } from '$lib/components/ui/skeleton';
// Component imports
import CurrentAppointment from '$lib/components/today/CurrentAppointment.svelte';
import TodayCalendar from '$lib/components/today/TodayCalendar.svelte';
import PendingApprovals from '$lib/components/today/PendingApprovals.svelte';
// import QuickStats from '$lib/components/today/QuickStats.svelte';
// import UpcomingArrivals from '$lib/components/today/UpcomingArrivals.svelte';
// import RecentCheckouts from '$lib/components/today/RecentCheckouts.svelte';
// import TodayRevenue from '$lib/components/today/TodayRevenue.svelte';
// import LoyaltyRedemptions from '$lib/components/today/LoyaltyRedemptions.svelte';
import BookingModal from '$lib/components/admin/BookingModal.svelte';
import UserModal from '$lib/components/admin/UserModal.svelte';
// =============== Auth & Permissions ===============
let pageState = $state<'loading' | 'authorized' | 'unauthorized'>('loading');
$effect(() => {
if (!browser) return;
if (authStore.isLoading) {
pageState = 'loading';
return;
}
if (!authStore.isAuthenticated) {
pageState = 'unauthorized';
goto('/login', { replaceState: true });
return;
}
if (authStore.currentUser?.role !== 'admin') {
pageState = 'unauthorized';
goto('/', { replaceState: true });
return;
}
pageState = 'authorized';
});
// =============== Modal State ===============
let showBookingModal = $state(false);
let showUserModal = $state(false);
let selectedBookingId = $state<string | null>(null);
let selectedUserId = $state<string | null>(null);
function openBookingModal(bookingId: string) {
selectedBookingId = bookingId;
showBookingModal = true;
}
function openUserModal(userId: string) {
selectedUserId = userId;
showUserModal = true;
}
// Get current date string for display
const today = new SvelteDate();
const dateString = today.toLocaleDateString('en-US', {
weekday: 'long',
year: 'numeric',
month: 'long',
day: 'numeric'
});
</script>
{#if pageState === 'loading'}
<div class="mx-auto max-w-7xl space-y-6 p-6">
<!-- Header Skeleton -->
<div class="mb-6">
<Skeleton class="h-8 w-64" />
<Skeleton class="mt-2 h-4 w-96" />
</div>
<!-- Current Appointment Skeleton -->
<Skeleton class="h-64 w-full" />
<!-- Grid Skeleton -->
<div class="grid grid-cols-1 gap-6 lg:grid-cols-3">
<div class="lg:col-span-2">
<Skeleton class="h-96 w-full" />
</div>
<div class="space-y-6">
<Skeleton class="h-48 w-full" />
<Skeleton class="h-48 w-full" />
</div>
</div>
<!-- Bottom Grid Skeleton -->
<div class="grid grid-cols-1 gap-6 md:grid-cols-2 lg:grid-cols-3">
<Skeleton class="h-64 w-full" />
<Skeleton class="h-64 w-full" />
<Skeleton class="h-64 w-full" />
</div>
</div>
{:else if pageState === 'authorized'}
<div class="mx-auto max-w-7xl space-y-6 p-6">
<!-- Header -->
<div class="mb-6">
<h1 class="text-3xl font-bold">Today's Schedule</h1>
<p class="text-gray-600">{dateString}</p>
</div>
<!-- Current/Next Appointment Card (Full Width) -->
<CurrentAppointment {openBookingModal} {openUserModal} />
<!-- Main Content Grid -->
<div class="grid grid-cols-1 gap-6 lg:grid-cols-3">
<!-- Left Column: Today's Calendar (2/3 width on large screens) -->
<div class="lg:col-span-2">
<TodayCalendar {openBookingModal} {openUserModal} />
</div>
<!-- Right Column: Quick Info Panels (1/3 width on large screens) -->
<div class="space-y-6">
<!-- 3 oldest pending appointments for approval -->
<PendingApprovals {openBookingModal} />
<!-- Quick Stats -->
<!-- <QuickStats /> -->
<!-- Upcoming Arrivals (Next 2 hours) -->
<!-- <UpcomingArrivals {openBookingModal} {openUserModal} /> -->
</div>
</div>
<!-- Bottom Row: Additional Panels -->
<div class="grid grid-cols-1 gap-6 md:grid-cols-2 lg:grid-cols-3">
<!-- Recent Checkouts -->
<!-- <RecentCheckouts {openBookingModal} /> -->
<!-- Today's Revenue -->
<!-- <TodayRevenue /> -->
<!-- Loyalty Redemptions Today -->
<!-- <LoyaltyRedemptions {openUserModal} /> -->
</div>
</div>
<!-- Modals -->
{#if showBookingModal && selectedBookingId}
<BookingModal bind:open={showBookingModal} bookingId={selectedBookingId} />
{/if}
{#if showUserModal && selectedUserId}
<UserModal bind:open={showUserModal} userId={selectedUserId} {openBookingModal} />
{/if}
{/if}