Bookings
This commit is contained in:
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -129,7 +129,6 @@ func main() {
|
|||||||
r.Get("/search", bookings.SearchAdminBookingsHandler)
|
r.Get("/search", bookings.SearchAdminBookingsHandler)
|
||||||
r.Get("/user/{user_id}", bookings.GetAllBookingsByUserHandler)
|
r.Get("/user/{user_id}", bookings.GetAllBookingsByUserHandler)
|
||||||
r.Get("/{id}", bookings.GetAdminBookingHandler)
|
r.Get("/{id}", bookings.GetAdminBookingHandler)
|
||||||
r.Get("/{id}/summary", bookings.GetAdminBookingSummaryHandler)
|
|
||||||
r.Put("/{id}/progress", bookings.ProgressBookingHandler)
|
r.Put("/{id}/progress", bookings.ProgressBookingHandler)
|
||||||
r.Post("/{id}/confirm", bookings.ConfirmBookingHandler)
|
r.Post("/{id}/confirm", bookings.ConfirmBookingHandler)
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -491,17 +491,75 @@
|
|||||||
|
|
||||||
type Booking = {
|
type Booking = {
|
||||||
id: string;
|
id: string;
|
||||||
user_id: string;
|
start_time: string; // ISO 8601
|
||||||
start_time: string;
|
status:
|
||||||
status: 'Confirmed' | 'Completed' | 'Cancelled' | 'Pending' | 'In Progress';
|
| 'pending'
|
||||||
|
| 'confirmed'
|
||||||
|
| 'in_progress'
|
||||||
|
| 'completed'
|
||||||
|
| 'client_cancelled'
|
||||||
|
| 'we_cancelled'
|
||||||
|
| 're-schedule'
|
||||||
|
| 'no_show';
|
||||||
notes?: string;
|
notes?: string;
|
||||||
created_at: string;
|
created_at: string; // ISO 8601
|
||||||
services?: { id: string; name: string }[];
|
updated_at: string; // ISO 8601
|
||||||
|
created_by?: string;
|
||||||
|
|
||||||
|
// Nested user object
|
||||||
user?: {
|
user?: {
|
||||||
full_name?: string;
|
id: string;
|
||||||
|
first_name: string;
|
||||||
|
last_name: string;
|
||||||
|
full_name: string;
|
||||||
email?: string;
|
email?: string;
|
||||||
phone?: 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[]>([]);
|
let bookings = $state<Booking[]>([]);
|
||||||
@@ -524,27 +582,39 @@
|
|||||||
});
|
});
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
const data = await response.json();
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Map the response correctly - the backend returns the full Booking objects
|
||||||
bookings = data.bookings.map((b: any) => ({
|
bookings = data.bookings.map((b: any) => ({
|
||||||
id: b.id,
|
id: b.id,
|
||||||
user_id: b.user_id,
|
|
||||||
start_time: b.start_time,
|
start_time: b.start_time,
|
||||||
status: b.status,
|
status: b.status,
|
||||||
notes: b.notes,
|
notes: b.notes,
|
||||||
created_at: b.created_at,
|
created_at: b.created_at,
|
||||||
services: b.services?.map((s: any) => ({
|
updated_at: b.updated_at,
|
||||||
id: s.service_id,
|
created_by: b.created_by,
|
||||||
name: s.service_name || 'Unknown Service'
|
// User info is nested under user object
|
||||||
})),
|
user: b.user
|
||||||
user: {
|
? {
|
||||||
full_name: b.user?.full_name,
|
id: b.user.id,
|
||||||
email: b.user?.email,
|
full_name: b.user.full_name
|
||||||
phone: b.user?.phone
|
// 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 {
|
} else {
|
||||||
const text = await response.text();
|
const text = await response.text();
|
||||||
toast.error('Failed to load bookings: ' + text);
|
toast.error('Failed to load bookings: ' + text);
|
||||||
@@ -561,6 +631,14 @@
|
|||||||
async function searchBookings() {
|
async function searchBookings() {
|
||||||
if (pageState !== 'authorized') return;
|
if (pageState !== 'authorized') return;
|
||||||
loadingSearch = true;
|
loadingSearch = true;
|
||||||
|
|
||||||
|
// If no search query, use the regular get-all endpoint
|
||||||
|
if (!bookingQuery.trim()) {
|
||||||
|
await fetchBookings();
|
||||||
|
loadingSearch = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
`/api/admin/bookings/search?q=${encodeURIComponent(bookingQuery)}`,
|
`/api/admin/bookings/search?q=${encodeURIComponent(bookingQuery)}`,
|
||||||
@@ -574,22 +652,28 @@
|
|||||||
);
|
);
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
const data = await response.json();
|
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) => ({
|
bookings = data.bookings.map((b: any) => ({
|
||||||
id: b.booking.id,
|
id: b.id,
|
||||||
user_id: b.booking.user_id,
|
start_time: b.start_time,
|
||||||
start_time: b.booking.start_time,
|
status: b.status,
|
||||||
status: b.booking.status,
|
notes: b.notes,
|
||||||
notes: b.booking.notes,
|
created_at: b.created_at,
|
||||||
created_at: b.booking.created_at,
|
updated_at: b.updated_at,
|
||||||
services: b.services?.map((s: any) => ({
|
created_by: b.created_by,
|
||||||
id: s.service_id,
|
user: b.user
|
||||||
name: s.service_name || 'Unknown Service'
|
? {
|
||||||
})),
|
id: b.user.id,
|
||||||
user: {
|
full_name: b.user.full_name
|
||||||
full_name: b.user?.full_name,
|
}
|
||||||
email: b.user?.email,
|
: undefined,
|
||||||
phone: b.user?.phone
|
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 {
|
} else {
|
||||||
const text = await response.text();
|
const text = await response.text();
|
||||||
@@ -607,7 +691,7 @@
|
|||||||
async function openBookingModal(bookingId: string) {
|
async function openBookingModal(bookingId: string) {
|
||||||
if (pageState !== 'authorized') return;
|
if (pageState !== 'authorized') return;
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`/api/admin/bookings/${bookingId}/summary`, {
|
const response = await fetch(`/api/admin/bookings/${bookingId}`, {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
@@ -616,22 +700,63 @@
|
|||||||
});
|
});
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
|
console.log('Booking details API response:', data); // Debug log
|
||||||
|
|
||||||
selectedBooking = {
|
selectedBooking = {
|
||||||
id: data.booking.id,
|
id: data.id,
|
||||||
user_id: data.booking.user_id,
|
start_time: data.start_time,
|
||||||
start_time: data.booking.start_time,
|
status: data.status,
|
||||||
status: data.booking.status,
|
notes: data.notes,
|
||||||
notes: data.booking.notes,
|
user: data.user
|
||||||
created_at: data.booking.created_at,
|
? {
|
||||||
services: data.services?.map((s: any) => ({
|
id: data.user.id,
|
||||||
id: s.service_id,
|
first_name: data.user.first_name,
|
||||||
name: s.service_name || 'Unknown Service'
|
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: {
|
payments: (data.payments || []).map((p: any) => ({
|
||||||
full_name: data.user?.full_name,
|
id: p.id,
|
||||||
email: data.user?.email,
|
booking_id: p.booking_id,
|
||||||
phone: data.user?.phone
|
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;
|
showBookingModal = true;
|
||||||
} else {
|
} else {
|
||||||
@@ -687,7 +812,7 @@
|
|||||||
|
|
||||||
// Filter demo bookings for this user
|
// Filter demo bookings for this user
|
||||||
bookingUserHistory = bookings
|
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());
|
.sort((a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime());
|
||||||
|
|
||||||
showUserModal = true;
|
showUserModal = true;
|
||||||
@@ -770,7 +895,6 @@
|
|||||||
// Toggle service active status
|
// Toggle service active status
|
||||||
async function toggleService(serviceId: string) {
|
async function toggleService(serviceId: string) {
|
||||||
servicesUpdating[serviceId] = true;
|
servicesUpdating[serviceId] = true;
|
||||||
console.log('toggling', serviceId);
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`/api/admin/services/${serviceId}/toggle`, {
|
const response = await fetch(`/api/admin/services/${serviceId}/toggle`, {
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
@@ -1424,7 +1548,7 @@
|
|||||||
<div>
|
<div>
|
||||||
<div class="flex gap-2">
|
<div class="flex gap-2">
|
||||||
<Input
|
<Input
|
||||||
placeholder="Search by name, email, phone, or service"
|
placeholder="Search by customer name, email, phone, or service"
|
||||||
bind:value={bookingQuery}
|
bind:value={bookingQuery}
|
||||||
onkeyup={(e) => {
|
onkeyup={(e) => {
|
||||||
if ((e as KeyboardEvent).key === 'Enter') searchBookings();
|
if ((e as KeyboardEvent).key === 'Enter') searchBookings();
|
||||||
@@ -1444,14 +1568,137 @@
|
|||||||
{:else}
|
{:else}
|
||||||
{#each bookings as b}
|
{#each bookings as b}
|
||||||
<div class="flex items-center justify-between rounded bg-gray-50 p-2">
|
<div class="flex items-center justify-between rounded bg-gray-50 p-2">
|
||||||
<div>
|
<div class="flex-1">
|
||||||
<div class="font-medium">
|
<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>
|
||||||
<div class="text-xs text-gray-500">
|
<div class="mt-1 flex items-center gap-2 text-xs text-gray-500">
|
||||||
{b.status} • {b.user?.full_name || 'Unknown User'} • {b.services
|
<span
|
||||||
?.map((s) => s.name)
|
class="inline-flex items-center rounded-full px-2 py-0.5 text-xs font-medium {b.status ===
|
||||||
.join(', ')}
|
'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>
|
||||||
</div>
|
</div>
|
||||||
<Button variant="outline" onclick={() => openBookingModal(b.id)}>View</Button>
|
<Button variant="outline" onclick={() => openBookingModal(b.id)}>View</Button>
|
||||||
@@ -2451,7 +2698,7 @@
|
|||||||
<div>
|
<div>
|
||||||
<div class="font-medium">{new Date(hb.start_time).toLocaleString()}</div>
|
<div class="font-medium">{new Date(hb.start_time).toLocaleString()}</div>
|
||||||
<div class="text-xs text-gray-500">
|
<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>
|
||||||
</div>
|
</div>
|
||||||
<Button variant="outline" onclick={() => openBookingModal(hb.id)}>Open</Button>
|
<Button variant="outline" onclick={() => openBookingModal(hb.id)}>Open</Button>
|
||||||
@@ -2469,56 +2716,243 @@
|
|||||||
|
|
||||||
{#if selectedBooking}
|
{#if selectedBooking}
|
||||||
<Modal.Root bind:open={showBookingModal}>
|
<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.Header>
|
||||||
<Modal.Title class="text-lg font-semibold">
|
<Modal.Title class="text-lg font-semibold">Booking Details</Modal.Title>
|
||||||
Booking: {selectedBooking.id}
|
<div class="mt-1 text-sm text-gray-500">ID: {selectedBooking.id}</div>
|
||||||
</Modal.Title>
|
|
||||||
</Modal.Header>
|
</Modal.Header>
|
||||||
|
|
||||||
<!-- Main Booking Details Grid -->
|
<div class="space-y-6 px-4 pb-4">
|
||||||
<div class="grid gap-4 px-4 pb-4 md:grid-cols-2">
|
<!-- Status Badge -->
|
||||||
<!-- Column 1: Customer Details -->
|
<div class="flex items-center gap-2">
|
||||||
<div>
|
<span
|
||||||
<div class="text-sm text-gray-500">Customer</div>
|
class="inline-flex items-center rounded-full px-3 py-1 text-sm font-medium
|
||||||
<div class="font-medium">
|
{selectedBooking.status === 'pending'
|
||||||
{selectedBooking.user?.full_name || 'Unknown User'}
|
? 'bg-yellow-100 text-yellow-800'
|
||||||
</div>
|
: selectedBooking.status === 'confirmed'
|
||||||
<div class="mt-2 text-sm text-gray-500">Email</div>
|
? 'bg-emerald-100 text-emerald-800'
|
||||||
<div class="font-medium">{selectedBooking.user?.email || '—'}</div>
|
: selectedBooking.status === 'in_progress'
|
||||||
<div class="mt-2 text-sm text-gray-500">Phone</div>
|
? 'bg-blue-100 text-blue-800'
|
||||||
<div class="font-medium">{selectedBooking.user?.phone || '—'}</div>
|
: selectedBooking.status === 'completed'
|
||||||
<div class="mt-2 text-sm text-gray-500">Status</div>
|
? 'bg-green-100 text-green-800'
|
||||||
<div class="font-medium">{selectedBooking.status}</div>
|
: 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>
|
</div>
|
||||||
|
|
||||||
<!-- Column 2: Appointment Details -->
|
<!-- Customer Information -->
|
||||||
<div>
|
<div class="rounded-lg border border-gray-200 bg-gray-50 p-4">
|
||||||
<div class="text-sm text-gray-500">Scheduled</div>
|
<h3 class="mb-3 text-sm font-semibold uppercase tracking-wide text-gray-600">
|
||||||
<div class="font-medium">
|
Customer Information
|
||||||
{new Date(selectedBooking.start_time).toLocaleString()}
|
</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>
|
||||||
<div class="mt-2 text-sm text-gray-500">Services</div>
|
{#if selectedBooking.user?.notes}
|
||||||
<div class="mb-4 font-medium">
|
<div class="mt-3 rounded-md border border-blue-200 bg-blue-50 p-3">
|
||||||
{selectedBooking.services?.map((s) => s.name).join(', ') || '—'}
|
<div class="mb-1 text-xs font-semibold text-blue-800">Customer Notes</div>
|
||||||
</div>
|
<div class="text-sm text-blue-900">{selectedBooking.user.notes}</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.'}
|
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
|
||||||
<div class="mb-4 font-medium">—</div>
|
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</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>
|
</div>
|
||||||
|
|
||||||
<Modal.Footer class="flex items-center justify-end gap-2">
|
<Modal.Footer class="flex items-center justify-end gap-2">
|
||||||
|
|||||||
@@ -14,8 +14,8 @@ CREATE TYPE account_role AS ENUM ('unverified_email', 'verified_email', 'admin',
|
|||||||
CREATE TYPE account_type AS ENUM ('email', 'google', 'microsoft', 'facebook', 'guest');
|
CREATE TYPE account_type AS ENUM ('email', 'google', 'microsoft', 'facebook', 'guest');
|
||||||
CREATE TYPE payment_type AS ENUM ('deposit', 'full', 'tip', 'balance', 'partial');
|
CREATE TYPE payment_type AS ENUM ('deposit', 'full', 'tip', 'balance', 'partial');
|
||||||
CREATE TYPE payment_method AS ENUM ('online_square', 'in_person_card', 'cash', 'giftcard', 'discount');
|
CREATE TYPE payment_method AS ENUM ('online_square', 'in_person_card', 'cash', 'giftcard', 'discount');
|
||||||
CREATE TYPE booking_status AS ENUM ('pending', 'confirmed', 'in_progress', 'completed', 'client_cancelled', 'we_cancelled', 're-schedule', 'no_show');
|
|
||||||
CREATE TYPE payment_status AS ENUM ('pending', 'completed', 'failed', 'refunded');
|
CREATE TYPE payment_status AS ENUM ('pending', 'completed', 'failed', 'refunded');
|
||||||
|
CREATE TYPE booking_status AS ENUM ('pending', 'confirmed', 'in_progress', 'completed', 'client_cancelled', 'we_cancelled', 're-schedule', 'no_show');
|
||||||
|
|
||||||
-- =======================================
|
-- =======================================
|
||||||
-- SHORT ID GENERATION
|
-- SHORT ID GENERATION
|
||||||
@@ -130,6 +130,7 @@ CREATE TABLE user_referrals (
|
|||||||
referrer_id CHAR(12) NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
referrer_id CHAR(12) NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||||
referred_id CHAR(12) NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
referred_id CHAR(12) NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||||
referred_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
referred_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
|
claimed_booking_id CHAR(12) REFERENCES bookings(id) ON DELETE SET NULL,
|
||||||
PRIMARY KEY (referrer_id, referred_id)
|
PRIMARY KEY (referrer_id, referred_id)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
+57
-60
@@ -209,76 +209,73 @@ done
|
|||||||
echo ""
|
echo ""
|
||||||
echo "7️⃣ Creating 6 Demo Bookings..."
|
echo "7️⃣ Creating 6 Demo Bookings..."
|
||||||
|
|
||||||
# Check if we have enough services created
|
# Function to create a booking
|
||||||
if [ ${#SERVICE_IDS[@]} -lt 6 ]; then
|
create_booking() {
|
||||||
echo "❌ ERROR: Only ${#SERVICE_IDS[@]} services were created successfully."
|
local TOKEN=$1
|
||||||
echo " Need at least 6 services to create demo bookings."
|
local START_TIME=$2
|
||||||
echo " Skipping booking creation..."
|
local SERVICE_IDS_JSON=$3
|
||||||
|
local NOTES=$4
|
||||||
|
local BOOKING_NAME=$5
|
||||||
|
|
||||||
|
local BOOKING_JSON="{\"start_time\":\"$START_TIME\",\"service_ids\":$SERVICE_IDS_JSON"
|
||||||
|
if [ -n "$NOTES" ]; then
|
||||||
|
BOOKING_JSON="$BOOKING_JSON,\"notes\":\"$NOTES\""
|
||||||
|
fi
|
||||||
|
BOOKING_JSON="$BOOKING_JSON}"
|
||||||
|
|
||||||
echo ""
|
echo ""
|
||||||
else
|
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||||
echo "✅ All 6 services available. Proceeding with booking creation..."
|
echo "Creating: $BOOKING_NAME"
|
||||||
|
echo "Time: $START_TIME"
|
||||||
|
echo "Services: $SERVICE_IDS_JSON"
|
||||||
|
|
||||||
# Function to create a booking
|
CREATE_RESPONSE=$(curl -s -w "\n%{http_code}" -X POST \
|
||||||
create_booking() {
|
-H 'Content-Type: application/json' \
|
||||||
local TOKEN=$1
|
-H "Authorization: Bearer $TOKEN" \
|
||||||
local START_TIME=$2
|
-d "$BOOKING_JSON" \
|
||||||
local SERVICE_IDS_JSON=$3
|
"$BASE_URL/bookings")
|
||||||
local NOTES=$4
|
|
||||||
local BOOKING_NAME=$5
|
|
||||||
|
|
||||||
local BOOKING_JSON="{\"start_time\":\"$START_TIME\",\"service_ids\":$SERVICE_IDS_JSON"
|
HTTP_CODE=$(echo "$CREATE_RESPONSE" | tail -n1)
|
||||||
if [ -n "$NOTES" ]; then
|
RESPONSE_BODY=$(echo "$CREATE_RESPONSE" | sed '$d')
|
||||||
BOOKING_JSON="$BOOKING_JSON,\"notes\":\"$NOTES\""
|
|
||||||
|
if [ "$HTTP_CODE" = "201" ]; then
|
||||||
|
echo "✅ Created: $BOOKING_NAME"
|
||||||
|
BOOKING_ID=$(echo "$RESPONSE_BODY" | grep -o '"id":"[^"]*' | cut -d'"' -f4)
|
||||||
|
echo " Booking ID: $BOOKING_ID"
|
||||||
|
else
|
||||||
|
echo "❌ Failed to create: $BOOKING_NAME (HTTP $HTTP_CODE)"
|
||||||
|
if [ -n "$RESPONSE_BODY" ]; then
|
||||||
|
echo "Response body: $RESPONSE_BODY"
|
||||||
fi
|
fi
|
||||||
BOOKING_JSON="$BOOKING_JSON}"
|
fi
|
||||||
|
|
||||||
echo ""
|
sleep 0.2
|
||||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
}
|
||||||
echo "Creating: $BOOKING_NAME"
|
|
||||||
echo "Time: $START_TIME"
|
|
||||||
echo "Services: $SERVICE_IDS_JSON"
|
|
||||||
|
|
||||||
CREATE_RESPONSE=$(curl -s -w "\n%{http_code}" -X POST \
|
# Use TZ=Europe/London to generate London-local times with correct offset
|
||||||
-H 'Content-Type: application/json' \
|
# Note: date command respects TZ for parsing and formatting
|
||||||
-H "Authorization: Bearer $TOKEN" \
|
|
||||||
-d "$BOOKING_JSON" \
|
|
||||||
"$BASE_URL/bookings")
|
|
||||||
|
|
||||||
HTTP_CODE=$(echo "$CREATE_RESPONSE" | tail -n1)
|
# Get tomorrow at 08:00 London time as base (ensures future)
|
||||||
RESPONSE_BODY=$(echo "$CREATE_RESPONSE" | sed '$d')
|
TOMORROW_BASE=$(TZ=Europe/London date -d "tomorrow 08:00" +%Y-%m-%d)
|
||||||
|
|
||||||
if [ "$HTTP_CODE" = "201" ]; then
|
# Calculate future dates in London time
|
||||||
echo "✅ Created: $BOOKING_NAME"
|
NEXT_WEEK_DATE=$(TZ=Europe/London date -d "$TOMORROW_BASE +7 days" +%Y-%m-%d)
|
||||||
BOOKING_ID=$(echo "$RESPONSE_BODY" | grep -o '"id":"[^"]*' | cut -d'"' -f4)
|
WEEK_AFTER_DATE=$(TZ=Europe/London date -d "$TOMORROW_BASE +14 days" +%Y-%m-%d)
|
||||||
echo " Booking ID: $BOOKING_ID"
|
|
||||||
else
|
|
||||||
echo "❌ Failed to create: $BOOKING_NAME (HTTP $HTTP_CODE)"
|
|
||||||
if [ -n "$RESPONSE_BODY" ]; then
|
|
||||||
echo "Response body: $RESPONSE_BODY"
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
sleep 0.2
|
# Helper function to format a London time as RFC3339 with 'T' (required by Go backend)
|
||||||
}
|
format_london_time() {
|
||||||
|
local DATE_PART="$1"
|
||||||
|
local TIME_PART="$2"
|
||||||
|
TZ=Europe/London date -d "$DATE_PART $TIME_PART" +"%Y-%m-%dT%H:%M:%S%:z"
|
||||||
|
}
|
||||||
|
|
||||||
# 1. Establish the base day (Tomorrow) with a time that guarantees it's in the future.
|
# Create demo bookings using London time with proper offset
|
||||||
TOMORROW_BASE=$(date -u -d "tomorrow 08:00:00" +%Y-%m-%d)
|
create_booking "$USER_TOKEN" "$(format_london_time "$TOMORROW_BASE" "10:00:00")" "[\"${SERVICE_IDS[0]}\"]" "" "Classic Manicure - Tomorrow 10:00"
|
||||||
|
create_booking "$USER_TOKEN" "$(format_london_time "$TOMORROW_BASE" "14:30:00")" "[\"${SERVICE_IDS[1]}\",\"${SERVICE_IDS[5]}\"]" "Want French manicure with simple nail art on accent fingers" "Gel Manicure + Nail Art - Tomorrow 14:30"
|
||||||
# 2. Calculate the date 7 days after the TOMORROW_BASE date
|
create_booking "$USER_TOKEN" "$(format_london_time "$NEXT_WEEK_DATE" "11:00:00")" "[\"${SERVICE_IDS[2]}\"]" "Special treat for myself" "Luxury Pedicure - Next Week 11:00"
|
||||||
NEXT_WEEK_DATE=$(date -u -d "$TOMORROW_BASE +7 days" +%Y-%m-%d)
|
create_booking "$USER_TOKEN" "$(format_london_time "$NEXT_WEEK_DATE" "15:45:00")" "[\"${SERVICE_IDS[3]}\"]" "Need quick refresh before event" "Express Mani & Pedi - Next Week 15:45"
|
||||||
|
create_booking "$USER_TOKEN" "$(format_london_time "$WEEK_AFTER_DATE" "13:15:00")" "[\"${SERVICE_IDS[4]}\",\"${SERVICE_IDS[1]}\"]" "Remove old gel and apply new BIAB" "Gel Removal + New Gel - Week After 13:15"
|
||||||
# 3. Calculate the date 14 days after the TOMORROW_BASE date
|
create_booking "$USER_TOKEN" "$(format_london_time "$WEEK_AFTER_DATE" "16:30:00")" "[\"${SERVICE_IDS[0]}\",\"${SERVICE_IDS[5]}\"]" "Birthday celebration - want something special!" "Classic + Nail Art - Week After 16:30"
|
||||||
WEEK_AFTER_DATE=$(date -u -d "$TOMORROW_BASE +14 days" +%Y-%m-%d)
|
|
||||||
|
|
||||||
|
|
||||||
# Create demo bookings using the full timestamp (using the fixed dates)
|
|
||||||
create_booking "$USER_TOKEN" "$(date -u -d "$TOMORROW_BASE 10:00:00" +%Y-%m-%dT%H:%M:%SZ)" "[\"${SERVICE_IDS[0]}\"]" "" "Classic Manicure - Tomorrow 10:00"
|
|
||||||
create_booking "$USER_TOKEN" "$(date -u -d "$TOMORROW_BASE 14:30:00" +%Y-%m-%dT%H:%M:%SZ)" "[\"${SERVICE_IDS[1]}\",\"${SERVICE_IDS[5]}\"]" "Want French manicure with simple nail art on accent fingers" "Gel Manicure + Nail Art - Tomorrow 14:30"
|
|
||||||
create_booking "$USER_TOKEN" "$(date -u -d "$NEXT_WEEK_DATE 11:00:00" +%Y-%m-%dT%H:%M:%SZ)" "[\"${SERVICE_IDS[2]}\"]" "Special treat for myself" "Luxury Pedicure - Next Week 11:00"
|
|
||||||
create_booking "$USER_TOKEN" "$(date -u -d "$NEXT_WEEK_DATE 15:45:00" +%Y-%m-%dT%H:%M:%SZ)" "[\"${SERVICE_IDS[3]}\"]" "Need quick refresh before event" "Express Mani & Pedi - Next Week 15:45"
|
|
||||||
create_booking "$USER_TOKEN" "$(date -u -d "$WEEK_AFTER_DATE 13:15:00" +%Y-%m-%dT%H:%M:%SZ)" "[\"${SERVICE_IDS[4]}\",\"${SERVICE_IDS[1]}\"]" "Remove old gel and apply new BIAB" "Gel Removal + New Gel - Week After 13:15"
|
|
||||||
create_booking "$USER_TOKEN" "$(date -u -d "$WEEK_AFTER_DATE 16:30:00" +%Y-%m-%dT%H:%M:%SZ)" "[\"${SERVICE_IDS[0]}\",\"${SERVICE_IDS[5]}\"]" "Birthday celebration - want something special!" "Classic + Nail Art - Week After 16:30"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# --- 8️⃣ Create Holiday Exceptional Groups ---
|
# --- 8️⃣ Create Holiday Exceptional Groups ---
|
||||||
echo ""
|
echo ""
|
||||||
|
|||||||
Reference in New Issue
Block a user