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:
@@ -25,6 +25,7 @@ type AdminReserveSlotRequest struct {
|
||||
UserID *string `json:"user_id"`
|
||||
StartTime time.Time `json:"start_time"`
|
||||
ServiceIDs []string `json:"service_ids"`
|
||||
CustomServiceIDs []string `json:"custom_service_ids,omitempty"`
|
||||
ServiceOverrides []ServiceOverrideRequest `json:"service_overrides"`
|
||||
TTLMinutes int `json:"ttl_minutes"` // 15 for both walk-in and call-in
|
||||
ReservationType string `json:"reservation_type"` // "walkin" or "callin"
|
||||
@@ -72,8 +73,8 @@ func AdminReserveSlotHandler(w http.ResponseWriter, r *http.Request) {
|
||||
var svcDuration int
|
||||
|
||||
if req.ReservationType == "callin" {
|
||||
if len(req.ServiceIDs) == 0 {
|
||||
http.Error(w, "At least one service is required for call-in bookings", http.StatusBadRequest)
|
||||
if len(req.ServiceIDs) == 0 && len(req.CustomServiceIDs) == 0 {
|
||||
http.Error(w, "At least one service or custom service is required for call-in bookings", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
if req.StartTime.Before(time.Now()) {
|
||||
@@ -81,7 +82,8 @@ func AdminReserveSlotHandler(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
var err error
|
||||
svcDuration, err = calculateServiceDurationWithOverrides(r.Context(), req.ServiceIDs, req.ServiceOverrides)
|
||||
allIDs := append(req.ServiceIDs, req.CustomServiceIDs...)
|
||||
svcDuration, err = calculateServiceDurationWithOverrides(r.Context(), allIDs, req.ServiceOverrides)
|
||||
if err != nil {
|
||||
log.Printf("Failed to calculate duration: %v", err)
|
||||
http.Error(w, "Internal server error", http.StatusInternalServerError)
|
||||
@@ -131,8 +133,13 @@ func AdminReserveSlotHandler(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 {
|
||||
@@ -203,7 +210,11 @@ func calculateServiceDurationWithOverrides(ctx context.Context, serviceIDs []str
|
||||
if len(overrides) == 0 {
|
||||
var duration int
|
||||
err := db.DB.QueryRow(ctx, `
|
||||
SELECT COALESCE(SUM(duration_minutes), 0) FROM services WHERE id = ANY($1)
|
||||
SELECT COALESCE(SUM(dur), 0) FROM (
|
||||
SELECT duration_minutes AS dur FROM services WHERE id = ANY($1)
|
||||
UNION ALL
|
||||
SELECT duration_minutes FROM custom_services WHERE id = ANY($1)
|
||||
) combined
|
||||
`, serviceIDs).Scan(&duration)
|
||||
return duration, err
|
||||
}
|
||||
@@ -219,7 +230,9 @@ func calculateServiceDurationWithOverrides(ctx context.Context, serviceIDs []str
|
||||
// Get all services
|
||||
rows, err := db.DB.Query(ctx, `
|
||||
SELECT id, duration_minutes FROM services WHERE id = ANY($1)
|
||||
`, serviceIDs)
|
||||
UNION ALL
|
||||
SELECT id, duration_minutes FROM custom_services WHERE id = ANY($1)
|
||||
`, serviceIDs, serviceIDs)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -296,10 +296,13 @@ func AdminEditBookingHandler(w http.ResponseWriter, r *http.Request) {
|
||||
// Get booking duration for overlap check
|
||||
var durationMinutes int
|
||||
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)
|
||||
if err != nil {
|
||||
log.Printf("Failed to get booking duration %s: %v", bookingID, err)
|
||||
@@ -315,10 +318,13 @@ func AdminEditBookingHandler(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)
|
||||
if err != nil {
|
||||
@@ -418,12 +424,14 @@ func AdminEditBookingHandler(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
type AdminCreateBookingForUserRequest struct {
|
||||
UserID string `json:"user_id" validate:"required"`
|
||||
StartTime time.Time `json:"start_time" validate:"required"`
|
||||
ServiceIDs []string `json:"service_ids" validate:"required,min=1"`
|
||||
ServiceOverrides []ServiceOverride `json:"service_overrides,omitempty"`
|
||||
Notes *string `json:"notes,omitempty" validate:"omitempty,max=1000000"` // appointment notes, visible to customers and staff
|
||||
EnforceDeposits *bool `json:"enforce_deposits,omitempty"` // Optional: if true, enforce outstanding deposit checks; if false or omitted, bypass checks
|
||||
UserID string `json:"user_id" validate:"required"`
|
||||
StartTime time.Time `json:"start_time" validate:"required"`
|
||||
ServiceIDs []string `json:"service_ids,omitempty"`
|
||||
ServiceOverrides []ServiceOverride `json:"service_overrides,omitempty"`
|
||||
CustomServiceIDs []string `json:"custom_service_ids,omitempty"`
|
||||
CustomOverrides []ServiceOverride `json:"custom_service_overrides,omitempty"`
|
||||
Notes *string `json:"notes,omitempty" validate:"omitempty,max=1000000"`
|
||||
EnforceDeposits *bool `json:"enforce_deposits,omitempty"`
|
||||
}
|
||||
|
||||
func AdminCreateBookingForUserHandler(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -472,6 +480,12 @@ func AdminCreateBookingForUserHandler(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()
|
||||
@@ -511,12 +525,12 @@ func AdminCreateBookingForUserHandler(w http.ResponseWriter, r *http.Request) {
|
||||
http.Error(w, "Start time is required", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
if len(req.ServiceIDs) == 0 {
|
||||
http.Error(w, "At least one service is required", http.StatusBadRequest)
|
||||
if len(req.ServiceIDs) == 0 && len(req.CustomServiceIDs) == 0 {
|
||||
http.Error(w, "At least one service or custom service is required", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// Check patch test requirements for all services
|
||||
// Check patch test requirements for all regular services (custom services skip patch tests)
|
||||
for _, serviceID := range req.ServiceIDs {
|
||||
// Find patch test for this service
|
||||
var patchTestID string
|
||||
@@ -605,8 +619,8 @@ func AdminCreateBookingForUserHandler(w http.ResponseWriter, r *http.Request) {
|
||||
http.Error(w, "Start time is required", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
if len(req.ServiceIDs) == 0 {
|
||||
http.Error(w, "At least one service is required", http.StatusBadRequest)
|
||||
if len(req.ServiceIDs) == 0 && len(req.CustomServiceIDs) == 0 {
|
||||
http.Error(w, "At least one service or custom service is required", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -663,14 +677,19 @@ func AdminCreateBookingForUserHandler(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
// Check for overlapping confirmed/in_progress/completed bookings
|
||||
allIDs := append(req.ServiceIDs, req.CustomServiceIDs...)
|
||||
var dur int
|
||||
db.DB.QueryRow(r.Context(), `
|
||||
SELECT COALESCE(SUM(duration_minutes), 0) FROM services WHERE id = ANY($1)
|
||||
`, req.ServiceIDs).Scan(&dur)
|
||||
SELECT COALESCE(SUM(dur), 0) FROM (
|
||||
SELECT duration_minutes AS dur FROM services WHERE id = ANY($1)
|
||||
UNION ALL
|
||||
SELECT duration_minutes FROM custom_services WHERE id = ANY($1)
|
||||
) combined
|
||||
`, allIDs).Scan(&dur)
|
||||
newEnd := req.StartTime.Add(time.Duration(dur) * time.Minute)
|
||||
var cnt int
|
||||
db.DB.QueryRow(r.Context(), `
|
||||
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)) > $1
|
||||
SELECT COUNT(*) FROM bookings WHERE status IN ('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
|
||||
`, req.StartTime, newEnd).Scan(&cnt)
|
||||
if cnt > 0 {
|
||||
http.Error(w, "Cannot create booking - time slot overlaps with existing booking", http.StatusConflict)
|
||||
@@ -749,6 +768,23 @@ func AdminCreateBookingForUserHandler(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
customServiceInsertQuery := `
|
||||
INSERT INTO booking_custom_services (booking_id, custom_service_id)
|
||||
VALUES ($1, $2)
|
||||
`
|
||||
|
||||
for _, csID := range req.CustomServiceIDs {
|
||||
_, err := tx.Exec(r.Context(), customServiceInsertQuery, booking.ID, csID)
|
||||
if err != nil {
|
||||
log.Printf("Failed to insert custom booking service %s: %v", csID, err)
|
||||
http.Error(w, "Internal server error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
_, _ = tx.Exec(r.Context(), `
|
||||
UPDATE custom_services SET usage_count = usage_count + 1, last_used_at = NOW() WHERE id = $1
|
||||
`, csID)
|
||||
}
|
||||
|
||||
// Apply overrides (optional)
|
||||
if len(req.ServiceOverrides) > 0 {
|
||||
// Ensure overrides only reference services in this booking
|
||||
@@ -810,6 +846,55 @@ func AdminCreateBookingForUserHandler(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
if len(req.CustomOverrides) > 0 {
|
||||
customOverrideServiceIDs := make([]string, len(req.CustomOverrides))
|
||||
for i, o := range req.CustomOverrides {
|
||||
customOverrideServiceIDs[i] = o.ServiceID
|
||||
}
|
||||
var customCount int
|
||||
err = tx.QueryRow(r.Context(), `
|
||||
SELECT COUNT(*) FROM booking_custom_services
|
||||
WHERE booking_id = $1 AND custom_service_id = ANY($2)
|
||||
`, booking.ID, customOverrideServiceIDs).Scan(&customCount)
|
||||
if err != nil {
|
||||
log.Printf("Failed to verify custom service overrides: %v", err)
|
||||
http.Error(w, "Internal server error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
if customCount != len(req.CustomOverrides) {
|
||||
http.Error(w, "One or more custom service overrides do not belong to this booking", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
customOverrideUpdateQuery := `
|
||||
UPDATE booking_custom_services
|
||||
SET override_price = $1,
|
||||
override_duration_minutes = $2
|
||||
WHERE booking_id = $3 AND custom_service_id = $4
|
||||
`
|
||||
|
||||
for _, override := range req.CustomOverrides {
|
||||
_, err := tx.Exec(
|
||||
r.Context(),
|
||||
customOverrideUpdateQuery,
|
||||
override.OverridePrice,
|
||||
override.OverrideDurationMinutes,
|
||||
booking.ID,
|
||||
override.ServiceID,
|
||||
)
|
||||
if err != nil {
|
||||
log.Printf(
|
||||
"Failed to apply custom service override (booking %s, custom service %s): %v",
|
||||
booking.ID,
|
||||
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 admin booking creation: %v", err)
|
||||
http.Error(w, "Internal server error", http.StatusInternalServerError)
|
||||
@@ -994,7 +1079,14 @@ func queryBookingServicesWithDetails(ctx context.Context, bookingID string) ([]E
|
||||
FROM booking_services bs
|
||||
JOIN services s ON bs.service_id = s.id
|
||||
WHERE bs.booking_id = $1
|
||||
ORDER BY s.name
|
||||
UNION ALL
|
||||
SELECT cs.id, cs.name,
|
||||
COALESCE(bcs.override_price, cs.price),
|
||||
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
|
||||
ORDER BY name
|
||||
`, bookingID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -1217,8 +1309,13 @@ func RequestEditHandler(w http.ResponseWriter, r *http.Request) {
|
||||
if len(req.NewServices) > 0 {
|
||||
var overrideCount int
|
||||
err = db.DB.QueryRow(r.Context(), `
|
||||
SELECT COUNT(*) FROM booking_services
|
||||
WHERE booking_id = $1 AND (override_price IS NOT NULL OR override_duration_minutes IS NOT NULL)
|
||||
SELECT COUNT(*) FROM (
|
||||
SELECT 1 FROM booking_services
|
||||
WHERE booking_id = $1 AND (override_price IS NOT NULL OR override_duration_minutes IS NOT NULL)
|
||||
UNION ALL
|
||||
SELECT 1 FROM booking_custom_services
|
||||
WHERE booking_id = $1 AND (override_price IS NOT NULL OR override_duration_minutes IS NOT NULL)
|
||||
) overrides
|
||||
`, bookingID).Scan(&overrideCount)
|
||||
if err != nil {
|
||||
log.Printf("Failed to check overrides for booking %s: %v", bookingID, err)
|
||||
@@ -1281,16 +1378,29 @@ func RequestEditHandler(w http.ResponseWriter, r *http.Request) {
|
||||
var durationMinutes int
|
||||
if len(req.NewServices) > 0 {
|
||||
_ = tx.QueryRow(r.Context(), `
|
||||
SELECT COALESCE(SUM(s.duration_minutes), 60)
|
||||
FROM services s
|
||||
WHERE s.id = ANY($1)
|
||||
SELECT COALESCE(SUM(dur), 60) FROM (
|
||||
SELECT s.duration_minutes AS dur
|
||||
FROM services s
|
||||
WHERE s.id = ANY($1)
|
||||
UNION ALL
|
||||
SELECT cs.duration_minutes
|
||||
FROM custom_services cs
|
||||
WHERE cs.id = ANY($1)
|
||||
) sub
|
||||
`, req.NewServices).Scan(&durationMinutes)
|
||||
} else {
|
||||
_ = tx.QueryRow(r.Context(), `
|
||||
SELECT COALESCE(SUM(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)
|
||||
}
|
||||
|
||||
@@ -1492,10 +1602,13 @@ func AdminApproveEditRequestHandler(w http.ResponseWriter, r *http.Request) {
|
||||
if hasOverrides {
|
||||
// Use the existing booking_services with overrides
|
||||
err = tx.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)
|
||||
} else {
|
||||
// Use standard durations or new_services if provided
|
||||
@@ -1509,10 +1622,13 @@ func AdminApproveEditRequestHandler(w http.ResponseWriter, r *http.Request) {
|
||||
} else {
|
||||
// Use existing booking services
|
||||
err = tx.QueryRow(r.Context(), `
|
||||
SELECT COALESCE(SUM(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 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 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)
|
||||
}
|
||||
}
|
||||
@@ -1531,10 +1647,17 @@ func AdminApproveEditRequestHandler(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, *newStartTime, newEndTime).Scan(&overlapCount)
|
||||
if err != nil {
|
||||
@@ -1627,6 +1750,12 @@ func AdminApproveEditRequestHandler(w http.ResponseWriter, r *http.Request) {
|
||||
http.Error(w, "Internal server error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
_, err = tx.Exec(r.Context(), "DELETE FROM booking_custom_services WHERE booking_id = $1", bookingID)
|
||||
if err != nil {
|
||||
log.Printf("Failed to delete existing custom services for booking %s: %v", bookingID, err)
|
||||
http.Error(w, "Internal server error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// Insert new services
|
||||
for _, serviceID := range newServices {
|
||||
|
||||
@@ -124,8 +124,13 @@ func ReserveSlotHandler(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 {
|
||||
|
||||
@@ -157,12 +157,17 @@ func (s *PaymentService) GetBookingPaymentSummary(ctx context.Context, bookingID
|
||||
|
||||
var totalAmount float64
|
||||
err := db.DB.QueryRow(ctx, `
|
||||
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
|
||||
) price_sub
|
||||
`, bookingID).Scan(&totalAmount)
|
||||
|
||||
if err != nil && !errors.Is(err, pgx.ErrNoRows) {
|
||||
@@ -363,10 +368,17 @@ func (s *PaymentService) GetBookingRemainingBalanceCents(ctx context.Context, bo
|
||||
var remainingCents int64
|
||||
err := db.DB.QueryRow(ctx, `
|
||||
WITH booking_total AS (
|
||||
SELECT COALESCE(SUM(COALESCE(bs.override_price, s.price)), 0) AS total_pounds
|
||||
FROM booking_services bs
|
||||
LEFT JOIN services s ON bs.service_id = s.id
|
||||
WHERE bs.booking_id = $1
|
||||
SELECT COALESCE(SUM(price_val), 0) AS total_pounds FROM (
|
||||
SELECT COALESCE(bs.override_price, s.price) AS price_val
|
||||
FROM booking_services bs
|
||||
LEFT 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
|
||||
LEFT JOIN custom_services cs ON bcs.custom_service_id = cs.id
|
||||
WHERE bcs.booking_id = $1
|
||||
) price_sub
|
||||
),
|
||||
paid_total AS (
|
||||
SELECT COALESCE(SUM(amount), 0) AS paid_pounds
|
||||
|
||||
@@ -408,18 +408,23 @@ func GetAvailableHours(w http.ResponseWriter, r *http.Request) {
|
||||
bookingRows, _ := db.DB.Query(r.Context(), `
|
||||
SELECT
|
||||
b.start_time,
|
||||
COALESCE(SUM(
|
||||
CASE
|
||||
COALESCE((SELECT SUM(dur) FROM (
|
||||
SELECT 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 AS dur
|
||||
FROM booking_services bs LEFT JOIN services s ON bs.service_id = s.id WHERE bs.booking_id = b.id
|
||||
UNION ALL
|
||||
SELECT CASE
|
||||
WHEN bcs.override_duration_minutes IS NOT NULL AND bcs.override_duration_minutes > 0
|
||||
THEN bcs.override_duration_minutes
|
||||
ELSE cs.duration_minutes
|
||||
END
|
||||
), 0) AS total_duration
|
||||
FROM booking_custom_services bcs LEFT JOIN custom_services cs ON bcs.custom_service_id = cs.id WHERE bcs.booking_id = b.id
|
||||
) sub), 0) AS total_duration
|
||||
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.start_time >= $1 AND b.start_time <= $2
|
||||
GROUP BY b.id, b.start_time
|
||||
ORDER BY b.start_time
|
||||
`, start, end)
|
||||
|
||||
|
||||
@@ -55,9 +55,15 @@ func GetCurrentAndNextHandler(w http.ResponseWriter, r *http.Request) {
|
||||
AND (
|
||||
start_time + (
|
||||
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),
|
||||
(SELECT SUM(dur) 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),
|
||||
0
|
||||
) || ' minutes'
|
||||
)::interval
|
||||
@@ -75,9 +81,15 @@ func GetCurrentAndNextHandler(w http.ResponseWriter, r *http.Request) {
|
||||
AND (
|
||||
start_time + (
|
||||
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),
|
||||
(SELECT SUM(dur) 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),
|
||||
0
|
||||
) || ' minutes'
|
||||
)::interval
|
||||
@@ -242,7 +254,16 @@ func fetchAppointment(r *http.Request, query string, args ...interface{}) (*Appo
|
||||
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
|
||||
cs.name,
|
||||
cs.description,
|
||||
COALESCE(bcs.override_price, cs.price),
|
||||
COALESCE(bcs.override_duration_minutes, 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 {
|
||||
@@ -312,9 +333,15 @@ func GetTodayAppointmentsHandler(w http.ResponseWriter, r *http.Request) {
|
||||
AND (
|
||||
start_time + (
|
||||
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),
|
||||
(SELECT SUM(dur) 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),
|
||||
0
|
||||
) || ' minutes'
|
||||
)::interval
|
||||
@@ -332,9 +359,15 @@ func GetTodayAppointmentsHandler(w http.ResponseWriter, r *http.Request) {
|
||||
AND (
|
||||
start_time + (
|
||||
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),
|
||||
(SELECT SUM(dur) 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),
|
||||
0
|
||||
) || ' minutes'
|
||||
)::interval
|
||||
@@ -397,7 +430,14 @@ func GetTodayAppointmentsHandler(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
|
||||
cs.name,
|
||||
COALESCE(bcs.override_duration_minutes, 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
|
||||
`, apt.ID)
|
||||
|
||||
if err != nil {
|
||||
@@ -511,7 +551,14 @@ func GetPendingApprovalsHandler(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
|
||||
cs.name,
|
||||
COALESCE(bcs.override_duration_minutes, 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
|
||||
`, apt.ID)
|
||||
|
||||
if err != nil {
|
||||
|
||||
@@ -135,12 +135,21 @@ func GetCustomerRelationshipHandler(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
rows, err := db.DB.Query(r.Context(), `
|
||||
SELECT s.name, COUNT(*) as count
|
||||
FROM booking_services bsvc
|
||||
JOIN bookings b ON bsvc.booking_id = b.id
|
||||
JOIN services s ON bsvc.service_id = s.id
|
||||
WHERE b.user_id = $1 AND b.status = 'completed'
|
||||
GROUP BY s.name
|
||||
SELECT name, cnt FROM (
|
||||
SELECT s.name, COUNT(*) as count, COUNT(*) as cnt
|
||||
FROM booking_services bsvc
|
||||
JOIN bookings b ON bsvc.booking_id = b.id
|
||||
JOIN services s ON bsvc.service_id = s.id
|
||||
WHERE b.user_id = $1 AND b.status = 'completed'
|
||||
GROUP BY s.name
|
||||
UNION ALL
|
||||
SELECT cs.name, COUNT(*) as count, COUNT(*) as cnt
|
||||
FROM booking_custom_services bcs
|
||||
JOIN bookings b ON bcs.booking_id = b.id
|
||||
JOIN custom_services cs ON bcs.custom_service_id = cs.id
|
||||
WHERE b.user_id = $1 AND b.status = 'completed'
|
||||
GROUP BY cs.name
|
||||
) combined
|
||||
ORDER BY count DESC
|
||||
LIMIT 5
|
||||
`, userID)
|
||||
|
||||
Reference in New Issue
Block a user