Fix booking count filters, add admin notifications, and improve bookings
UI
This commit is contained in:
@@ -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
|
||||
}
|
||||
@@ -173,8 +172,8 @@ func RegisterHandler(w http.ResponseWriter, r *http.Request) {
|
||||
var userID string
|
||||
err = tx.QueryRow(r.Context(), `
|
||||
INSERT INTO users
|
||||
(n_first_name, n_last_name, phone, date_of_birth, email, password_hash,
|
||||
account_type, privacy_policy_and_terms_consent, policy_consent_updated_at,
|
||||
(n_first_name, n_last_name, phone, date_of_birth, email, password_hash,
|
||||
account_type, privacy_policy_and_terms_consent, policy_consent_updated_at,
|
||||
created_at, updated_at)
|
||||
VALUES
|
||||
($1, $2, $3, $4, $5, $6, 'email', $7, $8, $8, $8)
|
||||
@@ -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 {
|
||||
@@ -246,7 +244,7 @@ func LoginHandler(w http.ResponseWriter, r *http.Request) {
|
||||
var userID, passwordHash, role string
|
||||
ctx := context.Background()
|
||||
err := db.DB.QueryRow(ctx, `
|
||||
SELECT id, password_hash, account_role
|
||||
SELECT id, password_hash, account_role
|
||||
FROM users
|
||||
WHERE email = $1 AND account_type = 'email'
|
||||
`, req.Email).Scan(&userID, &passwordHash, &role)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -318,13 +318,13 @@ func GetAvailableHours(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
// Load bookings
|
||||
bookingRows, _ := db.DB.Query(r.Context(), `
|
||||
SELECT
|
||||
b.start_time,
|
||||
SELECT
|
||||
b.start_time,
|
||||
COALESCE(SUM(
|
||||
CASE
|
||||
WHEN bs.override_duration_minutes IS NOT NULL AND bs.override_duration_minutes > 0
|
||||
THEN bs.override_duration_minutes
|
||||
ELSE s.duration_minutes
|
||||
CASE
|
||||
WHEN bs.override_duration_minutes IS NOT NULL AND bs.override_duration_minutes > 0
|
||||
THEN bs.override_duration_minutes
|
||||
ELSE s.duration_minutes
|
||||
END
|
||||
), 0) AS total_duration
|
||||
FROM bookings b
|
||||
|
||||
Reference in New Issue
Block a user