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">
+13 -11
View File
@@ -76,17 +76,19 @@
<PortfolioCarousel />
<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>
<p class="leading-relaxed text-gray-600">
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed non risus. Suspendisse lectus
tortor, dignissim sit amet, adipiscing nec, ultricies sed, dolor. Cras elementum ultrices
diam. Maecenas ligula massa, varius a, semper congue, euismod non, mi. Proin porttitor, orci
nec nonummy molestie,
</p>
</div>
</section>
{#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>
<p class="leading-relaxed text-gray-600">
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed non risus. Suspendisse lectus
tortor, dignissim sit amet, adipiscing nec, ultricies sed, dolor. Cras elementum ultrices
diam. Maecenas ligula massa, varius a, semper congue, euismod non, mi. Proin porttitor, orci
nec nonummy molestie,
</p>
</div>
</section>
{/if}
<section class="py-20 text-center">
<h2 class="mb-4 text-2xl font-semibold">Ready to Treat Yourself?</h2>
+220 -99
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) {
const data = await response.json();
bookings = data.bookings || [];
} else {
// toast.error('Failed to load booking history');
if (!response.ok) {
const text = await response.text();
toast.error('Failed to load upcoming bookings: ' + text);
return;
}
const data = await response.json();
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,97 +490,152 @@
</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>
<span
class="inline-flex items-center rounded-full px-3 py-1 text-xs font-medium {booking.status ===
'confirmed'
? 'bg-emerald-100 text-emerald-800'
: booking.status === 'pending'
? 'bg-amber-100 text-amber-800'
: booking.status === 'completed'
? 'bg-green-100 text-green-800'
: 'bg-gray-100 text-gray-800'}"
>
{booking.status}
</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>
{/each}
</div>
{/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
{#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-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>
{#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}
<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}
<!-- 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}
</Card.Content>
</Card.Root>
{:else if activeTab === 'referral'}
<!-- Referral Program -->
<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"