refactor(bookings): replace inline computed queries with booking fields
Simplify bookings handlers by using bookings.total_duration_minutes, bookings.total_amount, and bookings.end_time computed columns instead of inline UNION sub-queries against booking_services/booking_custom_services. Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
@@ -110,7 +110,7 @@ func AdminReserveSlotHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
// DB uses 0=Monday..6=Sunday; Go uses 0=Sunday..6=Saturday. Convert.
|
// DB uses 0=Monday..6=Sunday; Go uses 0=Sunday..6=Saturday. Convert.
|
||||||
weekday := int((localStart.Weekday() + 6) % 7)
|
weekday := int((localStart.Weekday() + 6) % 7)
|
||||||
var closeStr string
|
var closeStr string
|
||||||
if err := db.Conn.QueryRow(r.Context(), `SELECT end_time::text FROM working_hours WHERE weekday = $1`, weekday).Scan(&closeStr); err != nil {
|
if err := db.Conn.QueryRow(r.Context(), `SELECT end_time FROM working_hours WHERE weekday = $1`, weekday).Scan(&closeStr); err != nil {
|
||||||
if errors.Is(err, pgx.ErrNoRows) {
|
if errors.Is(err, pgx.ErrNoRows) {
|
||||||
http.Error(w, "Not open on this day", http.StatusBadRequest)
|
http.Error(w, "Not open on this day", http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
@@ -132,15 +132,7 @@ func AdminReserveSlotHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
if err := db.Conn.QueryRow(r.Context(), `
|
if err := db.Conn.QueryRow(r.Context(), `
|
||||||
SELECT COUNT(*) FROM bookings WHERE status IN ('pending','confirmed','in_progress','completed')
|
SELECT COUNT(*) FROM bookings WHERE status IN ('pending','confirmed','in_progress','completed')
|
||||||
AND start_time < $2
|
AND start_time < $2
|
||||||
AND start_time + (INTERVAL '1 minute' * (
|
AND end_time > $1
|
||||||
SELECT COALESCE(SUM(dur),60) FROM (
|
|
||||||
SELECT COALESCE(bs.override_duration_minutes,s.duration_minutes) AS dur
|
|
||||||
FROM booking_services bs JOIN services s ON bs.service_id=s.id WHERE bs.booking_id=bookings.id
|
|
||||||
UNION ALL
|
|
||||||
SELECT COALESCE(bcs.override_duration_minutes,cs.duration_minutes)
|
|
||||||
FROM booking_custom_services bcs JOIN custom_services cs ON bcs.custom_service_id=cs.id WHERE bcs.booking_id=bookings.id
|
|
||||||
) sub
|
|
||||||
)) > $1
|
|
||||||
`, req.StartTime, endTime).Scan(&cnt); err != nil {
|
`, req.StartTime, endTime).Scan(&cnt); err != nil {
|
||||||
log.Printf("Failed to check overlap: %v", err)
|
log.Printf("Failed to check overlap: %v", err)
|
||||||
http.Error(w, "Internal server error", http.StatusInternalServerError)
|
http.Error(w, "Internal server error", http.StatusInternalServerError)
|
||||||
|
|||||||
@@ -428,23 +428,11 @@ func GetAllUserBookingsHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
dataQuery := `
|
dataQuery := `
|
||||||
SELECT
|
SELECT
|
||||||
b.id, b.start_time, b.status, b.notes, b.created_at, b.updated_at, b.created_by,
|
b.id, b.start_time, b.status, b.notes, b.created_at, b.updated_at, b.created_by,
|
||||||
(SELECT COALESCE(SUM(price_val), 0) FROM (
|
(SELECT total_amount FROM bookings WHERE id = b.id) AS total_amount,
|
||||||
SELECT COALESCE(bs.override_price, s.price) AS price_val
|
|
||||||
FROM booking_services bs JOIN services s ON bs.service_id = s.id WHERE bs.booking_id = b.id
|
|
||||||
UNION ALL
|
|
||||||
SELECT COALESCE(bcs.override_price, cs.price)
|
|
||||||
FROM booking_custom_services bcs JOIN custom_services cs ON bcs.custom_service_id = cs.id WHERE bcs.booking_id = b.id
|
|
||||||
) sub) AS total_amount,
|
|
||||||
(SELECT COALESCE(SUM(amount), 0)
|
(SELECT COALESCE(SUM(amount), 0)
|
||||||
FROM payments
|
FROM payments
|
||||||
WHERE booking_id = b.id AND status = 'completed') AS amount_paid,
|
WHERE booking_id = b.id AND status = 'completed') AS amount_paid,
|
||||||
(SELECT COALESCE(SUM(dur_val), 0) FROM (
|
(SELECT total_duration_minutes FROM bookings WHERE id = b.id) AS duration_minutes,
|
||||||
SELECT COALESCE(bs.override_duration_minutes, s.duration_minutes) AS dur_val
|
|
||||||
FROM booking_services bs JOIN services s ON bs.service_id = s.id WHERE bs.booking_id = b.id
|
|
||||||
UNION ALL
|
|
||||||
SELECT COALESCE(bcs.override_duration_minutes, cs.duration_minutes)
|
|
||||||
FROM booking_custom_services bcs JOIN custom_services cs ON bcs.custom_service_id = cs.id WHERE bcs.booking_id = b.id
|
|
||||||
) sub) AS duration_minutes,
|
|
||||||
b.deposit_required,
|
b.deposit_required,
|
||||||
(SELECT COALESCE(SUM(amount), 0)
|
(SELECT COALESCE(SUM(amount), 0)
|
||||||
FROM payments
|
FROM payments
|
||||||
@@ -1440,15 +1428,7 @@ func UpdateBookingServicesHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
WHERE id != $1
|
WHERE id != $1
|
||||||
AND status IN ('confirmed', 'pending', 'in_progress', 'completed')
|
AND status IN ('confirmed', 'pending', 'in_progress', 'completed')
|
||||||
AND start_time < $3
|
AND start_time < $3
|
||||||
AND start_time + (INTERVAL '1 minute' * (
|
AND end_time > $2
|
||||||
SELECT COALESCE(SUM(dur), 60) FROM (
|
|
||||||
SELECT COALESCE(bs.override_duration_minutes, s.duration_minutes) AS dur
|
|
||||||
FROM booking_services bs JOIN services s ON bs.service_id = s.id WHERE bs.booking_id = bookings.id
|
|
||||||
UNION ALL
|
|
||||||
SELECT COALESCE(bcs.override_duration_minutes, cs.duration_minutes)
|
|
||||||
FROM booking_custom_services bcs JOIN custom_services cs ON bcs.custom_service_id = cs.id WHERE bcs.booking_id = bookings.id
|
|
||||||
) sub
|
|
||||||
)) > $2
|
|
||||||
`, bookingID, startTime, newEndTime).Scan(&overlapCount)
|
`, bookingID, startTime, newEndTime).Scan(&overlapCount)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("Failed to check overlap: %v", err)
|
log.Printf("Failed to check overlap: %v", err)
|
||||||
@@ -2176,7 +2156,7 @@ func CreateBookingHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
// DB uses 0=Monday..6=Sunday; Go uses 0=Sunday..6=Saturday. Convert.
|
// DB uses 0=Monday..6=Sunday; Go uses 0=Sunday..6=Saturday. Convert.
|
||||||
weekday := int((localStart.Weekday() + 6) % 7)
|
weekday := int((localStart.Weekday() + 6) % 7)
|
||||||
var closeStr string
|
var closeStr string
|
||||||
if err := db.Conn.QueryRow(r.Context(), `SELECT end_time::text FROM working_hours WHERE weekday = $1`, weekday).Scan(&closeStr); err != nil {
|
if err := db.Conn.QueryRow(r.Context(), `SELECT end_time FROM working_hours WHERE weekday = $1`, weekday).Scan(&closeStr); err != nil {
|
||||||
log.Printf("Failed to get hours: %v", err)
|
log.Printf("Failed to get hours: %v", err)
|
||||||
http.Error(w, "Could not verify hours", http.StatusInternalServerError)
|
http.Error(w, "Could not verify hours", http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
@@ -2209,15 +2189,7 @@ func CreateBookingHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
if err := tx.QueryRow(r.Context(), `
|
if err := tx.QueryRow(r.Context(), `
|
||||||
SELECT COUNT(*) FROM bookings WHERE status IN ('pending','confirmed','in_progress','completed')
|
SELECT COUNT(*) FROM bookings WHERE status IN ('pending','confirmed','in_progress','completed')
|
||||||
AND start_time < $2
|
AND start_time < $2
|
||||||
AND start_time + (INTERVAL '1 minute' * (
|
AND end_time > $1
|
||||||
SELECT COALESCE(SUM(dur), 60) FROM (
|
|
||||||
SELECT COALESCE(bs.override_duration_minutes,s.duration_minutes) AS dur
|
|
||||||
FROM booking_services bs JOIN services s ON bs.service_id=s.id WHERE bs.booking_id=bookings.id
|
|
||||||
UNION ALL
|
|
||||||
SELECT COALESCE(bcs.override_duration_minutes,cs.duration_minutes)
|
|
||||||
FROM booking_custom_services bcs JOIN custom_services cs ON bcs.custom_service_id=cs.id WHERE bcs.booking_id=bookings.id
|
|
||||||
) sub
|
|
||||||
)) > $1
|
|
||||||
`, req.StartTime, endTime).Scan(&cnt); err != nil {
|
`, req.StartTime, endTime).Scan(&cnt); err != nil {
|
||||||
log.Printf("Failed to check slot overlap: %v", err)
|
log.Printf("Failed to check slot overlap: %v", err)
|
||||||
http.Error(w, "Internal server error", http.StatusInternalServerError)
|
http.Error(w, "Internal server error", http.StatusInternalServerError)
|
||||||
@@ -2406,17 +2378,7 @@ func EditBookingHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
var durationMinutes int
|
var durationMinutes int
|
||||||
if err := db.Conn.QueryRow(r.Context(), `
|
if err := db.Conn.QueryRow(r.Context(), `
|
||||||
SELECT COALESCE(SUM(dur), 60) FROM (
|
SELECT total_duration_minutes FROM bookings WHERE id = $1
|
||||||
SELECT COALESCE(bs.override_duration_minutes, s.duration_minutes) AS dur
|
|
||||||
FROM booking_services bs
|
|
||||||
JOIN services s ON bs.service_id = s.id
|
|
||||||
WHERE bs.booking_id = $1
|
|
||||||
UNION ALL
|
|
||||||
SELECT COALESCE(bcs.override_duration_minutes, cs.duration_minutes)
|
|
||||||
FROM booking_custom_services bcs
|
|
||||||
JOIN custom_services cs ON bcs.custom_service_id = cs.id
|
|
||||||
WHERE bcs.booking_id = $1
|
|
||||||
) sub
|
|
||||||
`, bookingID).Scan(&durationMinutes); err != nil {
|
`, bookingID).Scan(&durationMinutes); err != nil {
|
||||||
log.Printf("Failed to get booking duration %s: %v", bookingID, err)
|
log.Printf("Failed to get booking duration %s: %v", bookingID, err)
|
||||||
durationMinutes = 60
|
durationMinutes = 60
|
||||||
@@ -2445,15 +2407,7 @@ func EditBookingHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
WHERE id != $1
|
WHERE id != $1
|
||||||
AND status NOT IN ('completed', 'client_cancelled', 'we_cancelled', 'no_show', 'deposit_lapsed')
|
AND status NOT IN ('completed', 'client_cancelled', 'we_cancelled', 'no_show', 'deposit_lapsed')
|
||||||
AND start_time < $3
|
AND start_time < $3
|
||||||
AND start_time + (INTERVAL '1 minute' * (
|
AND end_time > $2
|
||||||
SELECT COALESCE(SUM(dur), 60) FROM (
|
|
||||||
SELECT COALESCE(bs.override_duration_minutes, s.duration_minutes) AS dur
|
|
||||||
FROM booking_services bs JOIN services s ON bs.service_id = s.id WHERE bs.booking_id = bookings.id
|
|
||||||
UNION ALL
|
|
||||||
SELECT COALESCE(bcs.override_duration_minutes, cs.duration_minutes)
|
|
||||||
FROM booking_custom_services bcs JOIN custom_services cs ON bcs.custom_service_id = cs.id WHERE bcs.booking_id = bookings.id
|
|
||||||
) sub
|
|
||||||
)) > $2
|
|
||||||
`, bookingID, req.StartTime, newEndTime).Scan(&overlapCount); err != nil {
|
`, bookingID, req.StartTime, newEndTime).Scan(&overlapCount); err != nil {
|
||||||
log.Printf("Failed to check overlap %s: %v", bookingID, err)
|
log.Printf("Failed to check overlap %s: %v", bookingID, err)
|
||||||
http.Error(w, "Internal server error", http.StatusInternalServerError)
|
http.Error(w, "Internal server error", http.StatusInternalServerError)
|
||||||
@@ -2631,17 +2585,7 @@ func ProgressBookingHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
var bookingTotal float64
|
var bookingTotal float64
|
||||||
if err := tx.QueryRow(r.Context(), `
|
if err := tx.QueryRow(r.Context(), `
|
||||||
SELECT COALESCE(SUM(price_val), 0) FROM (
|
SELECT total_amount FROM bookings WHERE id = $1
|
||||||
SELECT COALESCE(bs.override_price, s.price) AS price_val
|
|
||||||
FROM booking_services bs
|
|
||||||
JOIN services s ON bs.service_id = s.id
|
|
||||||
WHERE bs.booking_id = $1
|
|
||||||
UNION ALL
|
|
||||||
SELECT COALESCE(bcs.override_price, cs.price)
|
|
||||||
FROM booking_custom_services bcs
|
|
||||||
JOIN custom_services cs ON bcs.custom_service_id = cs.id
|
|
||||||
WHERE bcs.booking_id = $1
|
|
||||||
) sub
|
|
||||||
`, bookingID).Scan(&bookingTotal); err != nil {
|
`, bookingID).Scan(&bookingTotal); err != nil {
|
||||||
log.Printf("Failed to calculate booking total for %s: %v", bookingID, err)
|
log.Printf("Failed to calculate booking total for %s: %v", bookingID, err)
|
||||||
}
|
}
|
||||||
@@ -2955,13 +2899,7 @@ func ConfirmBookingHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
if err := db.Conn.QueryRow(r.Context(), `
|
if err := db.Conn.QueryRow(r.Context(), `
|
||||||
SELECT COALESCE(SUM(dur), 60) FROM (
|
SELECT total_duration_minutes FROM bookings WHERE id = $1
|
||||||
SELECT COALESCE(bs.override_duration_minutes, s.duration_minutes) AS dur
|
|
||||||
FROM booking_services bs JOIN services s ON bs.service_id = s.id WHERE bs.booking_id = $1
|
|
||||||
UNION ALL
|
|
||||||
SELECT COALESCE(bcs.override_duration_minutes, cs.duration_minutes)
|
|
||||||
FROM booking_custom_services bcs JOIN custom_services cs ON bcs.custom_service_id = cs.id WHERE bcs.booking_id = $1
|
|
||||||
) sub
|
|
||||||
`, bookingID).Scan(&dur); err != nil {
|
`, bookingID).Scan(&dur); err != nil {
|
||||||
log.Printf("Failed to calculate duration on confirm: %v", err)
|
log.Printf("Failed to calculate duration on confirm: %v", err)
|
||||||
http.Error(w, "Internal server error", http.StatusInternalServerError)
|
http.Error(w, "Internal server error", http.StatusInternalServerError)
|
||||||
@@ -2987,15 +2925,7 @@ func ConfirmBookingHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
if err := tx.QueryRow(r.Context(), `
|
if err := tx.QueryRow(r.Context(), `
|
||||||
SELECT COUNT(*) FROM bookings WHERE id != $1 AND status IN ('pending','confirmed','in_progress','completed')
|
SELECT COUNT(*) FROM bookings WHERE id != $1 AND status IN ('pending','confirmed','in_progress','completed')
|
||||||
AND start_time < $3
|
AND start_time < $3
|
||||||
AND start_time + (INTERVAL '1 minute' * (
|
AND end_time > $2
|
||||||
SELECT COALESCE(SUM(dur), 60) FROM (
|
|
||||||
SELECT COALESCE(bs.override_duration_minutes,s.duration_minutes) AS dur
|
|
||||||
FROM booking_services bs JOIN services s ON bs.service_id=s.id WHERE bs.booking_id=bookings.id
|
|
||||||
UNION ALL
|
|
||||||
SELECT COALESCE(bcs.override_duration_minutes,cs.duration_minutes)
|
|
||||||
FROM booking_custom_services bcs JOIN custom_services cs ON bcs.custom_service_id=cs.id WHERE bcs.booking_id=bookings.id
|
|
||||||
) sub
|
|
||||||
)) > $2
|
|
||||||
`, bookingID, bkStart, endTime).Scan(&cnt); err != nil {
|
`, bookingID, bkStart, endTime).Scan(&cnt); err != nil {
|
||||||
log.Printf("Failed to check overlap on confirm: %v", err)
|
log.Printf("Failed to check overlap on confirm: %v", err)
|
||||||
http.Error(w, "Internal server error", http.StatusInternalServerError)
|
http.Error(w, "Internal server error", http.StatusInternalServerError)
|
||||||
@@ -3100,17 +3030,7 @@ func ConfirmBookingHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
if dav.Service != nil {
|
if dav.Service != nil {
|
||||||
var durationMinutes int
|
var durationMinutes int
|
||||||
db.Conn.QueryRow(r.Context(), `
|
db.Conn.QueryRow(r.Context(), `
|
||||||
SELECT COALESCE(SUM(dur), 60) FROM (
|
SELECT total_duration_minutes FROM bookings WHERE id = $1
|
||||||
SELECT COALESCE(bs.override_duration_minutes, s.duration_minutes) AS dur
|
|
||||||
FROM booking_services bs
|
|
||||||
JOIN services s ON bs.service_id = s.id
|
|
||||||
WHERE bs.booking_id = $1
|
|
||||||
UNION ALL
|
|
||||||
SELECT COALESCE(bcs.override_duration_minutes, cs.duration_minutes)
|
|
||||||
FROM booking_custom_services bcs
|
|
||||||
JOIN custom_services cs ON bcs.custom_service_id = cs.id
|
|
||||||
WHERE bcs.booking_id = $1
|
|
||||||
) sub
|
|
||||||
`, bookingID).Scan(&durationMinutes)
|
`, bookingID).Scan(&durationMinutes)
|
||||||
if durationMinutes == 0 {
|
if durationMinutes == 0 {
|
||||||
durationMinutes = 60
|
durationMinutes = 60
|
||||||
@@ -3547,15 +3467,7 @@ func GetBookingCalendarHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
if err := db.Conn.QueryRow(r.Context(), `
|
if err := db.Conn.QueryRow(r.Context(), `
|
||||||
SELECT id, user_id, start_time, status, COALESCE(notes, ''), COALESCE(created_by, ''), created_at, updated_at,
|
SELECT id, user_id, start_time, status, COALESCE(notes, ''), COALESCE(created_by, ''), created_at, updated_at,
|
||||||
COALESCE((SELECT COALESCE(SUM(dur), 60) FROM (
|
total_duration_minutes
|
||||||
SELECT COALESCE(bs.override_duration_minutes, s.duration_minutes) AS dur
|
|
||||||
FROM booking_services bs JOIN services s ON bs.service_id = s.id
|
|
||||||
WHERE bs.booking_id = bookings.id
|
|
||||||
UNION ALL
|
|
||||||
SELECT COALESCE(bcs.override_duration_minutes, cs.duration_minutes)
|
|
||||||
FROM booking_custom_services bcs JOIN custom_services cs ON bcs.custom_service_id = cs.id
|
|
||||||
WHERE bcs.booking_id = bookings.id
|
|
||||||
) sub), 60)
|
|
||||||
FROM bookings
|
FROM bookings
|
||||||
WHERE id = $1 AND user_id = $2
|
WHERE id = $1 AND user_id = $2
|
||||||
`, bookingID, userID).Scan(&bookingIDDB, &userIDDB, &startTime, &status, ¬es, &createdBy, &createdAt, &updatedAt, &durationMinutes); err != nil {
|
`, bookingID, userID).Scan(&bookingIDDB, &userIDDB, &startTime, &status, ¬es, &createdBy, &createdAt, &updatedAt, &durationMinutes); err != nil {
|
||||||
@@ -3697,13 +3609,7 @@ func GetOverlappingBookingsByTimeHandler(w http.ResponseWriter, r *http.Request)
|
|||||||
b.start_time,
|
b.start_time,
|
||||||
b.status,
|
b.status,
|
||||||
b.created_at,
|
b.created_at,
|
||||||
(SELECT COALESCE(SUM(dur_val), 60) FROM (
|
b.total_duration_minutes as duration,
|
||||||
SELECT COALESCE(bs.override_duration_minutes, s.duration_minutes) AS dur_val
|
|
||||||
FROM booking_services bs JOIN services s ON bs.service_id = s.id WHERE bs.booking_id = b.id
|
|
||||||
UNION ALL
|
|
||||||
SELECT COALESCE(bcs.override_duration_minutes, cs.duration_minutes)
|
|
||||||
FROM booking_custom_services bcs JOIN custom_services cs ON bcs.custom_service_id = cs.id WHERE bcs.booking_id = b.id
|
|
||||||
) sub) as duration,
|
|
||||||
u.id as user_id,
|
u.id as user_id,
|
||||||
u.fn,
|
u.fn,
|
||||||
u.email,
|
u.email,
|
||||||
@@ -3712,15 +3618,7 @@ func GetOverlappingBookingsByTimeHandler(w http.ResponseWriter, r *http.Request)
|
|||||||
LEFT JOIN users u ON b.user_id = u.id
|
LEFT JOIN users u ON b.user_id = u.id
|
||||||
WHERE b.status NOT IN ('completed', 'client_cancelled', 'we_cancelled', 'no_show', 'deposit_lapsed')
|
WHERE b.status NOT IN ('completed', 'client_cancelled', 'we_cancelled', 'no_show', 'deposit_lapsed')
|
||||||
AND b.start_time < $2
|
AND b.start_time < $2
|
||||||
AND b.start_time + (INTERVAL '1 minute' * (
|
AND b.end_time > $1
|
||||||
SELECT COALESCE(SUM(dur_val), 60) FROM (
|
|
||||||
SELECT COALESCE(bs2.override_duration_minutes, s2.duration_minutes) AS dur_val
|
|
||||||
FROM booking_services bs2 JOIN services s2 ON bs2.service_id = s2.id WHERE bs2.booking_id = b.id
|
|
||||||
UNION ALL
|
|
||||||
SELECT COALESCE(bcs2.override_duration_minutes, cs2.duration_minutes)
|
|
||||||
FROM booking_custom_services bcs2 JOIN custom_services cs2 ON bcs2.custom_service_id = cs2.id WHERE bcs2.booking_id = b.id
|
|
||||||
) sub
|
|
||||||
)) > $1
|
|
||||||
GROUP BY b.id, b.start_time, b.status, b.created_at, u.id, u.fn, u.email, u.phone
|
GROUP BY b.id, b.start_time, b.status, b.created_at, u.id, u.fn, u.email, u.phone
|
||||||
ORDER BY b.start_time ASC
|
ORDER BY b.start_time ASC
|
||||||
`, startTime, endTime)
|
`, startTime, endTime)
|
||||||
@@ -3790,14 +3688,7 @@ func GetOverlappingBookingsHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
var startTime time.Time
|
var startTime time.Time
|
||||||
var durationMinutes int
|
var durationMinutes int
|
||||||
if err := db.Conn.QueryRow(r.Context(), `
|
if err := db.Conn.QueryRow(r.Context(), `
|
||||||
SELECT b.start_time,
|
SELECT b.start_time, b.total_duration_minutes
|
||||||
(SELECT COALESCE(SUM(dur_val), 60) FROM (
|
|
||||||
SELECT COALESCE(bs.override_duration_minutes, s.duration_minutes) AS dur_val
|
|
||||||
FROM booking_services bs JOIN services s ON bs.service_id = s.id WHERE bs.booking_id = b.id
|
|
||||||
UNION ALL
|
|
||||||
SELECT COALESCE(bcs.override_duration_minutes, cs.duration_minutes)
|
|
||||||
FROM booking_custom_services bcs JOIN custom_services cs ON bcs.custom_service_id = cs.id WHERE bcs.booking_id = b.id
|
|
||||||
) sub)
|
|
||||||
FROM bookings b
|
FROM bookings b
|
||||||
WHERE b.id = $1
|
WHERE b.id = $1
|
||||||
`, bookingID).Scan(&startTime, &durationMinutes); err != nil {
|
`, bookingID).Scan(&startTime, &durationMinutes); err != nil {
|
||||||
@@ -3819,13 +3710,7 @@ func GetOverlappingBookingsHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
b.start_time,
|
b.start_time,
|
||||||
b.status,
|
b.status,
|
||||||
b.created_at,
|
b.created_at,
|
||||||
(SELECT COALESCE(SUM(dur_val), 60) FROM (
|
b.total_duration_minutes as duration,
|
||||||
SELECT COALESCE(bs.override_duration_minutes, s.duration_minutes) AS dur_val
|
|
||||||
FROM booking_services bs JOIN services s ON bs.service_id = s.id WHERE bs.booking_id = b.id
|
|
||||||
UNION ALL
|
|
||||||
SELECT COALESCE(bcs.override_duration_minutes, cs.duration_minutes)
|
|
||||||
FROM booking_custom_services bcs JOIN custom_services cs ON bcs.custom_service_id = cs.id WHERE bcs.booking_id = b.id
|
|
||||||
) sub) as duration,
|
|
||||||
u.fn,
|
u.fn,
|
||||||
u.email
|
u.email
|
||||||
FROM bookings b
|
FROM bookings b
|
||||||
@@ -3833,15 +3718,7 @@ func GetOverlappingBookingsHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
WHERE b.id != $1
|
WHERE b.id != $1
|
||||||
AND b.status NOT IN ('completed', 'client_cancelled', 'we_cancelled', 'no_show', 'deposit_lapsed')
|
AND b.status NOT IN ('completed', 'client_cancelled', 'we_cancelled', 'no_show', 'deposit_lapsed')
|
||||||
AND b.start_time < $3
|
AND b.start_time < $3
|
||||||
AND b.start_time + (INTERVAL '1 minute' * (
|
AND b.end_time > $2
|
||||||
SELECT COALESCE(SUM(dur_val), 60) FROM (
|
|
||||||
SELECT COALESCE(bs2.override_duration_minutes, s2.duration_minutes) AS dur_val
|
|
||||||
FROM booking_services bs2 JOIN services s2 ON bs2.service_id = s2.id WHERE bs2.booking_id = b.id
|
|
||||||
UNION ALL
|
|
||||||
SELECT COALESCE(bcs2.override_duration_minutes, cs2.duration_minutes)
|
|
||||||
FROM booking_custom_services bcs2 JOIN custom_services cs2 ON bcs2.custom_service_id = cs2.id WHERE bcs2.booking_id = b.id
|
|
||||||
) sub
|
|
||||||
)) > $2
|
|
||||||
GROUP BY b.id, b.start_time, b.status, b.created_at, u.fn, u.email
|
GROUP BY b.id, b.start_time, b.status, b.created_at, u.fn, u.email
|
||||||
ORDER BY b.created_at ASC
|
ORDER BY b.created_at ASC
|
||||||
`, bookingID, startTime, endTime)
|
`, bookingID, startTime, endTime)
|
||||||
@@ -3928,13 +3805,7 @@ func GetBookingsByDateRangeHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
b.status,
|
b.status,
|
||||||
b.notes,
|
b.notes,
|
||||||
b.created_at,
|
b.created_at,
|
||||||
(SELECT COALESCE(SUM(dur_val), 60) FROM (
|
b.total_duration_minutes as duration,
|
||||||
SELECT COALESCE(bs.override_duration_minutes, s.duration_minutes) AS dur_val
|
|
||||||
FROM booking_services bs JOIN services s ON bs.service_id = s.id WHERE bs.booking_id = b.id
|
|
||||||
UNION ALL
|
|
||||||
SELECT COALESCE(bcs.override_duration_minutes, cs.duration_minutes)
|
|
||||||
FROM booking_custom_services bcs JOIN custom_services cs ON bcs.custom_service_id = cs.id WHERE bcs.booking_id = b.id
|
|
||||||
) sub) as duration,
|
|
||||||
u.id as user_id,
|
u.id as user_id,
|
||||||
u.fn,
|
u.fn,
|
||||||
u.email,
|
u.email,
|
||||||
@@ -4035,13 +3906,7 @@ func GetBookingsByCreatedRangeHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
b.status,
|
b.status,
|
||||||
b.notes,
|
b.notes,
|
||||||
b.created_at,
|
b.created_at,
|
||||||
(SELECT COALESCE(SUM(dur_val), 60) FROM (
|
b.total_duration_minutes as duration,
|
||||||
SELECT COALESCE(bs.override_duration_minutes, s.duration_minutes) AS dur_val
|
|
||||||
FROM booking_services bs JOIN services s ON bs.service_id = s.id WHERE bs.booking_id = b.id
|
|
||||||
UNION ALL
|
|
||||||
SELECT COALESCE(bcs.override_duration_minutes, cs.duration_minutes)
|
|
||||||
FROM booking_custom_services bcs JOIN custom_services cs ON bcs.custom_service_id = cs.id WHERE bcs.booking_id = b.id
|
|
||||||
) sub) as duration,
|
|
||||||
u.id as user_id,
|
u.id as user_id,
|
||||||
u.fn,
|
u.fn,
|
||||||
u.email,
|
u.email,
|
||||||
@@ -4190,17 +4055,7 @@ func AdminRescheduleBookingHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
var durationMinutes int
|
var durationMinutes int
|
||||||
if err := db.Conn.QueryRow(r.Context(), `
|
if err := db.Conn.QueryRow(r.Context(), `
|
||||||
SELECT COALESCE(SUM(dur), 60) FROM (
|
SELECT total_duration_minutes FROM bookings WHERE id = $1
|
||||||
SELECT COALESCE(bs.override_duration_minutes, s.duration_minutes) AS dur
|
|
||||||
FROM booking_services bs
|
|
||||||
JOIN services s ON bs.service_id = s.id
|
|
||||||
WHERE bs.booking_id = $1
|
|
||||||
UNION ALL
|
|
||||||
SELECT COALESCE(bcs.override_duration_minutes, cs.duration_minutes)
|
|
||||||
FROM booking_custom_services bcs
|
|
||||||
JOIN custom_services cs ON bcs.custom_service_id = cs.id
|
|
||||||
WHERE bcs.booking_id = $1
|
|
||||||
) sub
|
|
||||||
`, bookingID).Scan(&durationMinutes); err != nil {
|
`, bookingID).Scan(&durationMinutes); err != nil {
|
||||||
log.Printf("Failed to get booking duration %s: %v", bookingID, err)
|
log.Printf("Failed to get booking duration %s: %v", bookingID, err)
|
||||||
durationMinutes = 60
|
durationMinutes = 60
|
||||||
@@ -4257,19 +4112,7 @@ func AdminRescheduleBookingHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
WHERE id != $1
|
WHERE id != $1
|
||||||
AND status NOT IN ('completed', 'client_cancelled', 'we_cancelled', 'no_show', 'deposit_lapsed')
|
AND status NOT IN ('completed', 'client_cancelled', 'we_cancelled', 'no_show', 'deposit_lapsed')
|
||||||
AND start_time < $3
|
AND start_time < $3
|
||||||
AND start_time + (INTERVAL '1 minute' * (
|
AND end_time > $2
|
||||||
SELECT COALESCE(SUM(dur), 60) FROM (
|
|
||||||
SELECT COALESCE(bs2.override_duration_minutes, s2.duration_minutes) AS dur
|
|
||||||
FROM booking_services bs2
|
|
||||||
JOIN services s2 ON bs2.service_id = s2.id
|
|
||||||
WHERE bs2.booking_id = bookings.id
|
|
||||||
UNION ALL
|
|
||||||
SELECT COALESCE(bcs.override_duration_minutes, cs.duration_minutes)
|
|
||||||
FROM booking_custom_services bcs
|
|
||||||
JOIN custom_services cs ON bcs.custom_service_id = cs.id
|
|
||||||
WHERE bcs.booking_id = bookings.id
|
|
||||||
) sub
|
|
||||||
)) > $2
|
|
||||||
`, bookingID, req.StartTime, newEndTime).Scan(&overlapCount); err != nil {
|
`, bookingID, req.StartTime, newEndTime).Scan(&overlapCount); err != nil {
|
||||||
log.Printf("Failed to check overlap %s: %v", bookingID, err)
|
log.Printf("Failed to check overlap %s: %v", bookingID, err)
|
||||||
http.Error(w, "Internal server error", http.StatusInternalServerError)
|
http.Error(w, "Internal server error", http.StatusInternalServerError)
|
||||||
@@ -4330,15 +4173,7 @@ func EvictPendingReleaseOverlapping(ctx context.Context, tx pgx.Tx, startTime, e
|
|||||||
UPDATE bookings SET status = 'deposit_lapsed', updated_at = NOW()
|
UPDATE bookings SET status = 'deposit_lapsed', updated_at = NOW()
|
||||||
WHERE status = 'pending_release'
|
WHERE status = 'pending_release'
|
||||||
AND start_time < $2
|
AND start_time < $2
|
||||||
AND start_time + (INTERVAL '1 minute' * (
|
AND end_time > $1
|
||||||
SELECT COALESCE(SUM(dur), 60) FROM (
|
|
||||||
SELECT COALESCE(bs.override_duration_minutes, s.duration_minutes) AS dur
|
|
||||||
FROM booking_services bs JOIN services s ON bs.service_id = s.id WHERE bs.booking_id = bookings.id
|
|
||||||
UNION ALL
|
|
||||||
SELECT COALESCE(bcs.override_duration_minutes, cs.duration_minutes)
|
|
||||||
FROM booking_custom_services bcs JOIN custom_services cs ON bcs.custom_service_id = cs.id WHERE bcs.booking_id = bookings.id
|
|
||||||
) sub
|
|
||||||
)) > $1
|
|
||||||
AND NOT EXISTS (
|
AND NOT EXISTS (
|
||||||
SELECT 1 FROM time_blockers
|
SELECT 1 FROM time_blockers
|
||||||
WHERE description = 'PAYMENT_IN_FLIGHT:' || bookings.id
|
WHERE description = 'PAYMENT_IN_FLIGHT:' || bookings.id
|
||||||
|
|||||||
@@ -672,7 +672,7 @@ func AdminCreateBookingForUserHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
// Check for overlapping confirmed/in_progress/completed bookings (inside transaction)
|
// Check for overlapping confirmed/in_progress/completed bookings (inside transaction)
|
||||||
var cnt int
|
var cnt int
|
||||||
err = tx.QueryRow(r.Context(), `
|
err = tx.QueryRow(r.Context(), `
|
||||||
SELECT COUNT(*) FROM bookings WHERE status IN ('pending','confirmed','in_progress','completed') AND start_time < $2 AND start_time + (INTERVAL '1 minute' * (SELECT COALESCE(SUM(dur),60) FROM (SELECT COALESCE(bs.override_duration_minutes,s.duration_minutes) AS dur FROM booking_services bs JOIN services s ON bs.service_id=s.id WHERE bs.booking_id=bookings.id UNION ALL SELECT COALESCE(bcs.override_duration_minutes,cs.duration_minutes) FROM booking_custom_services bcs JOIN custom_services cs ON bcs.custom_service_id=cs.id WHERE bcs.booking_id=bookings.id) sub)) > $1
|
SELECT COUNT(*) FROM bookings WHERE status IN ('pending','confirmed','in_progress','completed') AND start_time < $2 AND end_time > $1
|
||||||
`, req.StartTime, newEnd).Scan(&cnt)
|
`, req.StartTime, newEnd).Scan(&cnt)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("Failed to check overlap: %v", err)
|
log.Printf("Failed to check overlap: %v", err)
|
||||||
@@ -755,14 +755,12 @@ func AdminCreateBookingForUserHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
http.Error(w, "Internal server error", http.StatusInternalServerError)
|
http.Error(w, "Internal server error", http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
for _, csID := range req.CustomServiceIDs {
|
|
||||||
if _, err := tx.Exec(r.Context(), `
|
if _, err := tx.Exec(r.Context(), `
|
||||||
UPDATE custom_services SET usage_count = usage_count + 1, last_used_at = NOW() WHERE id = $1
|
UPDATE custom_services SET usage_count = usage_count + 1, last_used_at = NOW() WHERE id = ANY($1)
|
||||||
`, csID); err != nil {
|
`, req.CustomServiceIDs); err != nil {
|
||||||
log.Printf("ALERT: failed to update custom service usage: %v", err)
|
log.Printf("ALERT: failed to update custom service usage: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Apply overrides (optional)
|
// Apply overrides (optional)
|
||||||
if len(req.ServiceOverrides) > 0 {
|
if len(req.ServiceOverrides) > 0 {
|
||||||
@@ -1420,13 +1418,7 @@ func RequestEditHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if err := tx.QueryRow(r.Context(), `
|
if err := tx.QueryRow(r.Context(), `
|
||||||
SELECT COALESCE(SUM(dur), 60) FROM (
|
SELECT total_duration_minutes FROM bookings WHERE id = $1
|
||||||
SELECT COALESCE(bs.override_duration_minutes, s.duration_minutes) AS dur
|
|
||||||
FROM booking_services bs JOIN services s ON bs.service_id = s.id WHERE bs.booking_id = $1
|
|
||||||
UNION ALL
|
|
||||||
SELECT COALESCE(bcs.override_duration_minutes, cs.duration_minutes)
|
|
||||||
FROM booking_custom_services bcs JOIN custom_services cs ON bcs.custom_service_id = cs.id WHERE bcs.booking_id = $1
|
|
||||||
) sub
|
|
||||||
`, bookingID).Scan(&durMinutes); err != nil {
|
`, bookingID).Scan(&durMinutes); err != nil {
|
||||||
log.Printf("Failed to get duration for existing services: %v", err)
|
log.Printf("Failed to get duration for existing services: %v", err)
|
||||||
durMinutes = 60
|
durMinutes = 60
|
||||||
@@ -1444,15 +1436,7 @@ func RequestEditHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
WHERE id != $1
|
WHERE id != $1
|
||||||
AND status NOT IN ('completed','client_cancelled','we_cancelled','no_show','deposit_lapsed')
|
AND status NOT IN ('completed','client_cancelled','we_cancelled','no_show','deposit_lapsed')
|
||||||
AND start_time < $3
|
AND start_time < $3
|
||||||
AND start_time + (INTERVAL '1 minute' * (
|
AND end_time > $2
|
||||||
SELECT COALESCE(SUM(dur), 60) FROM (
|
|
||||||
SELECT COALESCE(bs.override_duration_minutes, s.duration_minutes) AS dur
|
|
||||||
FROM booking_services bs JOIN services s ON bs.service_id = s.id WHERE bs.booking_id = bookings.id
|
|
||||||
UNION ALL
|
|
||||||
SELECT COALESCE(bcs.override_duration_minutes, cs.duration_minutes)
|
|
||||||
FROM booking_custom_services bcs JOIN custom_services cs ON bcs.custom_service_id = cs.id WHERE bcs.booking_id = bookings.id
|
|
||||||
) sub
|
|
||||||
)) > $2
|
|
||||||
`, bookingID, *req.NewStartTime, newEnd).Scan(&overlapCount); err != nil {
|
`, bookingID, *req.NewStartTime, newEnd).Scan(&overlapCount); err != nil {
|
||||||
log.Printf("Failed to check overlap: %v", err)
|
log.Printf("Failed to check overlap: %v", err)
|
||||||
http.Error(w, "Internal server error", http.StatusInternalServerError)
|
http.Error(w, "Internal server error", http.StatusInternalServerError)
|
||||||
@@ -1545,17 +1529,7 @@ func RequestEditHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
`, req.NewServices).Scan(&durationMinutes)
|
`, req.NewServices).Scan(&durationMinutes)
|
||||||
} else {
|
} else {
|
||||||
_ = tx.QueryRow(r.Context(), `
|
_ = tx.QueryRow(r.Context(), `
|
||||||
SELECT COALESCE(SUM(dur), 60) FROM (
|
SELECT total_duration_minutes FROM bookings WHERE id = $1
|
||||||
SELECT COALESCE(bs.override_duration_minutes, s.duration_minutes) AS dur
|
|
||||||
FROM booking_services bs
|
|
||||||
JOIN services s ON bs.service_id = s.id
|
|
||||||
WHERE bs.booking_id = $1
|
|
||||||
UNION ALL
|
|
||||||
SELECT COALESCE(bcs.override_duration_minutes, cs.duration_minutes)
|
|
||||||
FROM booking_custom_services bcs
|
|
||||||
JOIN custom_services cs ON bcs.custom_service_id = cs.id
|
|
||||||
WHERE bcs.booking_id = $1
|
|
||||||
) sub
|
|
||||||
`, bookingID).Scan(&durationMinutes)
|
`, bookingID).Scan(&durationMinutes)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1761,13 +1735,7 @@ func AdminApproveEditRequestHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
if hasOverrides {
|
if hasOverrides {
|
||||||
// Use the existing booking_services with overrides
|
// Use the existing booking_services with overrides
|
||||||
err = tx.QueryRow(r.Context(), `
|
err = tx.QueryRow(r.Context(), `
|
||||||
SELECT COALESCE(SUM(dur), 60) FROM (
|
SELECT total_duration_minutes FROM bookings WHERE id = $1
|
||||||
SELECT COALESCE(bs.override_duration_minutes, s.duration_minutes) AS dur
|
|
||||||
FROM booking_services bs JOIN services s ON bs.service_id = s.id WHERE bs.booking_id = $1
|
|
||||||
UNION ALL
|
|
||||||
SELECT COALESCE(bcs.override_duration_minutes, cs.duration_minutes)
|
|
||||||
FROM booking_custom_services bcs JOIN custom_services cs ON bcs.custom_service_id = cs.id WHERE bcs.booking_id = $1
|
|
||||||
) sub
|
|
||||||
`, bookingID).Scan(&durationMinutes)
|
`, bookingID).Scan(&durationMinutes)
|
||||||
} else {
|
} else {
|
||||||
// Use standard durations or new_services if provided
|
// Use standard durations or new_services if provided
|
||||||
@@ -1805,15 +1773,7 @@ func AdminApproveEditRequestHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
WHERE id != $1
|
WHERE id != $1
|
||||||
AND status NOT IN ('completed', 'client_cancelled', 'we_cancelled', 'no_show', 'deposit_lapsed')
|
AND status NOT IN ('completed', 'client_cancelled', 'we_cancelled', 'no_show', 'deposit_lapsed')
|
||||||
AND start_time < $3
|
AND start_time < $3
|
||||||
AND start_time + (INTERVAL '1 minute' * (
|
AND end_time > $2
|
||||||
SELECT COALESCE(SUM(dur), 60) FROM (
|
|
||||||
SELECT COALESCE(bs.override_duration_minutes, s.duration_minutes) AS dur
|
|
||||||
FROM booking_services bs JOIN services s ON bs.service_id = s.id WHERE bs.booking_id = bookings.id
|
|
||||||
UNION ALL
|
|
||||||
SELECT COALESCE(bcs.override_duration_minutes, cs.duration_minutes)
|
|
||||||
FROM booking_custom_services bcs JOIN custom_services cs ON bcs.custom_service_id = cs.id WHERE bcs.booking_id = bookings.id
|
|
||||||
) sub
|
|
||||||
)) > $2
|
|
||||||
`, bookingID, *newStartTime, newEndTime).Scan(&overlapCount)
|
`, bookingID, *newStartTime, newEndTime).Scan(&overlapCount)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("Failed to check overlap: %v", err)
|
log.Printf("Failed to check overlap: %v", err)
|
||||||
|
|||||||
@@ -113,7 +113,7 @@ func ReserveSlotHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
localStart := req.StartTime.In(londonLocation)
|
localStart := req.StartTime.In(londonLocation)
|
||||||
weekday := int((localStart.Weekday() + 6) % 7)
|
weekday := int((localStart.Weekday() + 6) % 7)
|
||||||
var closeStr string
|
var closeStr string
|
||||||
if err := db.Conn.QueryRow(r.Context(), `SELECT end_time::text FROM working_hours WHERE weekday = $1`, weekday).Scan(&closeStr); err != nil {
|
if err := db.Conn.QueryRow(r.Context(), `SELECT end_time FROM working_hours WHERE weekday = $1`, weekday).Scan(&closeStr); err != nil {
|
||||||
log.Printf("Failed to get hours: %v", err)
|
log.Printf("Failed to get hours: %v", err)
|
||||||
http.Error(w, "Could not verify hours", http.StatusInternalServerError)
|
http.Error(w, "Could not verify hours", http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
@@ -132,15 +132,7 @@ func ReserveSlotHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
if err := db.Conn.QueryRow(r.Context(), `
|
if err := db.Conn.QueryRow(r.Context(), `
|
||||||
SELECT COUNT(*) FROM bookings WHERE status IN ('pending','confirmed','in_progress','completed')
|
SELECT COUNT(*) FROM bookings WHERE status IN ('pending','confirmed','in_progress','completed')
|
||||||
AND start_time < $2
|
AND start_time < $2
|
||||||
AND start_time + (INTERVAL '1 minute' * (
|
AND end_time > $1
|
||||||
SELECT COALESCE(SUM(dur),60) FROM (
|
|
||||||
SELECT COALESCE(bs.override_duration_minutes,s.duration_minutes) AS dur
|
|
||||||
FROM booking_services bs JOIN services s ON bs.service_id=s.id WHERE bs.booking_id=bookings.id
|
|
||||||
UNION ALL
|
|
||||||
SELECT COALESCE(bcs.override_duration_minutes,cs.duration_minutes)
|
|
||||||
FROM booking_custom_services bcs JOIN custom_services cs ON bcs.custom_service_id=cs.id WHERE bcs.booking_id=bookings.id
|
|
||||||
) sub
|
|
||||||
)) > $1
|
|
||||||
`, req.StartTime, endTime).Scan(&cnt); err != nil {
|
`, req.StartTime, endTime).Scan(&cnt); err != nil {
|
||||||
log.Printf("Failed to check overlap: %v", err)
|
log.Printf("Failed to check overlap: %v", err)
|
||||||
http.Error(w, "Internal server error", http.StatusInternalServerError)
|
http.Error(w, "Internal server error", http.StatusInternalServerError)
|
||||||
|
|||||||
Reference in New Issue
Block a user