Update frontend to match new tested backend
This commit is contained in:
@@ -4,6 +4,7 @@
|
|||||||
import { toast } from 'svelte-sonner';
|
import { toast } from 'svelte-sonner';
|
||||||
import * as Modal from '$lib/components/ui/dialog';
|
import * as Modal from '$lib/components/ui/dialog';
|
||||||
import { Button } from '$lib/components/ui/button';
|
import { Button } from '$lib/components/ui/button';
|
||||||
|
import type { Booking } from '$lib/types/booking';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
@@ -12,38 +13,6 @@
|
|||||||
|
|
||||||
let { open = $bindable(), bookingId }: Props = $props();
|
let { open = $bindable(), bookingId }: Props = $props();
|
||||||
|
|
||||||
type Booking = {
|
|
||||||
id: string;
|
|
||||||
start_time: string;
|
|
||||||
status: string;
|
|
||||||
notes?: string;
|
|
||||||
created_at: string;
|
|
||||||
updated_at: string;
|
|
||||||
services: Array<{
|
|
||||||
service_name?: string;
|
|
||||||
service_description?: string;
|
|
||||||
price?: number;
|
|
||||||
duration_minutes?: number;
|
|
||||||
}>;
|
|
||||||
payments: Array<{
|
|
||||||
id: string;
|
|
||||||
payment_type: string;
|
|
||||||
payment_method: string;
|
|
||||||
status: string;
|
|
||||||
amount: number;
|
|
||||||
created_at: string;
|
|
||||||
invoice_number?: number;
|
|
||||||
is_vat_applicable: boolean;
|
|
||||||
vat_amount?: number;
|
|
||||||
net_amount?: number;
|
|
||||||
vat_rate?: number;
|
|
||||||
}>;
|
|
||||||
total_amount: number;
|
|
||||||
amount_paid: number;
|
|
||||||
amount_due: number;
|
|
||||||
duration_minutes: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
let selectedBooking = $state<Booking | null>(null);
|
let selectedBooking = $state<Booking | null>(null);
|
||||||
let loading = $state(false);
|
let loading = $state(false);
|
||||||
|
|
||||||
@@ -66,7 +35,7 @@
|
|||||||
|
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
selectedBooking = data;
|
selectedBooking = data as Booking;
|
||||||
} else {
|
} else {
|
||||||
const text = await response.text();
|
const text = await response.text();
|
||||||
toast.error('Failed to load booking: ' + text);
|
toast.error('Failed to load booking: ' + text);
|
||||||
@@ -169,6 +138,48 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Deposit Info -->
|
||||||
|
{#if selectedBooking.deposit_required}
|
||||||
|
<div class="rounded-lg border border-blue-200 bg-blue-50 p-4">
|
||||||
|
<h3 class="mb-3 text-sm font-semibold tracking-wide text-blue-800 uppercase">
|
||||||
|
Deposit Information
|
||||||
|
</h3>
|
||||||
|
<div class="grid gap-3 md:grid-cols-2">
|
||||||
|
<div>
|
||||||
|
<div class="text-xs text-blue-600">Deposit Amount</div>
|
||||||
|
<div class="font-semibold text-blue-900">
|
||||||
|
£{selectedBooking.deposit_amount?.toFixed(2) || '0.00'}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div class="text-xs text-blue-600">Deposit Status</div>
|
||||||
|
<div class="font-semibold">
|
||||||
|
<span
|
||||||
|
class="{selectedBooking.deposit_paid
|
||||||
|
? 'bg-green-100 text-green-800'
|
||||||
|
: 'bg-red-100 text-red-800'} inline-flex items-center rounded-full px-2 py-0.5 text-xs font-medium"
|
||||||
|
>
|
||||||
|
{selectedBooking.deposit_paid ? 'Paid' : 'Outstanding'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{#if selectedBooking.deposit_deadline && !selectedBooking.deposit_paid}
|
||||||
|
<div class="md:col-span-2">
|
||||||
|
<div class="text-xs text-blue-600">Deadline</div>
|
||||||
|
<div class="font-semibold text-blue-900">
|
||||||
|
{new SvelteDate(selectedBooking.deposit_deadline).toLocaleDateString('en-GB', {
|
||||||
|
weekday: 'long',
|
||||||
|
day: 'numeric',
|
||||||
|
month: 'long',
|
||||||
|
year: 'numeric'
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
<!-- Services -->
|
<!-- Services -->
|
||||||
{#if selectedBooking.services && selectedBooking.services.length > 0}
|
{#if selectedBooking.services && selectedBooking.services.length > 0}
|
||||||
<div class="rounded-lg border border-gray-200 bg-gray-50 p-4">
|
<div class="rounded-lg border border-gray-200 bg-gray-50 p-4">
|
||||||
@@ -281,11 +292,14 @@
|
|||||||
|
|
||||||
<Modal.Footer class="flex items-center justify-end gap-2">
|
<Modal.Footer class="flex items-center justify-end gap-2">
|
||||||
{#if selectedBooking}
|
{#if selectedBooking}
|
||||||
<Button variant="outline" onclick={() => {
|
<Button
|
||||||
if (selectedBooking) {
|
variant="outline"
|
||||||
window.open(`/api/bookings/${selectedBooking.id}/calendar`, '_blank');
|
onclick={() => {
|
||||||
}
|
if (selectedBooking) {
|
||||||
}}>
|
window.open(`/api/bookings/${selectedBooking.id}/calendar`, '_blank');
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
Add to Calendar
|
Add to Calendar
|
||||||
</Button>
|
</Button>
|
||||||
{/if}
|
{/if}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
import * as Modal from '$lib/components/ui/dialog';
|
import * as Modal from '$lib/components/ui/dialog';
|
||||||
import { Button } from '$lib/components/ui/button';
|
import { Button } from '$lib/components/ui/button';
|
||||||
import ApprovalModal from '$lib/components/admin/ApprovalModal.svelte';
|
import ApprovalModal from '$lib/components/admin/ApprovalModal.svelte';
|
||||||
|
import type { Booking } from '$lib/types/booking';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
@@ -13,75 +14,6 @@
|
|||||||
|
|
||||||
let { open = $bindable(), bookingId }: Props = $props();
|
let { open = $bindable(), bookingId }: Props = $props();
|
||||||
|
|
||||||
type Booking = {
|
|
||||||
id: string;
|
|
||||||
start_time: string;
|
|
||||||
status:
|
|
||||||
| 'pending'
|
|
||||||
| 'confirmed'
|
|
||||||
| 'in_progress'
|
|
||||||
| 'completed'
|
|
||||||
| 'client_cancelled'
|
|
||||||
| 'we_cancelled'
|
|
||||||
| 're-schedule'
|
|
||||||
| 'no_show';
|
|
||||||
notes?: string;
|
|
||||||
created_at: string;
|
|
||||||
updated_at: string;
|
|
||||||
created_by?: string;
|
|
||||||
|
|
||||||
user?: {
|
|
||||||
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<{
|
|
||||||
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<{
|
|
||||||
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;
|
|
||||||
}>;
|
|
||||||
|
|
||||||
total_amount: number;
|
|
||||||
amount_paid: number;
|
|
||||||
amount_due: number;
|
|
||||||
duration_minutes: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
let selectedBooking = $state<Booking | null>(null);
|
let selectedBooking = $state<Booking | null>(null);
|
||||||
let showApprovalModal = $state(false);
|
let showApprovalModal = $state(false);
|
||||||
|
|
||||||
@@ -111,6 +43,17 @@
|
|||||||
start_time: data.start_time,
|
start_time: data.start_time,
|
||||||
status: data.status,
|
status: data.status,
|
||||||
notes: data.notes,
|
notes: data.notes,
|
||||||
|
created_at: data.created_at,
|
||||||
|
updated_at: data.updated_at,
|
||||||
|
created_by: data.created_by,
|
||||||
|
|
||||||
|
// Deposit fields
|
||||||
|
deposit_required: data.deposit_required ?? false,
|
||||||
|
deposit_amount: data.deposit_amount,
|
||||||
|
deposit_paid: data.deposit_paid ?? false,
|
||||||
|
deposit_deadline: data.deposit_deadline,
|
||||||
|
|
||||||
|
// User
|
||||||
user: data.user
|
user: data.user
|
||||||
? {
|
? {
|
||||||
id: data.user.id,
|
id: data.user.id,
|
||||||
@@ -129,14 +72,20 @@
|
|||||||
notes: data.user.notes
|
notes: data.user.notes
|
||||||
}
|
}
|
||||||
: undefined,
|
: undefined,
|
||||||
|
|
||||||
|
// Services
|
||||||
services: (data.services || []).map((s) => ({
|
services: (data.services || []).map((s) => ({
|
||||||
booking_id: s.booking_id,
|
booking_id: s.booking_id,
|
||||||
service_id: s.service_id,
|
service_id: s.service_id,
|
||||||
service_name: s.service_name,
|
service_name: s.service_name,
|
||||||
service_description: s.service_description,
|
service_description: s.service_description,
|
||||||
price: s.price,
|
price: s.price,
|
||||||
duration_minutes: s.duration_minutes
|
duration_minutes: s.duration_minutes,
|
||||||
|
override_price: s.override_price,
|
||||||
|
override_duration_minutes: s.override_duration_minutes
|
||||||
})),
|
})),
|
||||||
|
|
||||||
|
// Payments
|
||||||
payments: (data.payments || []).map((p) => ({
|
payments: (data.payments || []).map((p) => ({
|
||||||
id: p.id,
|
id: p.id,
|
||||||
booking_id: p.booking_id,
|
booking_id: p.booking_id,
|
||||||
@@ -154,13 +103,12 @@
|
|||||||
updated_at: p.updated_at,
|
updated_at: p.updated_at,
|
||||||
created_by: p.created_by
|
created_by: p.created_by
|
||||||
})),
|
})),
|
||||||
|
|
||||||
|
// Financials
|
||||||
total_amount: data.total_amount || 0,
|
total_amount: data.total_amount || 0,
|
||||||
amount_paid: data.amount_paid || 0,
|
amount_paid: data.amount_paid || 0,
|
||||||
amount_due: data.amount_due || 0,
|
amount_due: data.amount_due || 0,
|
||||||
duration_minutes: data.duration_minutes || 0,
|
duration_minutes: data.duration_minutes || 0
|
||||||
created_at: data.created_at,
|
|
||||||
updated_at: data.updated_at,
|
|
||||||
created_by: data.created_by
|
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
const text = await response.text();
|
const text = await response.text();
|
||||||
@@ -281,6 +229,48 @@
|
|||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Deposit Info -->
|
||||||
|
{#if selectedBooking.deposit_required}
|
||||||
|
<div class="rounded-lg border border-blue-200 bg-blue-50 p-4">
|
||||||
|
<h3 class="mb-3 text-sm font-semibold tracking-wide text-blue-800 uppercase">
|
||||||
|
Deposit Information
|
||||||
|
</h3>
|
||||||
|
<div class="grid gap-3 md:grid-cols-2">
|
||||||
|
<div>
|
||||||
|
<div class="text-xs text-blue-600">Deposit Amount</div>
|
||||||
|
<div class="font-semibold text-blue-900">
|
||||||
|
£{selectedBooking.deposit_amount?.toFixed(2) || '0.00'}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div class="text-xs text-blue-600">Deposit Status</div>
|
||||||
|
<div class="font-semibold">
|
||||||
|
<span
|
||||||
|
class="{selectedBooking.deposit_paid
|
||||||
|
? 'bg-green-100 text-green-800'
|
||||||
|
: 'bg-red-100 text-red-800'} inline-flex items-center rounded-full px-2 py-0.5 text-xs font-medium"
|
||||||
|
>
|
||||||
|
{selectedBooking.deposit_paid ? 'Paid' : 'Outstanding'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{#if selectedBooking.deposit_deadline && !selectedBooking.deposit_paid}
|
||||||
|
<div class="md:col-span-2">
|
||||||
|
<div class="text-xs text-blue-600">Deadline</div>
|
||||||
|
<div class="font-semibold text-blue-900">
|
||||||
|
{new SvelteDate(selectedBooking.deposit_deadline).toLocaleDateString('en-GB', {
|
||||||
|
weekday: 'long',
|
||||||
|
day: 'numeric',
|
||||||
|
month: 'long',
|
||||||
|
year: 'numeric'
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
<!-- Services -->
|
<!-- Services -->
|
||||||
{#if selectedBooking.services && selectedBooking.services.length > 0}
|
{#if selectedBooking.services && selectedBooking.services.length > 0}
|
||||||
<div class="rounded-lg border border-gray-200 bg-gray-50 p-4">
|
<div class="rounded-lg border border-gray-200 bg-gray-50 p-4">
|
||||||
|
|||||||
@@ -8,64 +8,11 @@
|
|||||||
import * as Card from '$lib/components/ui/card';
|
import * as Card from '$lib/components/ui/card';
|
||||||
import { Input } from '$lib/components/ui/input';
|
import { Input } from '$lib/components/ui/input';
|
||||||
import { Skeleton } from '$lib/components/ui/skeleton';
|
import { Skeleton } from '$lib/components/ui/skeleton';
|
||||||
|
import type { Booking } from '$lib/types/booking';
|
||||||
|
|
||||||
// Props
|
// Props
|
||||||
let { openBookingModal }: { openBookingModal: (bookingId: string) => void } = $props();
|
let { openBookingModal }: { openBookingModal: (bookingId: string) => void } = $props();
|
||||||
|
|
||||||
// Types
|
|
||||||
type Booking = {
|
|
||||||
id: string;
|
|
||||||
start_time: string;
|
|
||||||
status:
|
|
||||||
| 'pending'
|
|
||||||
| 'confirmed'
|
|
||||||
| 'in_progress'
|
|
||||||
| 'completed'
|
|
||||||
| 'client_cancelled'
|
|
||||||
| 'we_cancelled'
|
|
||||||
| 're-schedule'
|
|
||||||
| 'no_show';
|
|
||||||
notes?: string;
|
|
||||||
created_at: string;
|
|
||||||
updated_at: string;
|
|
||||||
created_by?: string;
|
|
||||||
user?: {
|
|
||||||
id: string;
|
|
||||||
full_name: string;
|
|
||||||
};
|
|
||||||
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<{
|
|
||||||
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;
|
|
||||||
}>;
|
|
||||||
total_amount: number;
|
|
||||||
amount_paid: number;
|
|
||||||
amount_due: number;
|
|
||||||
duration_minutes: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
// State
|
// State
|
||||||
let bookings = $state<Booking[]>([]);
|
let bookings = $state<Booking[]>([]);
|
||||||
let totalBookings = $state(0);
|
let totalBookings = $state(0);
|
||||||
@@ -110,28 +57,9 @@
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
bookings = data.bookings.map((b: any) => ({
|
bookings = data.bookings as Booking[];
|
||||||
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
|
|
||||||
}));
|
|
||||||
totalBookings = data.total || 0;
|
totalBookings = data.total || 0;
|
||||||
totalPages = data.totalPages || 1;
|
totalPages = data.total_pages || 1;
|
||||||
currentPage = data.page || 1;
|
currentPage = data.page || 1;
|
||||||
} else {
|
} else {
|
||||||
const text = await response.text();
|
const text = await response.text();
|
||||||
@@ -222,7 +150,7 @@
|
|||||||
|
|
||||||
// Format services list
|
// Format services list
|
||||||
function formatServices(services: Booking['services']): string {
|
function formatServices(services: Booking['services']): string {
|
||||||
const serviceNames = services.map((s) => s.service_name || 'Unknown Service');
|
const serviceNames = services?.map((s) => s.service_name || 'Unknown Service') || [];
|
||||||
if (serviceNames.length === 0) return 'No services';
|
if (serviceNames.length === 0) return 'No services';
|
||||||
if (serviceNames.length === 1) return serviceNames[0];
|
if (serviceNames.length === 1) return serviceNames[0];
|
||||||
if (serviceNames.length === 2) return serviceNames.join(' and ');
|
if (serviceNames.length === 2) return serviceNames.join(' and ');
|
||||||
@@ -232,7 +160,7 @@
|
|||||||
// Get status badge classes
|
// Get status badge classes
|
||||||
function getStatusClasses(status: Booking['status']): string {
|
function getStatusClasses(status: Booking['status']): string {
|
||||||
const baseClasses = 'inline-flex items-center rounded-full px-2 py-0.5 text-xs font-medium';
|
const baseClasses = 'inline-flex items-center rounded-full px-2 py-0.5 text-xs font-medium';
|
||||||
const statusMap = {
|
const statusMap: Record<string, string> = {
|
||||||
confirmed: 'bg-emerald-100 text-emerald-800',
|
confirmed: 'bg-emerald-100 text-emerald-800',
|
||||||
pending: 'bg-amber-100 text-amber-800',
|
pending: 'bg-amber-100 text-amber-800',
|
||||||
in_progress: 'bg-blue-100 text-blue-800',
|
in_progress: 'bg-blue-100 text-blue-800',
|
||||||
@@ -240,13 +168,14 @@
|
|||||||
client_cancelled: 'bg-red-100 text-red-800',
|
client_cancelled: 'bg-red-100 text-red-800',
|
||||||
we_cancelled: 'bg-rose-100 text-rose-800',
|
we_cancelled: 'bg-rose-100 text-rose-800',
|
||||||
're-schedule': 'bg-purple-100 text-purple-800',
|
're-schedule': 'bg-purple-100 text-purple-800',
|
||||||
no_show: 'bg-gray-100 text-gray-800'
|
no_show: 'bg-gray-100 text-gray-800',
|
||||||
|
no_deposit: 'bg-orange-100 text-orange-800'
|
||||||
};
|
};
|
||||||
return `${baseClasses} ${statusMap[status] || 'bg-gray-100 text-gray-800'}`;
|
return `${baseClasses} ${statusMap[status] || 'bg-gray-100 text-gray-800'}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getStatusDotClasses(status: Booking['status']): string {
|
function getStatusDotClasses(status: Booking['status']): string {
|
||||||
const statusMap = {
|
const statusMap: Record<string, string> = {
|
||||||
confirmed: 'bg-emerald-600',
|
confirmed: 'bg-emerald-600',
|
||||||
pending: 'bg-amber-600',
|
pending: 'bg-amber-600',
|
||||||
in_progress: 'bg-blue-600',
|
in_progress: 'bg-blue-600',
|
||||||
@@ -254,7 +183,8 @@
|
|||||||
client_cancelled: 'bg-red-600',
|
client_cancelled: 'bg-red-600',
|
||||||
we_cancelled: 'bg-rose-600',
|
we_cancelled: 'bg-rose-600',
|
||||||
're-schedule': 'bg-purple-600',
|
're-schedule': 'bg-purple-600',
|
||||||
no_show: 'bg-gray-600'
|
no_show: 'bg-gray-600',
|
||||||
|
no_deposit: 'bg-orange-600'
|
||||||
};
|
};
|
||||||
return `mr-1 h-1.5 w-1.5 rounded-full ${statusMap[status] || 'bg-gray-600'}`;
|
return `mr-1 h-1.5 w-1.5 rounded-full ${statusMap[status] || 'bg-gray-600'}`;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -651,17 +651,99 @@
|
|||||||
async function submitBooking() {
|
async function submitBooking() {
|
||||||
isSubmitting = true;
|
isSubmitting = true;
|
||||||
try {
|
try {
|
||||||
console.log('Submitting booking:', {
|
// Build the start_time in ISO format
|
||||||
services: selectedServices,
|
if (!selectedDate || !selectedTime) {
|
||||||
date: selectedDate,
|
toast.error('Please select a date and time');
|
||||||
time: selectedTime,
|
return;
|
||||||
customer: authStore.isAuthenticated ? authStore.currentUser : customerInfo
|
}
|
||||||
|
|
||||||
|
// Convert CalendarDate to JavaScript Date, then to ISO string
|
||||||
|
const [hours, minutes] = selectedTime.split(':').map(Number);
|
||||||
|
const bookingDate = selectedDate.toDate(getLocalTimeZone());
|
||||||
|
bookingDate.setHours(hours, minutes, 0, 0);
|
||||||
|
const startTimeISO = bookingDate.toISOString();
|
||||||
|
|
||||||
|
// Extract service IDs
|
||||||
|
const serviceIds = selectedServices.map((s) => s.id);
|
||||||
|
|
||||||
|
// Build request body
|
||||||
|
const requestBody = {
|
||||||
|
service_ids: serviceIds,
|
||||||
|
start_time: startTimeISO,
|
||||||
|
notes: customerInfo.specialRequests || null
|
||||||
|
};
|
||||||
|
|
||||||
|
console.log('Submitting booking:', requestBody);
|
||||||
|
|
||||||
|
const response = await fetch('/api/bookings', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
Authorization: `Bearer ${authStore.currentToken}`
|
||||||
|
},
|
||||||
|
body: JSON.stringify(requestBody)
|
||||||
});
|
});
|
||||||
|
|
||||||
toast.success('Booking submitted successfully!');
|
if (response.ok) {
|
||||||
|
const booking = await response.json();
|
||||||
|
console.log('Booking created:', booking);
|
||||||
|
|
||||||
|
// Show success message with booking details
|
||||||
|
const bookingDateStr = new Date(booking.start_time).toLocaleDateString('en-GB', {
|
||||||
|
weekday: 'long',
|
||||||
|
day: 'numeric',
|
||||||
|
month: 'long'
|
||||||
|
});
|
||||||
|
const bookingTimeStr = new Date(booking.start_time).toLocaleTimeString('en-GB', {
|
||||||
|
hour: 'numeric',
|
||||||
|
minute: '2-digit'
|
||||||
|
});
|
||||||
|
|
||||||
|
toast.success(`Booking confirmed for ${bookingDateStr} at ${bookingTimeStr}!`);
|
||||||
|
|
||||||
|
// Reset form and redirect to account page
|
||||||
|
selectedServices = [];
|
||||||
|
selectedDate = undefined;
|
||||||
|
selectedTime = null;
|
||||||
|
currentStep = 1;
|
||||||
|
|
||||||
|
// Navigate to account page to see bookings
|
||||||
|
window.location.href = '/account';
|
||||||
|
} else {
|
||||||
|
// Handle error response
|
||||||
|
const errorText = await response.text();
|
||||||
|
let errorMessage = 'Failed to submit booking. Please try again.';
|
||||||
|
|
||||||
|
// Try to parse error message from backend
|
||||||
|
try {
|
||||||
|
const errorData = JSON.parse(errorText);
|
||||||
|
if (errorData.error) {
|
||||||
|
errorMessage = errorData.error;
|
||||||
|
} else if (typeof errorData === 'string') {
|
||||||
|
errorMessage = errorData;
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Use default message if parsing fails
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle specific error cases
|
||||||
|
if (response.status === 409) {
|
||||||
|
toast.error('This time slot is no longer available. Please choose a different time.');
|
||||||
|
} else if (errorMessage.includes('patch test') || errorMessage.includes('Patch test')) {
|
||||||
|
toast.error(errorMessage + ' Please complete a patch test first.');
|
||||||
|
} else if (errorMessage.includes('48 hours') || errorMessage.includes('48h')) {
|
||||||
|
toast.error(errorMessage);
|
||||||
|
} else if (response.status === 400) {
|
||||||
|
toast.error(errorMessage);
|
||||||
|
} else {
|
||||||
|
toast.error('Failed to submit booking: ' + errorMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.error('Booking submission failed:', response.status, errorText);
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Booking submission failed:', error);
|
console.error('Booking submission error:', error);
|
||||||
toast.error('Failed to submit booking. Please try again.');
|
toast.error('Network error. Please check your connection and try again.');
|
||||||
} finally {
|
} finally {
|
||||||
isSubmitting = false;
|
isSubmitting = false;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,3 +39,94 @@ export interface AvailableHoursDay {
|
|||||||
slots: Array<{ startTime: string; endTime: string }>;
|
slots: Array<{ startTime: string; endTime: string }>;
|
||||||
source: string;
|
source: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type BookingStatus =
|
||||||
|
| 'pending'
|
||||||
|
| 'confirmed'
|
||||||
|
| 'in_progress'
|
||||||
|
| 'completed'
|
||||||
|
| 'client_cancelled'
|
||||||
|
| 'we_cancelled'
|
||||||
|
| 're-schedule'
|
||||||
|
| 'no_show'
|
||||||
|
| 'no_deposit';
|
||||||
|
|
||||||
|
export interface BookingService {
|
||||||
|
booking_id: string;
|
||||||
|
service_id: string;
|
||||||
|
override_price?: number;
|
||||||
|
override_duration_minutes?: number;
|
||||||
|
service_name?: string;
|
||||||
|
service_description?: string;
|
||||||
|
price?: number;
|
||||||
|
duration_minutes?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BookingUser {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Payment {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Booking {
|
||||||
|
id: string;
|
||||||
|
start_time: string;
|
||||||
|
status: BookingStatus;
|
||||||
|
notes?: string;
|
||||||
|
created_at: string;
|
||||||
|
updated_at: string;
|
||||||
|
created_by?: string;
|
||||||
|
|
||||||
|
// Deposit fields
|
||||||
|
deposit_required: boolean;
|
||||||
|
deposit_amount?: number;
|
||||||
|
deposit_paid: boolean;
|
||||||
|
deposit_deadline?: string;
|
||||||
|
|
||||||
|
// Joined fields
|
||||||
|
user?: BookingUser;
|
||||||
|
services?: BookingService[];
|
||||||
|
payments?: Payment[];
|
||||||
|
total_amount: number;
|
||||||
|
amount_paid: number;
|
||||||
|
amount_due: number;
|
||||||
|
duration_minutes: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BookingListResponse {
|
||||||
|
bookings: Booking[];
|
||||||
|
page: number;
|
||||||
|
per_page: number;
|
||||||
|
total: number;
|
||||||
|
total_pages: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user