Bookings
This commit is contained in:
@@ -491,17 +491,75 @@
|
||||
|
||||
type Booking = {
|
||||
id: string;
|
||||
user_id: string;
|
||||
start_time: string;
|
||||
status: 'Confirmed' | 'Completed' | 'Cancelled' | 'Pending' | 'In Progress';
|
||||
start_time: string; // ISO 8601
|
||||
status:
|
||||
| 'pending'
|
||||
| 'confirmed'
|
||||
| 'in_progress'
|
||||
| 'completed'
|
||||
| 'client_cancelled'
|
||||
| 'we_cancelled'
|
||||
| 're-schedule'
|
||||
| 'no_show';
|
||||
notes?: string;
|
||||
created_at: string;
|
||||
services?: { id: string; name: string }[];
|
||||
created_at: string; // ISO 8601
|
||||
updated_at: string; // ISO 8601
|
||||
created_by?: string;
|
||||
|
||||
// Nested user object
|
||||
user?: {
|
||||
full_name?: string;
|
||||
id: string;
|
||||
first_name: string;
|
||||
last_name: string;
|
||||
full_name: string;
|
||||
email?: string;
|
||||
phone?: string;
|
||||
profile_pic_url?: string;
|
||||
date_of_birth?: string;
|
||||
account_role: string;
|
||||
loyalty_stamps?: number;
|
||||
referral_code?: string;
|
||||
referral_code_uses?: number;
|
||||
created_at: string;
|
||||
notes?: string;
|
||||
};
|
||||
|
||||
// Services array - always present (backend ensures this)
|
||||
services: Array<{
|
||||
booking_id: string;
|
||||
service_id: string;
|
||||
override_price?: number;
|
||||
override_duration_minutes?: number;
|
||||
service_name?: string;
|
||||
service_description?: string;
|
||||
price?: number;
|
||||
duration_minutes?: number;
|
||||
}>;
|
||||
|
||||
// Payments array
|
||||
payments: Array<{
|
||||
id: string;
|
||||
booking_id: string;
|
||||
payment_type: 'deposit' | 'full' | 'tip' | 'balance' | 'partial';
|
||||
payment_method: 'online_square' | 'in_person_card' | 'cash' | 'giftcard' | 'discount';
|
||||
vendor_code?: string;
|
||||
invoice_number?: number;
|
||||
status: 'pending' | 'completed' | 'failed' | 'refunded';
|
||||
amount: number;
|
||||
is_vat_applicable: boolean;
|
||||
vat_rate?: number;
|
||||
vat_amount?: number;
|
||||
net_amount?: number;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
created_by?: string;
|
||||
}>;
|
||||
|
||||
// Computed/derived fields
|
||||
total_amount: number;
|
||||
amount_paid: number;
|
||||
amount_due: number;
|
||||
duration_minutes: number;
|
||||
};
|
||||
|
||||
let bookings = $state<Booking[]>([]);
|
||||
@@ -524,27 +582,39 @@
|
||||
});
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
if (data.bookings.length === 0) {
|
||||
console.log('Bookings API response:', data); // Debug log
|
||||
|
||||
if (data.bookings && data.bookings.length === 0) {
|
||||
bookings = [];
|
||||
return;
|
||||
}
|
||||
|
||||
// Map the response correctly - the backend returns the full Booking objects
|
||||
bookings = data.bookings.map((b: any) => ({
|
||||
id: b.id,
|
||||
user_id: b.user_id,
|
||||
start_time: b.start_time,
|
||||
status: b.status,
|
||||
notes: b.notes,
|
||||
created_at: b.created_at,
|
||||
services: b.services?.map((s: any) => ({
|
||||
id: s.service_id,
|
||||
name: s.service_name || 'Unknown Service'
|
||||
})),
|
||||
user: {
|
||||
full_name: b.user?.full_name,
|
||||
email: b.user?.email,
|
||||
phone: b.user?.phone
|
||||
}
|
||||
updated_at: b.updated_at,
|
||||
created_by: b.created_by,
|
||||
// User info is nested under user object
|
||||
user: b.user
|
||||
? {
|
||||
id: b.user.id,
|
||||
full_name: b.user.full_name
|
||||
// Add other user fields if needed
|
||||
}
|
||||
: undefined,
|
||||
// Services array should be present (even if empty)
|
||||
services: b.services || [],
|
||||
// Other computed fields from backend
|
||||
total_amount: b.total_amount || 0,
|
||||
amount_paid: b.amount_paid || 0,
|
||||
amount_due: b.amount_due || 0,
|
||||
duration_minutes: b.duration_minutes || 0
|
||||
}));
|
||||
console.log(bookings);
|
||||
console.log('Mapped bookings:', bookings); // Debug log
|
||||
} else {
|
||||
const text = await response.text();
|
||||
toast.error('Failed to load bookings: ' + text);
|
||||
@@ -561,6 +631,14 @@
|
||||
async function searchBookings() {
|
||||
if (pageState !== 'authorized') return;
|
||||
loadingSearch = true;
|
||||
|
||||
// If no search query, use the regular get-all endpoint
|
||||
if (!bookingQuery.trim()) {
|
||||
await fetchBookings();
|
||||
loadingSearch = false;
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(
|
||||
`/api/admin/bookings/search?q=${encodeURIComponent(bookingQuery)}`,
|
||||
@@ -574,22 +652,28 @@
|
||||
);
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
console.log('Search API response:', data); // Debug log
|
||||
|
||||
// Map the search response correctly (same structure as fetchBookings)
|
||||
bookings = data.bookings.map((b: any) => ({
|
||||
id: b.booking.id,
|
||||
user_id: b.booking.user_id,
|
||||
start_time: b.booking.start_time,
|
||||
status: b.booking.status,
|
||||
notes: b.booking.notes,
|
||||
created_at: b.booking.created_at,
|
||||
services: b.services?.map((s: any) => ({
|
||||
id: s.service_id,
|
||||
name: s.service_name || 'Unknown Service'
|
||||
})),
|
||||
user: {
|
||||
full_name: b.user?.full_name,
|
||||
email: b.user?.email,
|
||||
phone: b.user?.phone
|
||||
}
|
||||
id: b.id,
|
||||
start_time: b.start_time,
|
||||
status: b.status,
|
||||
notes: b.notes,
|
||||
created_at: b.created_at,
|
||||
updated_at: b.updated_at,
|
||||
created_by: b.created_by,
|
||||
user: b.user
|
||||
? {
|
||||
id: b.user.id,
|
||||
full_name: b.user.full_name
|
||||
}
|
||||
: undefined,
|
||||
services: b.services || [],
|
||||
total_amount: b.total_amount || 0,
|
||||
amount_paid: b.amount_paid || 0,
|
||||
amount_due: b.amount_due || 0,
|
||||
duration_minutes: b.duration_minutes || 0
|
||||
}));
|
||||
} else {
|
||||
const text = await response.text();
|
||||
@@ -607,7 +691,7 @@
|
||||
async function openBookingModal(bookingId: string) {
|
||||
if (pageState !== 'authorized') return;
|
||||
try {
|
||||
const response = await fetch(`/api/admin/bookings/${bookingId}/summary`, {
|
||||
const response = await fetch(`/api/admin/bookings/${bookingId}`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
@@ -616,22 +700,63 @@
|
||||
});
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
console.log('Booking details API response:', data); // Debug log
|
||||
|
||||
selectedBooking = {
|
||||
id: data.booking.id,
|
||||
user_id: data.booking.user_id,
|
||||
start_time: data.booking.start_time,
|
||||
status: data.booking.status,
|
||||
notes: data.booking.notes,
|
||||
created_at: data.booking.created_at,
|
||||
services: data.services?.map((s: any) => ({
|
||||
id: s.service_id,
|
||||
name: s.service_name || 'Unknown Service'
|
||||
id: data.id,
|
||||
start_time: data.start_time,
|
||||
status: data.status,
|
||||
notes: data.notes,
|
||||
user: data.user
|
||||
? {
|
||||
id: data.user.id,
|
||||
first_name: data.user.first_name,
|
||||
last_name: data.user.last_name,
|
||||
full_name: data.user.full_name,
|
||||
email: data.user.email,
|
||||
phone: data.user.phone,
|
||||
profile_pic_url: data.user.profile_pic_url,
|
||||
date_of_birth: data.user.date_of_birth,
|
||||
account_role: data.user.account_role,
|
||||
loyalty_stamps: data.user.loyalty_stamps,
|
||||
referral_code: data.user.referral_code,
|
||||
referral_code_uses: data.user.referral_code_uses,
|
||||
created_at: data.user.created_at,
|
||||
notes: data.user.notes
|
||||
}
|
||||
: undefined,
|
||||
services: (data.services || []).map((s: any) => ({
|
||||
booking_id: s.booking_id,
|
||||
service_id: s.service_id,
|
||||
service_name: s.service_name,
|
||||
service_description: s.service_description,
|
||||
price: s.price,
|
||||
duration_minutes: s.duration_minutes
|
||||
})),
|
||||
user: {
|
||||
full_name: data.user?.full_name,
|
||||
email: data.user?.email,
|
||||
phone: data.user?.phone
|
||||
}
|
||||
payments: (data.payments || []).map((p: any) => ({
|
||||
id: p.id,
|
||||
booking_id: p.booking_id,
|
||||
payment_type: p.payment_type,
|
||||
payment_method: p.payment_method,
|
||||
vendor_code: p.vendor_code,
|
||||
invoice_number: p.invoice_number,
|
||||
status: p.status,
|
||||
amount: p.amount,
|
||||
is_vat_applicable: p.is_vat_applicable,
|
||||
vat_rate: p.vat_rate,
|
||||
vat_amount: p.vat_amount,
|
||||
net_amount: p.net_amount,
|
||||
created_at: p.created_at,
|
||||
updated_at: p.updated_at,
|
||||
created_by: p.created_by
|
||||
})),
|
||||
total_amount: data.total_amount || 0,
|
||||
amount_paid: data.amount_paid || 0,
|
||||
amount_due: data.amount_due || 0,
|
||||
duration_minutes: data.duration_minutes || 0,
|
||||
created_at: data.created_at,
|
||||
updated_at: data.updated_at,
|
||||
created_by: data.created_by
|
||||
};
|
||||
showBookingModal = true;
|
||||
} else {
|
||||
@@ -687,7 +812,7 @@
|
||||
|
||||
// Filter demo bookings for this user
|
||||
bookingUserHistory = bookings
|
||||
.filter((b) => b.user_id === userId)
|
||||
.filter((b) => b?.user?.id === userId)
|
||||
.sort((a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime());
|
||||
|
||||
showUserModal = true;
|
||||
@@ -770,7 +895,6 @@
|
||||
// Toggle service active status
|
||||
async function toggleService(serviceId: string) {
|
||||
servicesUpdating[serviceId] = true;
|
||||
console.log('toggling', serviceId);
|
||||
try {
|
||||
const response = await fetch(`/api/admin/services/${serviceId}/toggle`, {
|
||||
method: 'PUT',
|
||||
@@ -1424,7 +1548,7 @@
|
||||
<div>
|
||||
<div class="flex gap-2">
|
||||
<Input
|
||||
placeholder="Search by name, email, phone, or service"
|
||||
placeholder="Search by customer name, email, phone, or service"
|
||||
bind:value={bookingQuery}
|
||||
onkeyup={(e) => {
|
||||
if ((e as KeyboardEvent).key === 'Enter') searchBookings();
|
||||
@@ -1444,14 +1568,137 @@
|
||||
{:else}
|
||||
{#each bookings as b}
|
||||
<div class="flex items-center justify-between rounded bg-gray-50 p-2">
|
||||
<div>
|
||||
<div class="flex-1">
|
||||
<div class="font-medium">
|
||||
{new Date(b.start_time).toLocaleString()}
|
||||
{(() => {
|
||||
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(
|
||||
date.getFullYear(),
|
||||
date.getMonth(),
|
||||
date.getDate()
|
||||
);
|
||||
const daysDiff = Math.floor(
|
||||
(bookingDate.getTime() - today.getTime()) / (1000 * 60 * 60 * 24)
|
||||
);
|
||||
|
||||
const days = [
|
||||
'Sunday',
|
||||
'Monday',
|
||||
'Tuesday',
|
||||
'Wednesday',
|
||||
'Thursday',
|
||||
'Friday',
|
||||
'Saturday'
|
||||
];
|
||||
const months = [
|
||||
'Jan',
|
||||
'Feb',
|
||||
'Mar',
|
||||
'Apr',
|
||||
'May',
|
||||
'June',
|
||||
'July',
|
||||
'Aug',
|
||||
'Sept',
|
||||
'Oct',
|
||||
'Nov',
|
||||
'Dec'
|
||||
];
|
||||
|
||||
const day = days[date.getDay()];
|
||||
const dateNum = date.getDate();
|
||||
const month = months[date.getMonth()];
|
||||
const year = date.getFullYear();
|
||||
const currentYear = now.getFullYear();
|
||||
const hours = date.getHours();
|
||||
const minutes = date.getMinutes().toString().padStart(2, '0');
|
||||
const ampm = hours >= 12 ? 'pm' : 'am';
|
||||
const hour12 = hours % 12 || 12;
|
||||
const time = `${hour12}:${minutes}${ampm}`;
|
||||
|
||||
// Today
|
||||
if (daysDiff === 0) {
|
||||
return `Today, ${time}`;
|
||||
}
|
||||
|
||||
// Tomorrow
|
||||
if (daysDiff === 1) {
|
||||
return `Tomorrow, ${time}`;
|
||||
}
|
||||
|
||||
// Within next 6 days (2-6 days ahead)
|
||||
if (daysDiff > 1 && daysDiff <= 6) {
|
||||
return `${day}, ${time}`;
|
||||
}
|
||||
|
||||
// Last 6 days (1-6 days ago)
|
||||
if (daysDiff < 0 && daysDiff >= -6) {
|
||||
return `Last ${day}, ${time}`;
|
||||
}
|
||||
|
||||
// Otherwise, full date
|
||||
const suffix =
|
||||
dateNum === 1 || dateNum === 21 || dateNum === 31
|
||||
? 'st'
|
||||
: dateNum === 2 || dateNum === 22
|
||||
? 'nd'
|
||||
: dateNum === 3 || dateNum === 23
|
||||
? 'rd'
|
||||
: 'th';
|
||||
const yearStr = year !== currentYear ? ` ${year}` : '';
|
||||
return `${day} the ${dateNum}${suffix} of ${month}${yearStr}, ${time}`;
|
||||
})()}
|
||||
</div>
|
||||
<div class="text-xs text-gray-500">
|
||||
{b.status} • {b.user?.full_name || 'Unknown User'} • {b.services
|
||||
?.map((s) => s.name)
|
||||
.join(', ')}
|
||||
<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'
|
||||
: b.status === 'client_cancelled'
|
||||
? 'bg-red-100 text-red-800'
|
||||
: b.status === 'we_cancelled'
|
||||
? 'bg-rose-100 text-rose-800'
|
||||
: b.status === 're-schedule'
|
||||
? 'bg-purple-100 text-purple-800'
|
||||
: b.status === 'no_show'
|
||||
? 'bg-gray-100 text-gray-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'
|
||||
: b.status === 'client_cancelled'
|
||||
? 'bg-red-600'
|
||||
: b.status === 'we_cancelled'
|
||||
? 'bg-rose-600'
|
||||
: b.status === 're-schedule'
|
||||
? 'bg-purple-600'
|
||||
: b.status === 'no_show'
|
||||
? 'bg-gray-600'
|
||||
: 'bg-gray-600'}"
|
||||
></span>
|
||||
{b.status}
|
||||
</span>
|
||||
<span>• {b.user?.full_name || 'Unknown User'}</span>
|
||||
<span
|
||||
>• {(b.services || [])
|
||||
.map((s) => s.service_name || 'Unknown Service')
|
||||
.join(', ')}</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<Button variant="outline" onclick={() => openBookingModal(b.id)}>View</Button>
|
||||
@@ -2451,7 +2698,7 @@
|
||||
<div>
|
||||
<div class="font-medium">{new Date(hb.start_time).toLocaleString()}</div>
|
||||
<div class="text-xs text-gray-500">
|
||||
{hb.status} • {hb.services?.map((s) => s.name).join(', ')}
|
||||
{hb.status} • {hb.services.map((s) => s.service_name).join(', ')}
|
||||
</div>
|
||||
</div>
|
||||
<Button variant="outline" onclick={() => openBookingModal(hb.id)}>Open</Button>
|
||||
@@ -2469,56 +2716,243 @@
|
||||
|
||||
{#if selectedBooking}
|
||||
<Modal.Root bind:open={showBookingModal}>
|
||||
<Modal.Content class="max-h-[90vh] max-w-sm overflow-y-auto md:max-w-lg">
|
||||
<Modal.Content class="max-h-[90vh] max-w-sm overflow-y-auto md:max-w-3xl">
|
||||
<Modal.Header>
|
||||
<Modal.Title class="text-lg font-semibold">
|
||||
Booking: {selectedBooking.id}
|
||||
</Modal.Title>
|
||||
<Modal.Title class="text-lg font-semibold">Booking Details</Modal.Title>
|
||||
<div class="mt-1 text-sm text-gray-500">ID: {selectedBooking.id}</div>
|
||||
</Modal.Header>
|
||||
|
||||
<!-- Main Booking Details Grid -->
|
||||
<div class="grid gap-4 px-4 pb-4 md:grid-cols-2">
|
||||
<!-- Column 1: Customer Details -->
|
||||
<div>
|
||||
<div class="text-sm text-gray-500">Customer</div>
|
||||
<div class="font-medium">
|
||||
{selectedBooking.user?.full_name || 'Unknown User'}
|
||||
</div>
|
||||
<div class="mt-2 text-sm text-gray-500">Email</div>
|
||||
<div class="font-medium">{selectedBooking.user?.email || '—'}</div>
|
||||
<div class="mt-2 text-sm text-gray-500">Phone</div>
|
||||
<div class="font-medium">{selectedBooking.user?.phone || '—'}</div>
|
||||
<div class="mt-2 text-sm text-gray-500">Status</div>
|
||||
<div class="font-medium">{selectedBooking.status}</div>
|
||||
<div class="space-y-6 px-4 pb-4">
|
||||
<!-- Status Badge -->
|
||||
<div class="flex items-center gap-2">
|
||||
<span
|
||||
class="inline-flex items-center rounded-full px-3 py-1 text-sm font-medium
|
||||
{selectedBooking.status === 'pending'
|
||||
? 'bg-yellow-100 text-yellow-800'
|
||||
: selectedBooking.status === 'confirmed'
|
||||
? 'bg-emerald-100 text-emerald-800'
|
||||
: selectedBooking.status === 'in_progress'
|
||||
? 'bg-blue-100 text-blue-800'
|
||||
: selectedBooking.status === 'completed'
|
||||
? 'bg-green-100 text-green-800'
|
||||
: selectedBooking.status === 'client_cancelled'
|
||||
? 'bg-red-100 text-red-800'
|
||||
: selectedBooking.status === 'we_cancelled'
|
||||
? 'bg-rose-100 text-rose-800'
|
||||
: selectedBooking.status === 're-schedule'
|
||||
? 'bg-purple-100 text-purple-800'
|
||||
: selectedBooking.status === 'no_show'
|
||||
? 'bg-gray-100 text-gray-800'
|
||||
: 'bg-gray-100 text-gray-800'}"
|
||||
>
|
||||
{selectedBooking.status.charAt(0).toUpperCase() + selectedBooking.status.slice(1)}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- Column 2: Appointment Details -->
|
||||
<div>
|
||||
<div class="text-sm text-gray-500">Scheduled</div>
|
||||
<div class="font-medium">
|
||||
{new Date(selectedBooking.start_time).toLocaleString()}
|
||||
<!-- 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">
|
||||
Customer Information
|
||||
</h3>
|
||||
<div class="grid gap-3 md:grid-cols-2">
|
||||
<div>
|
||||
<div class="text-xs text-gray-500">Name</div>
|
||||
<div class="font-medium">{selectedBooking.user?.full_name || '—'}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="text-xs text-gray-500">Email</div>
|
||||
<div class="break-all font-medium">{selectedBooking.user?.email || '—'}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="text-xs text-gray-500">Phone</div>
|
||||
<div class="font-medium">{selectedBooking.user?.phone || '—'}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="text-xs text-gray-500">Customer ID</div>
|
||||
<div class="truncate font-mono text-sm">{selectedBooking.user?.id || '—'}</div>
|
||||
</div>
|
||||
{#if selectedBooking.user?.loyalty_stamps !== undefined && selectedBooking.user?.loyalty_stamps !== null}
|
||||
<div>
|
||||
<div class="text-xs text-gray-500">Loyalty Stamps</div>
|
||||
<div class="font-medium">{selectedBooking.user.loyalty_stamps}</div>
|
||||
</div>
|
||||
{/if}
|
||||
{#if selectedBooking.user?.referral_code}
|
||||
<div>
|
||||
<div class="text-xs text-gray-500">Referral Code</div>
|
||||
<div class="font-medium">{selectedBooking.user.referral_code}</div>
|
||||
</div>
|
||||
{/if}
|
||||
{#if selectedBooking.user?.referral_code_uses !== undefined && selectedBooking.user?.referral_code_uses !== null}
|
||||
<div>
|
||||
<div class="text-xs text-gray-500">Referral Uses</div>
|
||||
<div class="font-medium">{selectedBooking.user.referral_code_uses}</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="mt-2 text-sm text-gray-500">Services</div>
|
||||
<div class="mb-4 font-medium">
|
||||
{selectedBooking.services?.map((s) => s.name).join(', ') || '—'}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Notes Section - now full width and visually emphasized -->
|
||||
<div class="md:col-span-2">
|
||||
<div class="mb-1 text-sm text-gray-500">Customer Notes</div>
|
||||
{#if selectedBooking.notes && selectedBooking.notes.length > 0}
|
||||
<!-- Applied amber styling to the actual notes content -->
|
||||
<div
|
||||
class="rounded-xl border border-amber-200 bg-amber-50 p-3 text-sm font-medium text-amber-900"
|
||||
>
|
||||
{selectedBooking.notes || 'No customer notes provided.'}
|
||||
{#if selectedBooking.user?.notes}
|
||||
<div class="mt-3 rounded-md border border-blue-200 bg-blue-50 p-3">
|
||||
<div class="mb-1 text-xs font-semibold text-blue-800">Customer Notes</div>
|
||||
<div class="text-sm text-blue-900">{selectedBooking.user.notes}</div>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="mb-4 font-medium">—</div>
|
||||
{/if}
|
||||
</div>
|
||||
<!-- END NOTES SECTION -->
|
||||
|
||||
<!-- 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">
|
||||
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()}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="text-xs text-gray-500">Duration</div>
|
||||
<div class="font-medium">{selectedBooking.duration_minutes} minutes</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="text-xs text-gray-500">Created</div>
|
||||
<div class="text-sm">{new Date(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>
|
||||
{#if selectedBooking.created_by}
|
||||
<div class="md:col-span-2">
|
||||
<div class="text-xs text-gray-500">Created By</div>
|
||||
<div class="text-sm">{selectedBooking.created_by}</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{#if selectedBooking.notes}
|
||||
<div class="mt-3 rounded-md border border-amber-200 bg-amber-50 p-3">
|
||||
<div class="mb-1 text-xs font-semibold text-amber-800">Booking Notes</div>
|
||||
<div class="text-sm text-amber-900">{selectedBooking.notes}</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<!-- 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">
|
||||
Services
|
||||
</h3>
|
||||
<div class="space-y-3">
|
||||
{#each selectedBooking.services as service}
|
||||
<div class="rounded-md border border-gray-300 bg-white p-3">
|
||||
<div class="font-medium">{service.service_name || '—'}</div>
|
||||
{#if service.service_description}
|
||||
<div class="mt-1 text-sm text-gray-600">{service.service_description}</div>
|
||||
{/if}
|
||||
<div class="mt-2 flex items-center justify-between text-sm">
|
||||
<span class="text-gray-600">{service.duration_minutes} min</span>
|
||||
<span class="font-semibold">£{service.price?.toFixed(2) || '0.00'}</span>
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- 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">
|
||||
Financial Summary
|
||||
</h3>
|
||||
<div class="space-y-2">
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-sm text-gray-600">Total Amount</span>
|
||||
<span class="font-semibold">£{selectedBooking.total_amount.toFixed(2)}</span>
|
||||
</div>
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-sm text-gray-600">Amount Paid</span>
|
||||
<span class="font-semibold text-green-700"
|
||||
>£{selectedBooking.amount_paid.toFixed(2)}</span
|
||||
>
|
||||
</div>
|
||||
<div class="flex items-center justify-between border-t border-gray-300 pt-2">
|
||||
<span class="font-medium text-gray-900">Amount Due</span>
|
||||
<span
|
||||
class="text-lg font-bold {selectedBooking.amount_due > 0
|
||||
? 'text-red-600'
|
||||
: 'text-green-600'}"
|
||||
>
|
||||
£{selectedBooking.amount_due.toFixed(2)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 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">
|
||||
Payment History
|
||||
</h3>
|
||||
<div class="space-y-3">
|
||||
{#each selectedBooking.payments as payment}
|
||||
<div class="rounded-md border border-gray-300 bg-white p-3">
|
||||
<div class="flex items-start justify-between">
|
||||
<div class="flex-1">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="font-medium capitalize"
|
||||
>{payment.payment_method.replace('_', ' ')}</span
|
||||
>
|
||||
<span
|
||||
class="inline-flex items-center rounded-full px-2 py-0.5 text-xs font-medium
|
||||
{payment.status === 'completed'
|
||||
? 'bg-green-100 text-green-800'
|
||||
: payment.status === 'pending'
|
||||
? 'bg-yellow-100 text-yellow-800'
|
||||
: payment.status === 'failed'
|
||||
? 'bg-red-100 text-red-800'
|
||||
: 'bg-gray-100 text-gray-800'}"
|
||||
>
|
||||
{payment.status}
|
||||
</span>
|
||||
</div>
|
||||
<div class="mt-1 text-xs text-gray-500">
|
||||
{payment.payment_type.charAt(0).toUpperCase() +
|
||||
payment.payment_type.slice(1)}
|
||||
</div>
|
||||
{#if payment.vendor_code || payment.invoice_number}
|
||||
<div class="mt-1 text-xs text-gray-500">
|
||||
{#if payment.vendor_code}Vendor: {payment.vendor_code}{/if}
|
||||
{#if payment.vendor_code && payment.invoice_number}
|
||||
•
|
||||
{/if}
|
||||
{#if payment.invoice_number}Invoice: #{payment.invoice_number}{/if}
|
||||
</div>
|
||||
{/if}
|
||||
{#if payment.is_vat_applicable}
|
||||
<div class="mt-2 text-xs text-gray-600">
|
||||
<div>Net: £{payment.net_amount?.toFixed(2) || '0.00'}</div>
|
||||
<div>
|
||||
VAT ({(payment.vat_rate || 0) * 100}%): £{payment.vat_amount?.toFixed(
|
||||
2
|
||||
) || '0.00'}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
<div class="mt-1 text-xs text-gray-400">
|
||||
{new Date(payment.created_at).toLocaleString()}
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-right font-semibold">
|
||||
£{payment.amount.toFixed(2)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<Modal.Footer class="flex items-center justify-end gap-2">
|
||||
|
||||
Reference in New Issue
Block a user