|
|
|
@@ -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)
|
|
|
|
@@ -367,7 +372,7 @@ func GetAllAdminBookingsHandler(w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
// Build base query - only what the component needs
|
|
|
|
|
baseQuery := `
|
|
|
|
|
WITH booking_totals AS (
|
|
|
|
|
SELECT
|
|
|
|
|
SELECT
|
|
|
|
|
bs.booking_id,
|
|
|
|
|
SUM(COALESCE(bs.override_duration_minutes, s.duration_minutes)) as total_duration,
|
|
|
|
|
SUM(COALESCE(bs.override_price, s.price)) as total_amount
|
|
|
|
@@ -376,16 +381,16 @@ func GetAllAdminBookingsHandler(w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
GROUP BY bs.booking_id
|
|
|
|
|
),
|
|
|
|
|
payment_totals AS (
|
|
|
|
|
SELECT
|
|
|
|
|
SELECT
|
|
|
|
|
booking_id,
|
|
|
|
|
SUM(amount) as total_paid
|
|
|
|
|
FROM payments
|
|
|
|
|
FROM payments
|
|
|
|
|
WHERE status = 'completed'
|
|
|
|
|
GROUP BY booking_id
|
|
|
|
|
)
|
|
|
|
|
SELECT
|
|
|
|
|
b.id,
|
|
|
|
|
b.start_time,
|
|
|
|
|
SELECT
|
|
|
|
|
b.id,
|
|
|
|
|
b.start_time,
|
|
|
|
|
b.status,
|
|
|
|
|
u.fn,
|
|
|
|
|
COALESCE(bt.total_duration, 0) as duration_minutes,
|
|
|
|
@@ -707,10 +712,10 @@ func GetAdminBookingHandler(w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
booking.User = &UserSummary{}
|
|
|
|
|
|
|
|
|
|
err := db.DB.QueryRow(r.Context(), `
|
|
|
|
|
SELECT
|
|
|
|
|
SELECT
|
|
|
|
|
b.id, b.user_id, b.start_time, b.status, b.notes,
|
|
|
|
|
b.created_at, b.updated_at, b.created_by,
|
|
|
|
|
u.fn, u.email, u.phone, u.profile_pic_url, u.loyalty_stamps,
|
|
|
|
|
u.fn, u.email, u.phone, u.profile_pic_url, u.loyalty_stamps,
|
|
|
|
|
u.referral_code, u.notes
|
|
|
|
|
FROM bookings b
|
|
|
|
|
LEFT JOIN users u ON b.user_id = u.id
|
|
|
|
@@ -736,8 +741,8 @@ func GetAdminBookingHandler(w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
// ----------------------------
|
|
|
|
|
var referralCodeUses int
|
|
|
|
|
err = db.DB.QueryRow(r.Context(), `
|
|
|
|
|
SELECT COUNT(*)
|
|
|
|
|
FROM user_referrals
|
|
|
|
|
SELECT COUNT(*)
|
|
|
|
|
FROM user_referrals
|
|
|
|
|
WHERE referrer_id = $1
|
|
|
|
|
`, booking.User.ID).Scan(&referralCodeUses)
|
|
|
|
|
if err != nil {
|
|
|
|
@@ -751,7 +756,7 @@ func GetAdminBookingHandler(w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
// 2. Fetch services and calculate totals
|
|
|
|
|
// ----------------------------
|
|
|
|
|
serviceRows, err := db.DB.Query(r.Context(), `
|
|
|
|
|
SELECT
|
|
|
|
|
SELECT
|
|
|
|
|
s.name,
|
|
|
|
|
coalesce(bs.override_price, s.price) as price,
|
|
|
|
|
coalesce(bs.override_duration_minutes, s.duration_minutes) as duration_minutes
|
|
|
|
@@ -894,7 +899,7 @@ func SearchAdminBookingsHandler(w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
// Build the main search query using CTEs to match your actual schema
|
|
|
|
|
searchQuery := `
|
|
|
|
|
WITH booking_totals AS (
|
|
|
|
|
SELECT
|
|
|
|
|
SELECT
|
|
|
|
|
bs.booking_id,
|
|
|
|
|
SUM(COALESCE(bs.override_duration_minutes, s.duration_minutes)) as total_duration,
|
|
|
|
|
SUM(COALESCE(bs.override_price, s.price)) as total_amount
|
|
|
|
@@ -903,16 +908,16 @@ func SearchAdminBookingsHandler(w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
GROUP BY bs.booking_id
|
|
|
|
|
),
|
|
|
|
|
payment_totals AS (
|
|
|
|
|
SELECT
|
|
|
|
|
SELECT
|
|
|
|
|
booking_id,
|
|
|
|
|
SUM(amount) as total_paid
|
|
|
|
|
FROM payments
|
|
|
|
|
FROM payments
|
|
|
|
|
WHERE status = 'completed'
|
|
|
|
|
GROUP BY booking_id
|
|
|
|
|
)
|
|
|
|
|
SELECT
|
|
|
|
|
b.id,
|
|
|
|
|
b.start_time,
|
|
|
|
|
SELECT
|
|
|
|
|
b.id,
|
|
|
|
|
b.start_time,
|
|
|
|
|
b.status,
|
|
|
|
|
u.fn as full_name,
|
|
|
|
|
COALESCE(bt.total_duration, 0) as duration_minutes,
|
|
|
|
@@ -921,7 +926,7 @@ func SearchAdminBookingsHandler(w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
LEFT JOIN users u ON b.user_id = u.id
|
|
|
|
|
LEFT JOIN booking_totals bt ON b.id = bt.booking_id
|
|
|
|
|
LEFT JOIN payment_totals pt ON b.id = pt.booking_id
|
|
|
|
|
WHERE
|
|
|
|
|
WHERE
|
|
|
|
|
b.id ILIKE $1 ESCAPE '\' OR
|
|
|
|
|
b.notes ILIKE $1 ESCAPE '\' OR
|
|
|
|
|
b.status::text ILIKE $1 ESCAPE '\' OR
|
|
|
|
@@ -931,8 +936,8 @@ func SearchAdminBookingsHandler(w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
u.email ILIKE $1 ESCAPE '\' OR
|
|
|
|
|
u.phone ILIKE $1 ESCAPE '\' OR
|
|
|
|
|
EXISTS (
|
|
|
|
|
SELECT 1 FROM booking_services bs
|
|
|
|
|
JOIN services s ON bs.service_id = s.id
|
|
|
|
|
SELECT 1 FROM booking_services bs
|
|
|
|
|
JOIN services s ON bs.service_id = s.id
|
|
|
|
|
WHERE bs.booking_id = b.id AND s.name ILIKE $1 ESCAPE '\'
|
|
|
|
|
)
|
|
|
|
|
ORDER BY b.start_time ASC
|
|
|
|
@@ -945,7 +950,7 @@ func SearchAdminBookingsHandler(w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
LEFT JOIN users u ON b.user_id = u.id
|
|
|
|
|
LEFT JOIN booking_services bs ON b.id = bs.booking_id
|
|
|
|
|
LEFT JOIN services s ON bs.service_id = s.id
|
|
|
|
|
WHERE
|
|
|
|
|
WHERE
|
|
|
|
|
b.id ILIKE $1 ESCAPE '\' OR
|
|
|
|
|
b.notes ILIKE $1 ESCAPE '\' OR
|
|
|
|
|
b.status::text ILIKE $1 ESCAPE '\' OR
|
|
|
|
@@ -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
|
|
|
|
@@ -1202,7 +1220,7 @@ func EditBookingHandler(w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
|
|
|
|
|
// Update booking start time (only for user's own bookings)
|
|
|
|
|
query := `
|
|
|
|
|
UPDATE bookings
|
|
|
|
|
UPDATE bookings
|
|
|
|
|
SET start_time = $1, updated_at = NOW()
|
|
|
|
|
WHERE id = $2 AND user_id = $3
|
|
|
|
|
RETURNING id, user_id, start_time, status, notes, created_at, updated_at, created_by
|
|
|
|
@@ -1273,7 +1291,7 @@ func ProgressBookingHandler(w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
|
|
|
|
|
// Update booking status
|
|
|
|
|
query := `
|
|
|
|
|
UPDATE bookings
|
|
|
|
|
UPDATE bookings
|
|
|
|
|
SET status = $1, updated_at = NOW()
|
|
|
|
|
WHERE id = $2
|
|
|
|
|
RETURNING id, user_id, start_time, status, notes, created_at, updated_at, created_by
|
|
|
|
@@ -1354,7 +1372,7 @@ func ConfirmBookingHandler(w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
|
|
|
|
|
// Update booking status and notes
|
|
|
|
|
bookingQuery := `
|
|
|
|
|
UPDATE bookings
|
|
|
|
|
UPDATE bookings
|
|
|
|
|
SET status = 'confirmed', notes = COALESCE($1, notes), updated_at = NOW()
|
|
|
|
|
WHERE id = $2 AND status = 'pending'
|
|
|
|
|
RETURNING id, user_id, start_time, status, notes, created_at, updated_at, created_by
|
|
|
|
@@ -1391,7 +1409,7 @@ func ConfirmBookingHandler(w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
if len(req.ServiceOverrides) > 0 {
|
|
|
|
|
// First, verify all service IDs belong to this booking
|
|
|
|
|
serviceCheckQuery := `
|
|
|
|
|
SELECT COUNT(*) FROM booking_services
|
|
|
|
|
SELECT COUNT(*) FROM booking_services
|
|
|
|
|
WHERE booking_id = $1 AND service_id = ANY($2)
|
|
|
|
|
`
|
|
|
|
|
serviceIDs := make([]string, len(req.ServiceOverrides))
|
|
|
|
@@ -1413,7 +1431,7 @@ func ConfirmBookingHandler(w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
|
|
|
|
|
// Update each service override
|
|
|
|
|
serviceUpdateQuery := `
|
|
|
|
|
UPDATE booking_services
|
|
|
|
|
UPDATE booking_services
|
|
|
|
|
SET override_price = $1,
|
|
|
|
|
override_duration_minutes = $2
|
|
|
|
|
WHERE booking_id = $3 AND service_id = $4
|
|
|
|
@@ -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
|
|
|
|
|
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{}{
|
|
|
|
@@ -1592,7 +1662,7 @@ func GetBookingHandler(w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
var durationMinutes int
|
|
|
|
|
|
|
|
|
|
serviceRows, err := db.DB.Query(r.Context(), `
|
|
|
|
|
SELECT
|
|
|
|
|
SELECT
|
|
|
|
|
bs.service_id, bs.override_price, bs.override_duration_minutes,
|
|
|
|
|
s.name, s.description, s.price, s.duration_minutes
|
|
|
|
|
FROM booking_services bs
|
|
|
|
|