added booking approvals
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -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>
|
||||
@@ -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}
|
||||
Reference in New Issue
Block a user