feat(backend): integrate custom services across booking, payment, scheduling, today, and user modules
Update booking create/confirm/progress/reserve handlers to support custom_service_ids and custom overrides. Add UNION ALL queries to include custom_services in booking detail, payment summary, scheduling availability, today dashboard, and customer relationship queries. Ultraworked with Sisyphus (https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
@@ -51,7 +51,7 @@ type Booking struct {
|
||||
|
||||
// Joined fields
|
||||
User *UserSummary `json:"user,omitempty"`
|
||||
Services []BookingService `json:"services,omitempty"`
|
||||
Services []BookingService `json:"services"`
|
||||
Payments []Payment `json:"payments,omitempty"`
|
||||
Discounts []BookingDiscount `json:"discounts,omitempty"`
|
||||
TotalAmount float64 `json:"total_amount"`
|
||||
@@ -195,8 +195,9 @@ type ProgressBookingRequest struct {
|
||||
|
||||
// ConfirmBookingRequest represents the request payload for confirming a booking
|
||||
type ConfirmBookingRequest struct {
|
||||
ServiceOverrides []ServiceOverride `json:"service_overrides,omitempty"`
|
||||
Notes *string `json:"notes,omitempty" validate:"omitempty,max=1000000"`
|
||||
ServiceOverrides []ServiceOverride `json:"service_overrides,omitempty"`
|
||||
CustomServiceOverrides []ServiceOverride `json:"custom_service_overrides,omitempty"`
|
||||
Notes *string `json:"notes,omitempty" validate:"omitempty,max=1000000"`
|
||||
}
|
||||
|
||||
// ServiceOverride represents override values for a specific service in a booking
|
||||
@@ -209,6 +210,7 @@ type ServiceOverride struct {
|
||||
// UpdateBookingServicesRequest represents the request payload for admin updating a booking's services and notes
|
||||
type UpdateBookingServicesRequest struct {
|
||||
ServiceIDs []string `json:"service_ids"`
|
||||
CustomServiceIDs []string `json:"custom_service_ids,omitempty"`
|
||||
ServiceOverrides []ServiceOverride `json:"service_overrides,omitempty"`
|
||||
Notes *string `json:"notes,omitempty" validate:"omitempty,max=1000000"`
|
||||
}
|
||||
@@ -367,23 +369,23 @@ func GetAllUserBookingsHandler(w http.ResponseWriter, r *http.Request) {
|
||||
baseQuery := `
|
||||
SELECT
|
||||
b.id, b.start_time, b.status, b.notes, b.created_at, b.updated_at, b.created_by,
|
||||
(SELECT COALESCE(SUM(CASE
|
||||
WHEN bs.override_price IS NOT NULL THEN bs.override_price
|
||||
ELSE s.price
|
||||
END), 0)
|
||||
FROM booking_services bs
|
||||
JOIN services s ON bs.service_id = s.id
|
||||
WHERE bs.booking_id = b.id) AS total_amount,
|
||||
(SELECT COALESCE(SUM(price_val), 0) FROM (
|
||||
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)
|
||||
FROM payments
|
||||
WHERE booking_id = b.id AND status = 'completed') AS amount_paid,
|
||||
(SELECT COALESCE(SUM(CASE
|
||||
WHEN bs.override_duration_minutes IS NOT NULL THEN bs.override_duration_minutes
|
||||
ELSE s.duration_minutes
|
||||
END), 0)
|
||||
FROM booking_services bs
|
||||
JOIN services s ON bs.service_id = s.id
|
||||
WHERE bs.booking_id = b.id) AS duration_minutes,
|
||||
(SELECT COALESCE(SUM(dur_val), 0) 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) AS duration_minutes,
|
||||
b.deposit_required,
|
||||
(SELECT COALESCE(SUM(amount), 0)
|
||||
FROM payments
|
||||
@@ -470,6 +472,7 @@ func GetAllUserBookingsHandler(w http.ResponseWriter, r *http.Request) {
|
||||
http.Error(w, "Internal server error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
b.DurationMinutes = durationMinutes
|
||||
if createdBy.Valid {
|
||||
b.CreatedBy = &createdBy.String
|
||||
}
|
||||
@@ -497,15 +500,29 @@ func GetAllUserBookingsHandler(w http.ResponseWriter, r *http.Request) {
|
||||
func GetAllAdminBookingsHandler(w http.ResponseWriter, r *http.Request) {
|
||||
req := parseGetAllBookingsRequest(r)
|
||||
|
||||
baseQuery := `
|
||||
baseQuery := `
|
||||
WITH booking_totals AS (
|
||||
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
|
||||
FROM booking_services bs
|
||||
LEFT JOIN services s ON bs.service_id = s.id
|
||||
GROUP BY bs.booking_id
|
||||
booking_id,
|
||||
COUNT(*) AS service_count,
|
||||
SUM(total_duration) AS total_duration,
|
||||
SUM(total_amount) AS total_amount
|
||||
FROM (
|
||||
SELECT
|
||||
bs.booking_id,
|
||||
COALESCE(bs.override_duration_minutes, s.duration_minutes) AS total_duration,
|
||||
COALESCE(bs.override_price, s.price) AS total_amount
|
||||
FROM booking_services bs
|
||||
LEFT JOIN services s ON bs.service_id = s.id
|
||||
UNION ALL
|
||||
SELECT
|
||||
bcs.booking_id,
|
||||
COALESCE(bcs.override_duration_minutes, cs.duration_minutes) AS total_duration,
|
||||
COALESCE(bcs.override_price, cs.price) AS total_amount
|
||||
FROM booking_custom_services bcs
|
||||
LEFT JOIN custom_services cs ON bcs.custom_service_id = cs.id
|
||||
) combined
|
||||
GROUP BY booking_id
|
||||
),
|
||||
payment_totals AS (
|
||||
SELECT
|
||||
@@ -525,6 +542,7 @@ func GetAllAdminBookingsHandler(w http.ResponseWriter, r *http.Request) {
|
||||
COALESCE(pt.total_paid, 0) AS amount_paid,
|
||||
COALESCE(bt.total_amount, 0) - COALESCE(pt.total_paid, 0) AS amount_due,
|
||||
b.deposit_required,
|
||||
COALESCE(bt.service_count, 0) AS service_count,
|
||||
(SELECT COALESCE(SUM(p2.amount), 0)
|
||||
FROM payments p2
|
||||
WHERE p2.booking_id = b.id AND p2.status = 'completed' AND p2.created_at < b.start_time) AS pre_start_amount_paid
|
||||
@@ -576,7 +594,7 @@ func GetAllAdminBookingsHandler(w http.ResponseWriter, r *http.Request) {
|
||||
paramCount++
|
||||
}
|
||||
|
||||
baseQuery += " ORDER BY b.start_time DESC"
|
||||
baseQuery += " ORDER BY service_count DESC, b.start_time DESC"
|
||||
if req.PerPage > 0 {
|
||||
baseQuery += fmt.Sprintf(" LIMIT $%d OFFSET $%d", paramCount, paramCount+1)
|
||||
args = append(args, req.PerPage, (req.Page-1)*req.PerPage)
|
||||
@@ -619,7 +637,7 @@ func GetAllAdminBookingsHandler(w http.ResponseWriter, r *http.Request) {
|
||||
if err := rows.Scan(
|
||||
&b.ID, &b.StartTime, &b.Status, &userFullName,
|
||||
&b.DurationMinutes, &totalAmount, &amountPaid, &amountDue,
|
||||
&depositRequired, &preStartAmountPaid,
|
||||
&depositRequired, new(int), &preStartAmountPaid,
|
||||
); err != nil {
|
||||
log.Printf("Failed to scan booking row: %v", err)
|
||||
http.Error(w, "Internal server error", http.StatusInternalServerError)
|
||||
@@ -640,7 +658,12 @@ func GetAllAdminBookingsHandler(w http.ResponseWriter, r *http.Request) {
|
||||
FROM booking_services bs
|
||||
JOIN services s ON bs.service_id = s.id
|
||||
WHERE bs.booking_id = ANY($1)
|
||||
ORDER BY bs.booking_id, s.name
|
||||
UNION ALL
|
||||
SELECT bcs.booking_id, cs.name
|
||||
FROM booking_custom_services bcs
|
||||
JOIN custom_services cs ON bcs.custom_service_id = cs.id
|
||||
WHERE bcs.booking_id = ANY($1)
|
||||
ORDER BY booking_id, name
|
||||
`, bookingIDs)
|
||||
if err != nil {
|
||||
log.Printf("Failed to fetch booking services: %v", err)
|
||||
@@ -748,14 +771,22 @@ func GetAllBookingsByUserHandler(w http.ResponseWriter, r *http.Request) {
|
||||
FROM booking_services bs
|
||||
LEFT JOIN services s ON bs.service_id = s.id
|
||||
WHERE bs.booking_id = $1
|
||||
UNION ALL
|
||||
SELECT
|
||||
cs.name,
|
||||
COALESCE(bcs.override_price, cs.price) AS price,
|
||||
COALESCE(bcs.override_duration_minutes, cs.duration_minutes) AS duration_minutes
|
||||
FROM booking_custom_services bcs
|
||||
LEFT JOIN custom_services cs ON bcs.custom_service_id = cs.id
|
||||
WHERE bcs.booking_id = $1
|
||||
`, b.ID)
|
||||
if err != nil {
|
||||
log.Printf("Failed to fetch services for booking %s: %v", b.ID, err)
|
||||
continue
|
||||
}
|
||||
|
||||
var totalAmount float64
|
||||
for serviceRows.Next() {
|
||||
var totalAmount float64
|
||||
for serviceRows.Next() {
|
||||
var svc BookingService
|
||||
var price float64
|
||||
var dur int
|
||||
@@ -867,14 +898,23 @@ func GetAdminBookingHandler(w http.ResponseWriter, r *http.Request) {
|
||||
booking.User.ReferralCodeUses = &referralCodeUses
|
||||
|
||||
serviceRows, err := db.DB.Query(r.Context(), `
|
||||
SELECT
|
||||
bs.service_id, s.name,
|
||||
COALESCE(bs.override_price, s.price) AS price,
|
||||
COALESCE(bs.override_duration_minutes, s.duration_minutes) AS duration_minutes
|
||||
FROM booking_services bs
|
||||
LEFT JOIN services s ON bs.service_id = s.id
|
||||
WHERE bs.booking_id = $1
|
||||
ORDER BY s.name
|
||||
SELECT service_id, name, price, duration_minutes FROM (
|
||||
SELECT
|
||||
bs.service_id, s.name,
|
||||
COALESCE(bs.override_price, s.price) AS price,
|
||||
COALESCE(bs.override_duration_minutes, s.duration_minutes) AS duration_minutes
|
||||
FROM booking_services bs
|
||||
LEFT JOIN services s ON bs.service_id = s.id
|
||||
WHERE bs.booking_id = $1
|
||||
UNION ALL
|
||||
SELECT
|
||||
bcs.custom_service_id, cs.name,
|
||||
COALESCE(bcs.override_price, cs.price) AS price,
|
||||
COALESCE(bcs.override_duration_minutes, cs.duration_minutes) AS duration_minutes
|
||||
FROM booking_custom_services bcs
|
||||
LEFT JOIN custom_services cs ON bcs.custom_service_id = cs.id
|
||||
WHERE bcs.booking_id = $1
|
||||
) sub ORDER BY name
|
||||
`, bookingID)
|
||||
if err != nil {
|
||||
log.Printf("Failed to fetch services for booking %s: %v", bookingID, err)
|
||||
@@ -1099,6 +1139,12 @@ func UpdateBookingServicesHandler(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
if _, err := tx.Exec(r.Context(), "DELETE FROM booking_custom_services WHERE booking_id = $1", bookingID); err != nil {
|
||||
log.Printf("Failed to delete booking custom services for %s: %v", bookingID, err)
|
||||
http.Error(w, "Internal server error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
for _, serviceID := range req.ServiceIDs {
|
||||
var ovPrice *float64
|
||||
var ovDuration *int
|
||||
@@ -1116,6 +1162,17 @@ func UpdateBookingServicesHandler(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
for _, csID := range req.CustomServiceIDs {
|
||||
if _, err := tx.Exec(r.Context(), `
|
||||
INSERT INTO booking_custom_services (booking_id, custom_service_id)
|
||||
VALUES ($1, $2)
|
||||
`, bookingID, csID); err != nil {
|
||||
log.Printf("Failed to insert custom booking service %s: %v", csID, err)
|
||||
http.Error(w, "Internal server error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if req.Notes != nil {
|
||||
if _, err := tx.Exec(r.Context(), "UPDATE bookings SET notes = $1 WHERE id = $2", *req.Notes, bookingID); err != nil {
|
||||
log.Printf("Failed to update notes for booking %s: %v", bookingID, err)
|
||||
@@ -1173,7 +1230,14 @@ func UpdateBookingServicesHandler(w http.ResponseWriter, r *http.Request) {
|
||||
FROM booking_services bs
|
||||
LEFT JOIN services s ON bs.service_id = s.id
|
||||
WHERE bs.booking_id = $1
|
||||
ORDER BY s.name
|
||||
UNION ALL
|
||||
SELECT
|
||||
bcs.custom_service_id, bcs.override_price, bcs.override_duration_minutes,
|
||||
cs.name, cs.description, cs.price, cs.duration_minutes
|
||||
FROM booking_custom_services bcs
|
||||
LEFT JOIN custom_services cs ON bcs.custom_service_id = cs.id
|
||||
WHERE bcs.booking_id = $1
|
||||
ORDER BY name
|
||||
`, bookingID)
|
||||
if err != nil {
|
||||
log.Printf("Failed to fetch services for booking %s: %v", bookingID, err)
|
||||
@@ -1321,12 +1385,25 @@ func SearchAdminBookingsHandler(w http.ResponseWriter, r *http.Request) {
|
||||
searchQuery := `
|
||||
WITH booking_totals AS (
|
||||
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
|
||||
FROM booking_services bs
|
||||
LEFT JOIN services s ON bs.service_id = s.id
|
||||
GROUP BY bs.booking_id
|
||||
booking_id,
|
||||
SUM(total_duration) AS total_duration,
|
||||
SUM(total_amount) AS total_amount
|
||||
FROM (
|
||||
SELECT
|
||||
bs.booking_id,
|
||||
COALESCE(bs.override_duration_minutes, s.duration_minutes) AS total_duration,
|
||||
COALESCE(bs.override_price, s.price) AS total_amount
|
||||
FROM booking_services bs
|
||||
LEFT JOIN services s ON bs.service_id = s.id
|
||||
UNION ALL
|
||||
SELECT
|
||||
bcs.booking_id,
|
||||
COALESCE(bcs.override_duration_minutes, cs.duration_minutes) AS total_duration,
|
||||
COALESCE(bcs.override_price, cs.price) AS total_amount
|
||||
FROM booking_custom_services bcs
|
||||
LEFT JOIN custom_services cs ON bcs.custom_service_id = cs.id
|
||||
) combined
|
||||
GROUP BY booking_id
|
||||
),
|
||||
payment_totals AS (
|
||||
SELECT
|
||||
@@ -1366,6 +1443,11 @@ func SearchAdminBookingsHandler(w http.ResponseWriter, r *http.Request) {
|
||||
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 '\'
|
||||
) OR
|
||||
EXISTS (
|
||||
SELECT 1 FROM booking_custom_services bcs
|
||||
JOIN custom_services cs ON bcs.custom_service_id = cs.id
|
||||
WHERE bcs.booking_id = b.id AND cs.name ILIKE $1 ESCAPE '\'
|
||||
)
|
||||
ORDER BY b.start_time DESC
|
||||
LIMIT $2 OFFSET $3
|
||||
@@ -1377,6 +1459,8 @@ 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
|
||||
LEFT JOIN booking_custom_services bcs ON b.id = bcs.booking_id
|
||||
LEFT JOIN custom_services cs ON bcs.custom_service_id = cs.id
|
||||
WHERE
|
||||
b.id ILIKE $1 ESCAPE '\' OR
|
||||
b.notes ILIKE $1 ESCAPE '\' OR
|
||||
@@ -1386,7 +1470,8 @@ func SearchAdminBookingsHandler(w http.ResponseWriter, r *http.Request) {
|
||||
u.fn ILIKE $1 ESCAPE '\' OR
|
||||
u.email ILIKE $1 ESCAPE '\' OR
|
||||
u.phone ILIKE $1 ESCAPE '\' OR
|
||||
s.name ILIKE $1 ESCAPE '\'
|
||||
s.name ILIKE $1 ESCAPE '\' OR
|
||||
cs.name ILIKE $1 ESCAPE '\'
|
||||
`
|
||||
|
||||
var total int
|
||||
@@ -1438,11 +1523,17 @@ func SearchAdminBookingsHandler(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
if len(bookingIDs) > 0 {
|
||||
serviceRows, err := db.DB.Query(r.Context(), `
|
||||
SELECT bs.booking_id, s.name
|
||||
FROM booking_services bs
|
||||
JOIN services s ON bs.service_id = s.id
|
||||
WHERE bs.booking_id = ANY($1)
|
||||
ORDER BY bs.booking_id, s.name
|
||||
SELECT booking_id, name FROM (
|
||||
SELECT bs.booking_id, s.name
|
||||
FROM booking_services bs
|
||||
JOIN services s ON bs.service_id = s.id
|
||||
WHERE bs.booking_id = ANY($1)
|
||||
UNION ALL
|
||||
SELECT bcs.booking_id, cs.name
|
||||
FROM booking_custom_services bcs
|
||||
JOIN custom_services cs ON bcs.custom_service_id = cs.id
|
||||
WHERE bcs.booking_id = ANY($1)
|
||||
) sub ORDER BY booking_id, name
|
||||
`, bookingIDs)
|
||||
if err != nil {
|
||||
log.Printf("Failed to fetch booking services: %v", err)
|
||||
@@ -1523,6 +1614,12 @@ func CreateBookingHandler(w http.ResponseWriter, r *http.Request) {
|
||||
FROM booking_services bs
|
||||
JOIN services s ON bs.service_id = s.id
|
||||
WHERE bs.booking_id = $1
|
||||
UNION ALL
|
||||
SELECT bcs.booking_id, bcs.custom_service_id, bcs.override_price, bcs.override_duration_minutes,
|
||||
cs.name, cs.description, cs.price, cs.duration_minutes
|
||||
FROM booking_custom_services bcs
|
||||
JOIN custom_services cs ON bcs.custom_service_id = cs.id
|
||||
WHERE bcs.booking_id = $1
|
||||
`, existingID)
|
||||
if err == nil {
|
||||
defer rows.Close()
|
||||
@@ -1710,8 +1807,13 @@ func CreateBookingHandler(w http.ResponseWriter, r *http.Request) {
|
||||
SELECT COUNT(*) FROM bookings WHERE status IN ('confirmed','in_progress','completed')
|
||||
AND start_time < $2
|
||||
AND start_time + (INTERVAL '1 minute' * (
|
||||
SELECT COALESCE(SUM(COALESCE(bs.override_duration_minutes,s.duration_minutes)),60)
|
||||
FROM booking_services bs JOIN services s ON bs.service_id=s.id WHERE bs.booking_id=bookings.id
|
||||
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)
|
||||
if cnt > 0 {
|
||||
@@ -1899,10 +2001,17 @@ func EditBookingHandler(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
var durationMinutes int
|
||||
if err := db.DB.QueryRow(r.Context(), `
|
||||
SELECT COALESCE(SUM(COALESCE(bs.override_duration_minutes, s.duration_minutes)), 60)
|
||||
FROM booking_services bs
|
||||
JOIN services s ON bs.service_id = s.id
|
||||
WHERE bs.booking_id = $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 = $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 {
|
||||
log.Printf("Failed to get booking duration %s: %v", bookingID, err)
|
||||
durationMinutes = 60
|
||||
@@ -1916,10 +2025,17 @@ func EditBookingHandler(w http.ResponseWriter, r *http.Request) {
|
||||
AND status NOT IN ('completed', 'client_cancelled', 'we_cancelled')
|
||||
AND start_time < $3
|
||||
AND start_time + (INTERVAL '1 minute' * (
|
||||
SELECT COALESCE(SUM(COALESCE(bs.override_duration_minutes, s.duration_minutes)), 60)
|
||||
FROM booking_services bs
|
||||
JOIN services s ON bs.service_id = s.id
|
||||
WHERE bs.booking_id = bookings.id
|
||||
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 {
|
||||
log.Printf("Failed to check overlap %s: %v", bookingID, err)
|
||||
@@ -2069,10 +2185,17 @@ func ProgressBookingHandler(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
var bookingTotal float64
|
||||
if err := db.DB.QueryRow(r.Context(), `
|
||||
SELECT COALESCE(SUM(COALESCE(bs.override_price, s.price)), 0)
|
||||
FROM booking_services bs
|
||||
JOIN services s ON bs.service_id = s.id
|
||||
WHERE bs.booking_id = $1
|
||||
SELECT COALESCE(SUM(price_val), 0) FROM (
|
||||
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 {
|
||||
log.Printf("Failed to calculate booking total for %s: %v", bookingID, err)
|
||||
}
|
||||
@@ -2316,6 +2439,17 @@ func ConfirmBookingHandler(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
for _, override := range req.CustomServiceOverrides {
|
||||
if override.OverridePrice != nil && *override.OverridePrice < 0 {
|
||||
http.Error(w, "Override price cannot be negative", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
if override.OverrideDurationMinutes != nil && *override.OverrideDurationMinutes <= 0 {
|
||||
http.Error(w, "Override duration must be positive", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
var bkStart time.Time
|
||||
if err := db.DB.QueryRow(r.Context(), "SELECT start_time FROM bookings WHERE id = $1", bookingID).Scan(&bkStart); err != nil {
|
||||
log.Printf("Failed to get start time: %v", err)
|
||||
@@ -2325,8 +2459,13 @@ func ConfirmBookingHandler(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
var dur int
|
||||
db.DB.QueryRow(r.Context(), `
|
||||
SELECT COALESCE(SUM(COALESCE(bs.override_duration_minutes, s.duration_minutes)), 60)
|
||||
FROM booking_services bs JOIN services s ON bs.service_id = s.id WHERE bs.booking_id = $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 = $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)
|
||||
newEnd := bkStart.Add(time.Duration(dur) * time.Minute)
|
||||
|
||||
@@ -2335,8 +2474,13 @@ func ConfirmBookingHandler(w http.ResponseWriter, r *http.Request) {
|
||||
SELECT COUNT(*) FROM bookings WHERE id != $1 AND status IN ('confirmed','in_progress','completed')
|
||||
AND start_time < $3
|
||||
AND start_time + (INTERVAL '1 minute' * (
|
||||
SELECT COALESCE(SUM(COALESCE(bs.override_duration_minutes,s.duration_minutes)),60)
|
||||
FROM booking_services bs JOIN services s ON bs.service_id=s.id WHERE bs.booking_id=bookings.id
|
||||
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, newEnd).Scan(&cnt)
|
||||
if cnt > 0 {
|
||||
@@ -2407,6 +2551,36 @@ func ConfirmBookingHandler(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
if len(req.CustomServiceOverrides) > 0 {
|
||||
customServiceIDs := make([]string, len(req.CustomServiceOverrides))
|
||||
for i, o := range req.CustomServiceOverrides {
|
||||
customServiceIDs[i] = o.ServiceID
|
||||
}
|
||||
var customCount int
|
||||
if err := tx.QueryRow(r.Context(), `
|
||||
SELECT COUNT(*) FROM booking_custom_services WHERE booking_id = $1 AND custom_service_id = ANY($2)
|
||||
`, bookingID, customServiceIDs).Scan(&customCount); err != nil {
|
||||
log.Printf("Failed to verify custom services for booking %s: %v", bookingID, err)
|
||||
http.Error(w, "Internal server error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
if customCount != len(req.CustomServiceOverrides) {
|
||||
http.Error(w, "One or more custom service IDs do not belong to this booking", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
for _, override := range req.CustomServiceOverrides {
|
||||
if _, err := tx.Exec(r.Context(), `
|
||||
UPDATE booking_custom_services
|
||||
SET override_price = $1, override_duration_minutes = $2
|
||||
WHERE booking_id = $3 AND custom_service_id = $4
|
||||
`, override.OverridePrice, override.OverrideDurationMinutes, bookingID, override.ServiceID); err != nil {
|
||||
log.Printf("Failed to update custom service override for booking %s, custom service %s: %v", bookingID, override.ServiceID, err)
|
||||
http.Error(w, "Internal server error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if err := tx.Commit(r.Context()); err != nil {
|
||||
log.Printf("Failed to commit booking confirmation: %v", err)
|
||||
http.Error(w, "Internal server error", http.StatusInternalServerError)
|
||||
@@ -2416,10 +2590,17 @@ func ConfirmBookingHandler(w http.ResponseWriter, r *http.Request) {
|
||||
if dav.Service != nil {
|
||||
var durationMinutes int
|
||||
db.DB.QueryRow(r.Context(), `
|
||||
SELECT COALESCE(SUM(COALESCE(bs.override_duration_minutes, s.duration_minutes)), 60)
|
||||
FROM booking_services bs
|
||||
JOIN services s ON bs.service_id = s.id
|
||||
WHERE bs.booking_id = $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 = $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)
|
||||
if durationMinutes == 0 {
|
||||
durationMinutes = 60
|
||||
@@ -2716,7 +2897,14 @@ func GetBookingHandler(w http.ResponseWriter, r *http.Request) {
|
||||
FROM booking_services bs
|
||||
LEFT JOIN services s ON bs.service_id = s.id
|
||||
WHERE bs.booking_id = $1
|
||||
ORDER BY s.name
|
||||
UNION ALL
|
||||
SELECT
|
||||
bcs.custom_service_id, bcs.override_price, bcs.override_duration_minutes,
|
||||
cs.name, cs.description, cs.price, cs.duration_minutes
|
||||
FROM booking_custom_services bcs
|
||||
LEFT JOIN custom_services cs ON bcs.custom_service_id = cs.id
|
||||
WHERE bcs.booking_id = $1
|
||||
ORDER BY name
|
||||
`, bookingID)
|
||||
if err != nil {
|
||||
log.Printf("Failed to fetch services for booking %s: %v", bookingID, err)
|
||||
@@ -2884,9 +3072,15 @@ func GetBookingCalendarHandler(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
if err := db.DB.QueryRow(r.Context(), `
|
||||
SELECT id, user_id, start_time, status, COALESCE(notes, ''), COALESCE(created_by, ''), created_at, updated_at,
|
||||
COALESCE((SELECT SUM(COALESCE(bs.override_duration_minutes, s.duration_minutes))
|
||||
FROM booking_services bs JOIN services s ON bs.service_id = s.id
|
||||
WHERE bs.booking_id = bookings.id), 60)
|
||||
COALESCE((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), 60)
|
||||
FROM bookings
|
||||
WHERE id = $1 AND user_id = $2
|
||||
`, bookingID, userID).Scan(&bookingIDDB, &userIDDB, &startTime, &status, ¬es, &createdBy, &createdAt, &updatedAt, &durationMinutes); err != nil {
|
||||
@@ -2904,6 +3098,11 @@ func GetBookingCalendarHandler(w http.ResponseWriter, r *http.Request) {
|
||||
FROM booking_services bs
|
||||
JOIN services s ON bs.service_id = s.id
|
||||
WHERE bs.booking_id = $1
|
||||
UNION ALL
|
||||
SELECT cs.name, 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
|
||||
`, bookingID)
|
||||
if err != nil {
|
||||
log.Printf("Failed to fetch services: %v", err)
|
||||
@@ -3010,22 +3209,29 @@ func GetOverlappingBookingsByTimeHandler(w http.ResponseWriter, r *http.Request)
|
||||
b.start_time,
|
||||
b.status,
|
||||
b.created_at,
|
||||
COALESCE(SUM(COALESCE(bs.override_duration_minutes, s.duration_minutes)), 60) as duration,
|
||||
(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) as duration,
|
||||
u.id as user_id,
|
||||
u.fn,
|
||||
u.email,
|
||||
u.phone
|
||||
FROM bookings b
|
||||
LEFT JOIN booking_services bs ON b.id = bs.booking_id
|
||||
LEFT JOIN services s ON bs.service_id = s.id
|
||||
LEFT JOIN users u ON b.user_id = u.id
|
||||
WHERE b.status NOT IN ('completed', 'client_cancelled', 'we_cancelled', 'no_show', 'no_deposit')
|
||||
AND b.start_time < $2
|
||||
AND b.start_time + (INTERVAL '1 minute' * (
|
||||
SELECT COALESCE(SUM(COALESCE(bs2.override_duration_minutes, s2.duration_minutes)), 60)
|
||||
FROM booking_services bs2
|
||||
JOIN services s2 ON bs2.service_id = s2.id
|
||||
WHERE bs2.booking_id = b.id
|
||||
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
|
||||
ORDER BY b.start_time ASC
|
||||
@@ -3047,11 +3253,17 @@ func GetOverlappingBookingsByTimeHandler(w http.ResponseWriter, r *http.Request)
|
||||
}
|
||||
|
||||
serviceRows, err := db.DB.Query(r.Context(), `
|
||||
SELECT s.name
|
||||
FROM booking_services bs
|
||||
JOIN services s ON bs.service_id = s.id
|
||||
WHERE bs.booking_id = $1
|
||||
ORDER BY s.name
|
||||
SELECT name FROM (
|
||||
SELECT s.name
|
||||
FROM booking_services bs
|
||||
JOIN services s ON bs.service_id = s.id
|
||||
WHERE bs.booking_id = $1
|
||||
UNION ALL
|
||||
SELECT cs.name
|
||||
FROM booking_custom_services bcs
|
||||
JOIN custom_services cs ON bcs.custom_service_id = cs.id
|
||||
WHERE bcs.booking_id = $1
|
||||
) sub ORDER BY name
|
||||
`, ob.ID)
|
||||
if err == nil {
|
||||
for serviceRows.Next() {
|
||||
@@ -3090,13 +3302,16 @@ func GetOverlappingBookingsHandler(w http.ResponseWriter, r *http.Request) {
|
||||
var startTime time.Time
|
||||
var durationMinutes int
|
||||
if err := db.DB.QueryRow(r.Context(), `
|
||||
SELECT b.start_time,
|
||||
COALESCE(SUM(COALESCE(bs.override_duration_minutes, s.duration_minutes)), 60)
|
||||
SELECT b.start_time,
|
||||
(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
|
||||
LEFT JOIN booking_services bs ON b.id = bs.booking_id
|
||||
LEFT JOIN services s ON bs.service_id = s.id
|
||||
WHERE b.id = $1
|
||||
GROUP BY b.start_time
|
||||
`, bookingID).Scan(&startTime, &durationMinutes); err != nil {
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
http.Error(w, "Booking not found", http.StatusNotFound)
|
||||
@@ -3116,21 +3331,28 @@ func GetOverlappingBookingsHandler(w http.ResponseWriter, r *http.Request) {
|
||||
b.start_time,
|
||||
b.status,
|
||||
b.created_at,
|
||||
COALESCE(SUM(COALESCE(bs.override_duration_minutes, s.duration_minutes)), 60) as duration,
|
||||
(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) as duration,
|
||||
u.fn,
|
||||
u.email
|
||||
FROM bookings b
|
||||
LEFT JOIN booking_services bs ON b.id = bs.booking_id
|
||||
LEFT JOIN services s ON bs.service_id = s.id
|
||||
LEFT JOIN users u ON b.user_id = u.id
|
||||
WHERE b.id != $1
|
||||
AND b.status NOT IN ('completed', 'client_cancelled', 'we_cancelled', 'no_show', 'no_deposit')
|
||||
AND b.start_time < $3
|
||||
AND b.start_time + (INTERVAL '1 minute' * (
|
||||
SELECT COALESCE(SUM(COALESCE(bs2.override_duration_minutes, s2.duration_minutes)), 60)
|
||||
FROM booking_services bs2
|
||||
JOIN services s2 ON bs2.service_id = s2.id
|
||||
WHERE bs2.booking_id = b.id
|
||||
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
|
||||
ORDER BY b.created_at ASC
|
||||
@@ -3153,11 +3375,17 @@ func GetOverlappingBookingsHandler(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
// Get services for this booking
|
||||
serviceRows, err := db.DB.Query(r.Context(), `
|
||||
SELECT s.name
|
||||
FROM booking_services bs
|
||||
JOIN services s ON bs.service_id = s.id
|
||||
WHERE bs.booking_id = $1
|
||||
ORDER BY s.name
|
||||
SELECT name FROM (
|
||||
SELECT s.name
|
||||
FROM booking_services bs
|
||||
JOIN services s ON bs.service_id = s.id
|
||||
WHERE bs.booking_id = $1
|
||||
UNION ALL
|
||||
SELECT cs.name
|
||||
FROM booking_custom_services bcs
|
||||
JOIN custom_services cs ON bcs.custom_service_id = cs.id
|
||||
WHERE bcs.booking_id = $1
|
||||
) sub ORDER BY name
|
||||
`, ob.ID)
|
||||
if err == nil {
|
||||
for serviceRows.Next() {
|
||||
@@ -3212,14 +3440,18 @@ func GetBookingsByDateRangeHandler(w http.ResponseWriter, r *http.Request) {
|
||||
b.status,
|
||||
b.notes,
|
||||
b.created_at,
|
||||
COALESCE(SUM(COALESCE(bs.override_duration_minutes, s.duration_minutes)), 60) as duration,
|
||||
(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) as duration,
|
||||
u.id as user_id,
|
||||
u.fn,
|
||||
u.email,
|
||||
u.phone
|
||||
FROM bookings b
|
||||
LEFT JOIN booking_services bs ON b.id = bs.booking_id
|
||||
LEFT JOIN services s ON bs.service_id = s.id
|
||||
LEFT JOIN users u ON b.user_id = u.id
|
||||
WHERE b.status NOT IN ('completed', 'client_cancelled', 'we_cancelled', 'no_show', 'no_deposit')
|
||||
AND b.start_time >= $1
|
||||
@@ -3245,11 +3477,17 @@ func GetBookingsByDateRangeHandler(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
serviceRows, err := db.DB.Query(r.Context(), `
|
||||
SELECT s.name
|
||||
FROM booking_services bs
|
||||
JOIN services s ON bs.service_id = s.id
|
||||
WHERE bs.booking_id = $1
|
||||
ORDER BY s.name
|
||||
SELECT name FROM (
|
||||
SELECT s.name
|
||||
FROM booking_services bs
|
||||
JOIN services s ON bs.service_id = s.id
|
||||
WHERE bs.booking_id = $1
|
||||
UNION ALL
|
||||
SELECT cs.name
|
||||
FROM booking_custom_services bcs
|
||||
JOIN custom_services cs ON bcs.custom_service_id = cs.id
|
||||
WHERE bcs.booking_id = $1
|
||||
) sub ORDER BY name
|
||||
`, ob.ID)
|
||||
if err == nil {
|
||||
for serviceRows.Next() {
|
||||
@@ -3309,14 +3547,18 @@ func GetBookingsByCreatedRangeHandler(w http.ResponseWriter, r *http.Request) {
|
||||
b.status,
|
||||
b.notes,
|
||||
b.created_at,
|
||||
COALESCE(SUM(COALESCE(bs.override_duration_minutes, s.duration_minutes)), 60) as duration,
|
||||
(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) as duration,
|
||||
u.id as user_id,
|
||||
u.fn,
|
||||
u.email,
|
||||
u.phone
|
||||
FROM bookings b
|
||||
LEFT JOIN booking_services bs ON b.id = bs.booking_id
|
||||
LEFT JOIN services s ON bs.service_id = s.id
|
||||
LEFT JOIN users u ON b.user_id = u.id
|
||||
WHERE b.created_at >= $1
|
||||
AND b.created_at < $2
|
||||
@@ -3341,11 +3583,17 @@ func GetBookingsByCreatedRangeHandler(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
serviceRows, err := db.DB.Query(r.Context(), `
|
||||
SELECT s.name
|
||||
FROM booking_services bs
|
||||
JOIN services s ON bs.service_id = s.id
|
||||
WHERE bs.booking_id = $1
|
||||
ORDER BY s.name
|
||||
SELECT name FROM (
|
||||
SELECT s.name
|
||||
FROM booking_services bs
|
||||
JOIN services s ON bs.service_id = s.id
|
||||
WHERE bs.booking_id = $1
|
||||
UNION ALL
|
||||
SELECT cs.name
|
||||
FROM booking_custom_services bcs
|
||||
JOIN custom_services cs ON bcs.custom_service_id = cs.id
|
||||
WHERE bcs.booking_id = $1
|
||||
) sub ORDER BY name
|
||||
`, ob.ID)
|
||||
if err == nil {
|
||||
for serviceRows.Next() {
|
||||
@@ -3419,10 +3667,17 @@ func AdminRescheduleBookingHandler(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
var durationMinutes int
|
||||
if err := db.DB.QueryRow(r.Context(), `
|
||||
SELECT COALESCE(SUM(COALESCE(bs.override_duration_minutes, s.duration_minutes)), 60)
|
||||
FROM booking_services bs
|
||||
JOIN services s ON bs.service_id = s.id
|
||||
WHERE bs.booking_id = $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 = $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 {
|
||||
log.Printf("Failed to get booking duration %s: %v", bookingID, err)
|
||||
durationMinutes = 60
|
||||
@@ -3436,10 +3691,17 @@ func AdminRescheduleBookingHandler(w http.ResponseWriter, r *http.Request) {
|
||||
AND status NOT IN ('completed', 'client_cancelled', 'we_cancelled')
|
||||
AND start_time < $3
|
||||
AND start_time + (INTERVAL '1 minute' * (
|
||||
SELECT COALESCE(SUM(COALESCE(bs2.override_duration_minutes, s2.duration_minutes)), 60)
|
||||
FROM booking_services bs2
|
||||
JOIN services s2 ON bs2.service_id = s2.id
|
||||
WHERE bs2.booking_id = bookings.id
|
||||
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 {
|
||||
log.Printf("Failed to check overlap %s: %v", bookingID, err)
|
||||
|
||||
Reference in New Issue
Block a user