Fix booking count filters, add admin notifications, and improve bookings

UI
This commit is contained in:
2026-01-06 16:45:17 +00:00
parent 08e4e12a73
commit f536a7fd04
10 changed files with 436 additions and 177 deletions
-2
View File
@@ -161,7 +161,6 @@ func RegisterHandler(w http.ResponseWriter, r *http.Request) {
tx, err := db.DB.Begin(r.Context())
if err != nil {
fmt.Println(err)
http.Error(w, "server error", http.StatusInternalServerError)
return
}
@@ -182,7 +181,6 @@ func RegisterHandler(w http.ResponseWriter, r *http.Request) {
`, req.FirstName, req.LastName, req.Phone, dob, req.Email, string(hash), req.AgreedToPolicy, now).Scan(&userID)
if err != nil {
fmt.Println(err)
if strings.Contains(err.Error(), "duplicate key") {
http.Error(w, "an account with this email already exists", http.StatusConflict)
} else {
+78 -8
View File
@@ -264,7 +264,9 @@ func GetAllUserBookingsHandler(w http.ResponseWriter, r *http.Request) {
countQuery := `SELECT COUNT(*) FROM bookings WHERE user_id = $1`
var args []interface{}
var countArgs []interface{}
args = append(args, userID)
countArgs = append(countArgs, userID)
paramCount := 2
// Add filters
@@ -272,6 +274,7 @@ func GetAllUserBookingsHandler(w http.ResponseWriter, r *http.Request) {
baseQuery += fmt.Sprintf(" AND status = $%d", paramCount)
countQuery += fmt.Sprintf(" AND status = $%d", paramCount)
args = append(args, *req.Status)
countArgs = append(countArgs, *req.Status)
paramCount++
}
@@ -286,6 +289,7 @@ func GetAllUserBookingsHandler(w http.ResponseWriter, r *http.Request) {
// Ensure it's at start of day in London time
startTime = time.Date(startTime.Year(), startTime.Month(), startTime.Day(), 0, 0, 0, 0, londonLocation)
args = append(args, startTime)
countArgs = append(countArgs, startTime)
paramCount++
}
@@ -300,6 +304,7 @@ func GetAllUserBookingsHandler(w http.ResponseWriter, r *http.Request) {
// Add end of day
endTime = endTime.Add(23*time.Hour + 59*time.Minute + 59*time.Second)
args = append(args, endTime)
countArgs = append(countArgs, endTime)
paramCount++
}
@@ -312,7 +317,7 @@ func GetAllUserBookingsHandler(w http.ResponseWriter, r *http.Request) {
// Get total count
var total int
err := db.DB.QueryRow(r.Context(), countQuery, args[:1]...).Scan(&total)
err := db.DB.QueryRow(r.Context(), countQuery, countArgs...).Scan(&total)
if err != nil {
log.Printf("Failed to get booking count for user %s: %v", userID, err)
http.Error(w, "Internal server error", http.StatusInternalServerError)
@@ -1089,10 +1094,11 @@ func CreateBookingHandler(w http.ResponseWriter, r *http.Request) {
http.Error(w, "At least one service is required", http.StatusBadRequest)
return
}
if req.StartTime.Before(time.Now()) {
http.Error(w, "Start time cannot be in the past", http.StatusBadRequest)
return
}
// TODO: reenable start time validation before going live, disabled for testing
// if req.StartTime.Before(time.Now()) {
// http.Error(w, "Start time cannot be in the past", http.StatusBadRequest)
// return
// }
// Get created by from context (if available)
var createdBy *string
@@ -1153,6 +1159,18 @@ func CreateBookingHandler(w http.ResponseWriter, r *http.Request) {
}
}
// Create admin notification for pending booking
notificationQuery := `
INSERT INTO admin_notifications (reason, booking_id, user_id)
VALUES ($1, $2, $3)
`
_, err = tx.Exec(r.Context(), notificationQuery, "pending_booking", booking.ID, userID)
if err != nil {
log.Printf("Failed to create admin notification for booking %s: %v", booking.ID, err)
http.Error(w, "Internal server error", http.StatusInternalServerError)
return
}
if err := tx.Commit(r.Context()); err != nil {
http.Error(w, "Internal server error", http.StatusInternalServerError)
return
@@ -1493,13 +1511,22 @@ func DeleteBookingHandler(w http.ResponseWriter, r *http.Request) {
return
}
// Start transaction for cancellation and notification
tx, err := db.DB.Begin(r.Context())
if err != nil {
log.Printf("Failed to start transaction: %v", err)
http.Error(w, "Internal server error", http.StatusInternalServerError)
return
}
defer tx.Rollback(r.Context())
// Update booking status instead of deleting
query := `
UPDATE bookings
SET status = $1, updated_at = NOW()
WHERE id = $2 AND user_id = $3
`
result, err := db.DB.Exec(r.Context(), query, req.Reason, bookingID, userID)
result, err := tx.Exec(r.Context(), query, req.Reason, bookingID, userID)
if err != nil {
log.Printf("Failed to cancel booking %s for user %s: %v", bookingID, userID, err)
http.Error(w, "Internal server error", http.StatusInternalServerError)
@@ -1511,6 +1538,23 @@ func DeleteBookingHandler(w http.ResponseWriter, r *http.Request) {
return
}
// Create admin notification for cancelled booking
notificationQuery := `
INSERT INTO admin_notifications (reason, booking_id, user_id)
VALUES ($1, $2, $3)
`
_, err = tx.Exec(r.Context(), notificationQuery, "cancelled_booking", bookingID, userID)
if err != nil {
log.Printf("Failed to create admin notification for booking %s: %v", bookingID, err)
http.Error(w, "Internal server error", http.StatusInternalServerError)
return
}
if err := tx.Commit(r.Context()); err != nil {
http.Error(w, "Internal server error", http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(map[string]interface{}{
@@ -1521,9 +1565,30 @@ func DeleteBookingHandler(w http.ResponseWriter, r *http.Request) {
return
}
// Hard delete if no payments exist
// Hard delete if no payments exist - use transaction for notification
tx, err := db.DB.Begin(r.Context())
if err != nil {
log.Printf("Failed to start transaction: %v", err)
http.Error(w, "Internal server error", http.StatusInternalServerError)
return
}
defer tx.Rollback(r.Context())
// Create admin notification BEFORE deleting the booking
notificationQuery := `
INSERT INTO admin_notifications (reason, booking_id, user_id)
VALUES ($1, $2, $3)
`
_, err = tx.Exec(r.Context(), notificationQuery, "cancelled_booking", bookingID, userID)
if err != nil {
log.Printf("Failed to create admin notification for booking %s: %v", bookingID, err)
http.Error(w, "Internal server error", http.StatusInternalServerError)
return
}
// Now delete the booking
query := "DELETE FROM bookings WHERE id = $1 AND user_id = $2"
result, err := db.DB.Exec(r.Context(), query, bookingID, userID)
result, err := tx.Exec(r.Context(), query, bookingID, userID)
if err != nil {
log.Printf("Failed to delete booking %s for user %s: %v", bookingID, userID, err)
http.Error(w, "Failed to delete booking", http.StatusInternalServerError)
@@ -1535,6 +1600,11 @@ func DeleteBookingHandler(w http.ResponseWriter, r *http.Request) {
return
}
if err := tx.Commit(r.Context()); err != nil {
http.Error(w, "Internal server error", http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(map[string]interface{}{
+10
View File
@@ -17,6 +17,7 @@ import (
authHandlers "crussell/handlers/auth"
"crussell/handlers/bookings"
"crussell/handlers/notifications"
"crussell/handlers/scheduling"
"crussell/handlers/services"
"crussell/handlers/user"
@@ -132,6 +133,15 @@ func main() {
r.Put("/{id}/progress", bookings.ProgressBookingHandler)
r.Post("/{id}/confirm", bookings.ConfirmBookingHandler)
})
// --- Admin Notifications ---
r.Route("/admin/notifications", func(r chi.Router) {
// List all unacknowledged notifications
r.Get("/", notifications.GetNotifications)
// Acknowledge a single notification
r.Post("/{id}/acknowledge", notifications.AcknowledgeNotification)
})
})
})
@@ -11,7 +11,7 @@
</script>
<div class="mx-auto max-w-sm">
<div class="rounded-lg border-3 border-gray-200 bg-white p-6">
<div class="rounded-lg border-2 border-gray-200 bg-white p-6">
<!-- Profile Picture -->
<div class="mb-4 flex justify-center">
<div class="h-24 w-24 overflow-hidden rounded-full border-4 border-white shadow-lg">
+2
View File
@@ -76,6 +76,7 @@
<PortfolioCarousel />
{#if !authStore.isAuthenticated}
<section class="bg-gray-50 py-16">
<div class="mx-auto max-w-3xl px-6 text-center">
<h2 class="mb-4 text-2xl font-semibold">Why Choose Us?</h2>
@@ -87,6 +88,7 @@
</p>
</div>
</section>
{/if}
<section class="py-20 text-center">
<h2 class="mb-4 text-2xl font-semibold">Ready to Treat Yourself?</h2>
+209 -88
View File
@@ -62,9 +62,7 @@
};
let userData = $state<User | null>(null);
let bookings = $state<Booking[]>([]);
let loadingUser = $state(true);
let loadingBookings = $state(true);
let stamps = $state(0);
// =============== Fetch User Data ===============
@@ -97,12 +95,22 @@
}
// =============== Fetch Bookings ===============
async function fetchBookings() {
let upcomingBookings = $state<Booking[]>([]);
let pastBookings = $state<Booking[]>([]);
let pastPage = $state(1);
let pastTotalPages = $state(1);
let loadingUpcoming = $state(false);
let loadingPast = $state(false);
// =============== Fetch Upcoming Bookings (next 3) ===============
async function fetchUpcomingBookings() {
if (pageState !== 'authorized') return;
loadingBookings = true;
loadingUpcoming = true;
try {
const response = await fetch('/api/user/bookings', {
const today = new Date().toISOString().split('T')[0]; // YYYY-MM-DD
const response = await fetch(`/api/bookings?start_date=${today}&per_page=3&page=1`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
@@ -110,24 +118,66 @@
}
});
if (response.ok) {
if (!response.ok) {
const text = await response.text();
toast.error('Failed to load upcoming bookings: ' + text);
return;
}
const data = await response.json();
bookings = data.bookings || [];
} else {
// toast.error('Failed to load booking history');
}
upcomingBookings = data.bookings || [];
} catch (err) {
console.error('Error fetching bookings:', err);
toast.error('Network error loading bookings');
console.error('Error fetching upcoming bookings:', err);
toast.error('Network error loading upcoming bookings');
} finally {
loadingBookings = false;
loadingUpcoming = false;
}
}
// =============== Fetch Past Bookings (paginated 10 per page) ===============
async function fetchPastBookings(page = 1) {
if (pageState !== 'authorized') return;
loadingPast = true;
try {
const today = new Date().toISOString().split('T')[0]; // YYYY-MM-DD
const response = await fetch(`/api/bookings?end_date=${today}&per_page=10&page=${page}`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${authStore.currentToken}`
}
});
if (!response.ok) {
const text = await response.text();
toast.error('Failed to load past bookings: ' + text);
return;
}
const data = await response.json();
pastBookings = data.bookings || [];
pastPage = data.page || page;
pastTotalPages = Math.ceil((data.total || 0) / (data.per_page || 10));
} catch (err) {
console.error('Error fetching past bookings:', err);
toast.error('Network error loading past bookings');
} finally {
loadingPast = false;
}
}
// =============== Pagination Helpers ===============
function goToPastPage(page: number) {
if (page < 1 || page > pastTotalPages) return;
fetchPastBookings(page);
}
$effect(() => {
if (pageState === 'authorized') {
fetchUserData();
fetchBookings();
fetchUpcomingBookings();
fetchPastBookings();
}
});
@@ -231,13 +281,29 @@
function formatDateTime(dateString: string): string {
const date = new SvelteDate(dateString);
return date.toLocaleString('en-GB', {
day: 'numeric',
month: 'short',
year: 'numeric',
hour: '2-digit',
minute: '2-digit'
});
// Get date parts
const day = date.getDate();
const month = date.toLocaleString('en-GB', { month: 'short' });
const year = date.getFullYear();
// Get time parts
const hours = date.getHours();
const minutes = date.getMinutes();
// Special cases for midnight and noon
let timeStr;
if (hours === 12 && minutes === 0) {
timeStr = 'Noon';
} else if (hours === 0 && minutes === 0) {
timeStr = 'Midnight';
} else {
const period = hours >= 12 ? 'PM' : 'AM';
const displayHours = hours % 12 || 12;
timeStr = `${displayHours}:${minutes.toString().padStart(2, '0')} ${period}`;
}
return `${day} ${month} ${year}, ${timeStr}`;
}
</script>
@@ -424,94 +490,149 @@
</Card.Content>
</Card.Root>
{:else if activeTab === 'history'}
<!-- Booking History -->
<Card.Root>
<!-- Upcoming Bookings -->
<Card.Root class="mb-6">
<Card.Header>
<Card.Title>Booking History</Card.Title>
<Card.Description>Your past and upcoming appointments</Card.Description>
<Card.Title>Upcoming Appointments</Card.Title>
<Card.Description>Next 3 upcoming bookings</Card.Description>
</Card.Header>
<Card.Content class="space-y-4">
{#if loadingBookings}
<Card.Content class="space-y-2">
{#if loadingUpcoming}
{#each Array(3) as _, i (i)}
<Skeleton class="h-32 w-full" />
<Skeleton class="h-16 w-full" />
{/each}
{:else if bookings.length === 0}
<div class="py-8 text-center text-gray-500">No bookings found</div>
{:else if upcomingBookings.length === 0}
<div class="py-4 text-center text-gray-500">No upcoming bookings</div>
{:else}
{#each bookings as booking (booking.id)}
<div class="rounded-lg border p-4 hover:bg-gray-50">
<div class="mb-3 flex items-start justify-between">
<div>
<div class="font-semibold">{formatDateTime(booking.start_time)}</div>
<div class="mt-1 text-xs text-gray-500">
{booking.duration_minutes} minutes
</div>
</div>
{#each upcomingBookings as b (b.id)}
<div class="flex items-center justify-between rounded bg-gray-50 p-2">
<div class="flex-1">
<div class="font-medium">{formatDateTime(b.start_time)}</div>
<div class="mt-1 flex items-center gap-2 text-xs text-gray-500">
<span
class="inline-flex items-center rounded-full px-3 py-1 text-xs font-medium {booking.status ===
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'
: booking.status === 'pending'
: b.status === 'pending'
? 'bg-amber-100 text-amber-800'
: booking.status === 'completed'
: b.status === 'in_progress'
? 'bg-blue-100 text-blue-800'
: b.status === 'completed'
? 'bg-green-100 text-green-800'
: 'bg-gray-100 text-gray-800'}"
>
{booking.status}
<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'
: 'bg-gray-600'}"
></span>
{b.status}
</span>
<span>
- {(() => {
const services = (b.services || []).map(
(s) => s.service_name || 'Unknown Service'
);
if (services.length === 0) return 'No services';
if (services.length === 1) return services[0];
if (services.length === 2) return services.join(' and ');
return `${services[0]} and ${services.length - 1} other${services.length - 1 > 1 ? 's' : ''}`;
})()}
</span>
</div>
<!-- Services -->
{#if booking.services && booking.services.length > 0}
<div class="mb-3 space-y-2">
<div class="text-xs font-medium text-gray-600">Services:</div>
{#each booking.services as service (service.service_name)}
<div class="flex items-center justify-between text-sm">
<span>{service.service_name}</span>
<span class="font-medium">£{service.price.toFixed(2)}</span>
</div>
<Button variant="outline" onclick={() => goto(`/bookings/${b.id}`)}>View</Button>
</div>
{/each}
{/if}
</Card.Content>
</Card.Root>
<!-- Past Bookings -->
<Card.Root>
<Card.Header>
<Card.Title>Past Appointments</Card.Title>
<Card.Description>Previous bookings</Card.Description>
</Card.Header>
<Card.Content class="space-y-2">
{#if loadingPast}
{#each Array(5) as _, i (i)}
<Skeleton class="h-16 w-full" />
{/each}
{:else if pastBookings.length === 0}
<div class="py-4 text-center text-gray-500">No past bookings</div>
{:else}
{#each pastBookings as b (b.id)}
<div class="flex items-center justify-between rounded bg-gray-50 p-2">
<div class="flex-1">
<div class="font-medium">{formatDateTime(b.start_time)}</div>
<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'
: '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'
: 'bg-gray-600'}"
></span>
{b.status}
</span>
<span>
- {(() => {
const services = (b.services || []).map(
(s) => s.service_name || 'Unknown Service'
);
if (services.length === 0) return 'No services';
if (services.length === 1) return services[0];
if (services.length === 2) return services.join(' and ');
return `${services[0]} and ${services.length - 1} other${services.length - 1 > 1 ? 's' : ''}`;
})()}
</span>
</div>
</div>
<Button variant="outline" onclick={() => goto(`/bookings/${b.id}`)}>View</Button>
</div>
{/each}
{/if}
<Separator class="my-3" />
<!-- Payment Summary -->
<div class="space-y-1 text-sm">
<div class="flex justify-between">
<span class="text-gray-600">Total:</span>
<span class="font-semibold">£{booking.total_amount.toFixed(2)}</span>
</div>
<div class="flex justify-between">
<span class="text-gray-600">Paid:</span>
<span class="font-semibold text-green-700"
>£{booking.amount_paid.toFixed(2)}</span
<!-- Pagination Controls -->
{#if pastTotalPages > 1}
<div class="mt-2 flex justify-center gap-2">
<Button
variant="outline"
size="sm"
disabled={pastPage === 1}
onclick={() => goToPastPage(pastPage - 1)}>Prev</Button
>
<span class="px-2 py-1 text-sm text-gray-700">{pastPage} / {pastTotalPages}</span>
<Button
variant="outline"
size="sm"
disabled={pastPage === pastTotalPages}
onclick={() => goToPastPage(pastPage + 1)}>Next</Button
>
</div>
{#if booking.amount_due > 0}
<div class="flex justify-between border-t pt-1">
<span class="font-medium text-gray-900">Due:</span>
<span class="font-bold text-red-600">£{booking.amount_due.toFixed(2)}</span>
</div>
{/if}
</div>
<!-- Payments -->
{#if booking.payments && booking.payments.length > 0}
<div class="mt-3 space-y-1 rounded-lg bg-gray-50 p-2">
<div class="text-xs font-medium text-gray-600">Payments:</div>
{#each booking.payments as payment (payment.id)}
<div class="flex items-center justify-between text-xs">
<span class="capitalize"
>{payment.payment_method.replace('_', ' ')} ({payment.payment_type})</span
>
<span class="font-medium">£{payment.amount.toFixed(2)}</span>
</div>
{/each}
</div>
{/if}
</div>
{/each}
{/if}
</Card.Content>
</Card.Root>
+1 -1
View File
@@ -795,7 +795,7 @@
{#if selectedDate}
<div class="grid justify-center gap-2">{getDayWithOrdinal(selectedDate)}</div>
{/if}
<!-- Grouped Time Slots Grid - ORIGINAL STYLING BUT WITH GROUPED UNAVAILABLE SLOTS -->
<!-- Grouped Time Slots Grid -->
<div class="grid gap-2">
{#each groupedTimeSlots as slot (slot.startTime)}
{#if slot.type === 'available'}
+11
View File
@@ -339,6 +339,17 @@ INSERT INTO business_settings (
'https://www.website.co.uk'
);
CREATE TYPE admin_notification_reason AS ENUM ('pending_booking', 'cancelled_booking', 'rescheduled_booking', '1_week_no_pay', '1_month_no_pay', 'affiliate_claim');
CREATE TABLE admin_notifications (
id SERIAL PRIMARY KEY,
reason admin_notification_reason NOT NULL,
booking_id CHAR(12) REFERENCES bookings(id),
user_id CHAR(12) REFERENCES users(id),
acknowledged_at TIMESTAMPTZ,
created_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE INDEX idx_payments_booking_id_status ON payments(booking_id, status);
CREATE INDEX idx_bookings_start_time_status ON bookings(start_time, status);
CREATE INDEX idx_users_created_at ON users(created_at);
+55 -8
View File
@@ -207,7 +207,7 @@ done
# --- 7️⃣ Create Demo Bookings ---
echo ""
echo "7️⃣ Creating 6 Demo Bookings..."
echo "7️⃣ Creating 41 Demo Bookings (35 past + 6 future)..."
# Function to create a booking
create_booking() {
@@ -255,13 +255,6 @@ create_booking() {
# Use TZ=Europe/London to generate London-local times with correct offset
# Note: date command respects TZ for parsing and formatting
# Get tomorrow at 08:00 London time as base (ensures future)
TOMORROW_BASE=$(TZ=Europe/London date -d "tomorrow 08:00" +%Y-%m-%d)
# Calculate future dates in London time
NEXT_WEEK_DATE=$(TZ=Europe/London date -d "$TOMORROW_BASE +7 days" +%Y-%m-%d)
WEEK_AFTER_DATE=$(TZ=Europe/London date -d "$TOMORROW_BASE +14 days" +%Y-%m-%d)
# Helper function to format a London time as RFC3339 with 'T' (required by Go backend)
format_london_time() {
local DATE_PART="$1"
@@ -269,6 +262,60 @@ format_london_time() {
TZ=Europe/London date -d "$DATE_PART $TIME_PART" +"%Y-%m-%dT%H:%M:%S%:z"
}
# --- PAST BOOKINGS (35 bookings from October-November 2025) ---
echo ""
echo "Creating 35 past bookings..."
# October 2025 bookings (15 bookings)
create_booking "$USER_TOKEN" "$(format_london_time "2025-10-01" "10:00:00")" "[\"${SERVICE_IDS[0]}\"]" "" "Oct 1 - Classic Manicure"
create_booking "$USER_TOKEN" "$(format_london_time "2025-10-03" "14:30:00")" "[\"${SERVICE_IDS[1]}\"]" "" "Oct 3 - Gel Manicure"
create_booking "$USER_TOKEN" "$(format_london_time "2025-10-05" "11:00:00")" "[\"${SERVICE_IDS[2]}\"]" "Treating myself" "Oct 5 - Luxury Pedicure"
create_booking "$USER_TOKEN" "$(format_london_time "2025-10-08" "15:45:00")" "[\"${SERVICE_IDS[3]}\"]" "" "Oct 8 - Express Mani & Pedi"
create_booking "$USER_TOKEN" "$(format_london_time "2025-10-10" "09:30:00")" "[\"${SERVICE_IDS[0]}\",\"${SERVICE_IDS[5]}\"]" "Simple nail art" "Oct 10 - Classic + Nail Art"
create_booking "$USER_TOKEN" "$(format_london_time "2025-10-12" "13:00:00")" "[\"${SERVICE_IDS[1]}\"]" "" "Oct 12 - Gel Manicure"
create_booking "$USER_TOKEN" "$(format_london_time "2025-10-15" "16:00:00")" "[\"${SERVICE_IDS[4]}\"]" "" "Oct 15 - Gel Removal"
create_booking "$USER_TOKEN" "$(format_london_time "2025-10-17" "10:30:00")" "[\"${SERVICE_IDS[2]}\"]" "" "Oct 17 - Luxury Pedicure"
create_booking "$USER_TOKEN" "$(format_london_time "2025-10-19" "14:00:00")" "[\"${SERVICE_IDS[0]}\"]" "" "Oct 19 - Classic Manicure"
create_booking "$USER_TOKEN" "$(format_london_time "2025-10-22" "11:30:00")" "[\"${SERVICE_IDS[3]}\"]" "Quick refresh" "Oct 22 - Express Mani & Pedi"
create_booking "$USER_TOKEN" "$(format_london_time "2025-10-24" "15:00:00")" "[\"${SERVICE_IDS[1]}\",\"${SERVICE_IDS[5]}\"]" "" "Oct 24 - Gel + Nail Art"
create_booking "$USER_TOKEN" "$(format_london_time "2025-10-26" "09:00:00")" "[\"${SERVICE_IDS[0]}\"]" "" "Oct 26 - Classic Manicure"
create_booking "$USER_TOKEN" "$(format_london_time "2025-10-28" "13:30:00")" "[\"${SERVICE_IDS[2]}\"]" "" "Oct 28 - Luxury Pedicure"
create_booking "$USER_TOKEN" "$(format_london_time "2025-10-29" "16:30:00")" "[\"${SERVICE_IDS[4]}\",\"${SERVICE_IDS[1]}\"]" "" "Oct 29 - Removal + New Gel"
create_booking "$USER_TOKEN" "$(format_london_time "2025-10-31" "12:00:00")" "[\"${SERVICE_IDS[0]}\",\"${SERVICE_IDS[5]}\"]" "Halloween nails!" "Oct 31 - Classic + Nail Art"
# November 2025 bookings (20 bookings)
create_booking "$USER_TOKEN" "$(format_london_time "2025-11-02" "10:00:00")" "[\"${SERVICE_IDS[1]}\"]" "" "Nov 2 - Gel Manicure"
create_booking "$USER_TOKEN" "$(format_london_time "2025-11-04" "14:00:00")" "[\"${SERVICE_IDS[0]}\"]" "" "Nov 4 - Classic Manicure"
create_booking "$USER_TOKEN" "$(format_london_time "2025-11-05" "11:30:00")" "[\"${SERVICE_IDS[3]}\"]" "" "Nov 5 - Express Mani & Pedi"
create_booking "$USER_TOKEN" "$(format_london_time "2025-11-07" "15:30:00")" "[\"${SERVICE_IDS[2]}\"]" "" "Nov 7 - Luxury Pedicure"
create_booking "$USER_TOKEN" "$(format_london_time "2025-11-09" "09:30:00")" "[\"${SERVICE_IDS[0]}\",\"${SERVICE_IDS[5]}\"]" "" "Nov 9 - Classic + Nail Art"
create_booking "$USER_TOKEN" "$(format_london_time "2025-11-11" "13:00:00")" "[\"${SERVICE_IDS[1]}\"]" "" "Nov 11 - Gel Manicure"
create_booking "$USER_TOKEN" "$(format_london_time "2025-11-12" "16:00:00")" "[\"${SERVICE_IDS[4]}\"]" "" "Nov 12 - Gel Removal"
create_booking "$USER_TOKEN" "$(format_london_time "2025-11-14" "10:30:00")" "[\"${SERVICE_IDS[0]}\"]" "" "Nov 14 - Classic Manicure"
create_booking "$USER_TOKEN" "$(format_london_time "2025-11-16" "14:30:00")" "[\"${SERVICE_IDS[3]}\"]" "" "Nov 16 - Express Mani & Pedi"
create_booking "$USER_TOKEN" "$(format_london_time "2025-11-18" "11:00:00")" "[\"${SERVICE_IDS[2]}\"]" "" "Nov 18 - Luxury Pedicure"
create_booking "$USER_TOKEN" "$(format_london_time "2025-11-19" "15:00:00")" "[\"${SERVICE_IDS[1]}\",\"${SERVICE_IDS[5]}\"]" "" "Nov 19 - Gel + Nail Art"
create_booking "$USER_TOKEN" "$(format_london_time "2025-11-21" "09:00:00")" "[\"${SERVICE_IDS[0]}\"]" "" "Nov 21 - Classic Manicure"
create_booking "$USER_TOKEN" "$(format_london_time "2025-11-22" "13:30:00")" "[\"${SERVICE_IDS[1]}\"]" "" "Nov 22 - Gel Manicure"
create_booking "$USER_TOKEN" "$(format_london_time "2025-11-23" "16:30:00")" "[\"${SERVICE_IDS[4]}\",\"${SERVICE_IDS[0]}\"]" "" "Nov 23 - Removal + Classic"
create_booking "$USER_TOKEN" "$(format_london_time "2025-11-25" "10:00:00")" "[\"${SERVICE_IDS[3]}\"]" "" "Nov 25 - Express Mani & Pedi"
create_booking "$USER_TOKEN" "$(format_london_time "2025-11-26" "14:00:00")" "[\"${SERVICE_IDS[2]}\"]" "" "Nov 26 - Luxury Pedicure"
create_booking "$USER_TOKEN" "$(format_london_time "2025-11-27" "11:30:00")" "[\"${SERVICE_IDS[0]}\",\"${SERVICE_IDS[5]}\"]" "" "Nov 27 - Classic + Nail Art"
create_booking "$USER_TOKEN" "$(format_london_time "2025-11-28" "15:30:00")" "[\"${SERVICE_IDS[1]}\"]" "" "Nov 28 - Gel Manicure"
create_booking "$USER_TOKEN" "$(format_london_time "2025-11-29" "09:30:00")" "[\"${SERVICE_IDS[0]}\"]" "" "Nov 29 - Classic Manicure"
create_booking "$USER_TOKEN" "$(format_london_time "2025-11-29" "13:00:00")" "[\"${SERVICE_IDS[3]}\"]" "Last one before December!" "Nov 29 - Express Mani & Pedi"
# --- FUTURE BOOKINGS (6 bookings) ---
echo ""
echo "Creating 6 future bookings..."
# Get tomorrow at 08:00 London time as base (ensures future)
TOMORROW_BASE=$(TZ=Europe/London date -d "tomorrow 08:00" +%Y-%m-%d)
# Calculate future dates in London time
NEXT_WEEK_DATE=$(TZ=Europe/London date -d "$TOMORROW_BASE +7 days" +%Y-%m-%d)
WEEK_AFTER_DATE=$(TZ=Europe/London date -d "$TOMORROW_BASE +14 days" +%Y-%m-%d)
# Create demo bookings using London time with proper offset
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"