Fix booking count filters, add admin notifications, and improve bookings
UI
This commit is contained in:
@@ -0,0 +1,224 @@
|
||||
package bookings
|
||||
|
||||
import (
|
||||
"crussell/db"
|
||||
"crussell/mw"
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"log"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
)
|
||||
|
||||
// UserCancelBookingHandler allows an authenticated user to cancel a booking they own.
|
||||
// The update is performed in a single statement with appropriate conditions.
|
||||
func UserCancelBookingHandler(w http.ResponseWriter, r *http.Request) {
|
||||
bookingID := chi.URLParam(r, "id")
|
||||
|
||||
userID, ok := r.Context().Value(mw.UserIDKey).(string)
|
||||
if !ok || userID == "" {
|
||||
http.Error(w, "Authentication required", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
res, err := db.DB.Exec(r.Context(), `
|
||||
UPDATE bookings
|
||||
SET status = 'client_cancelled', updated_at = $1
|
||||
WHERE id = $2 AND user_id = $3 AND status IN ('pending', 'confirmed', 'in_progress')
|
||||
`, time.Now(), bookingID, userID)
|
||||
if err != nil {
|
||||
log.Printf("Failed to cancel booking %s: %v", bookingID, err)
|
||||
http.Error(w, "Internal server error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
rowsAffected := res.RowsAffected()
|
||||
if rowsAffected == 0 {
|
||||
http.Error(w, "Booking not cancellable", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
|
||||
// AdminCancelBookingHandler allows an admin to cancel any booking.
|
||||
// The update uses a status filter and checks RowsAffected for existence.
|
||||
func AdminCancelBookingHandler(w http.ResponseWriter, r *http.Request) {
|
||||
bookingID := chi.URLParam(r, "id")
|
||||
|
||||
res, err := db.DB.Exec(r.Context(), `
|
||||
UPDATE bookings
|
||||
SET status = 'we_cancelled', updated_at = $1
|
||||
WHERE id = $2 AND status IN ('pending', 'confirmed', 'in_progress')
|
||||
`, time.Now(), bookingID)
|
||||
if err != nil {
|
||||
log.Printf("Failed to admin cancel booking %s: %v", bookingID, err)
|
||||
http.Error(w, "Internal server error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
rowsAffected := res.RowsAffected()
|
||||
if rowsAffected == 0 {
|
||||
http.Error(w, "Booking not cancellable", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
|
||||
// AdminListPendingBookingsHandler returns all bookings with status `pending` by delegating to the existing admin list handler.
|
||||
func AdminListPendingBookingsHandler(w http.ResponseWriter, r *http.Request) {
|
||||
r = r.Clone(r.Context())
|
||||
q := r.URL.Query()
|
||||
q.Set("status", "pending")
|
||||
r.URL.RawQuery = q.Encode()
|
||||
|
||||
GetAllAdminBookingsHandler(w, r)
|
||||
}
|
||||
|
||||
// AdminGetInProgressBookingHandler returns the booking that is currently in progress.
|
||||
// It joins the bookings table with users to populate the UserSummary in the returned Booking.
|
||||
func AdminGetInProgressBookingHandler(w http.ResponseWriter, r *http.Request) {
|
||||
var b Booking
|
||||
var userID, fullName string
|
||||
|
||||
err := db.DB.QueryRow(r.Context(), `
|
||||
SELECT
|
||||
b.id,
|
||||
b.start_time,
|
||||
b.status,
|
||||
b.notes,
|
||||
b.created_at,
|
||||
b.updated_at,
|
||||
b.created_by,
|
||||
u.id,
|
||||
u.fn
|
||||
FROM bookings b
|
||||
LEFT JOIN users u ON b.user_id = u.id
|
||||
WHERE b.status = 'in_progress'
|
||||
ORDER BY b.start_time
|
||||
LIMIT 1
|
||||
`).Scan(
|
||||
&b.ID,
|
||||
&b.StartTime,
|
||||
&b.Status,
|
||||
&b.Notes,
|
||||
&b.CreatedAt,
|
||||
&b.UpdatedAt,
|
||||
&b.CreatedBy,
|
||||
&userID,
|
||||
&fullName,
|
||||
)
|
||||
if err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
http.Error(w, "No in-progress booking found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
log.Printf("Failed to fetch in-progress booking: %v", err)
|
||||
http.Error(w, "Internal server error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// Populate the UserSummary field
|
||||
b.User = &UserSummary{
|
||||
ID: userID,
|
||||
FullName: fullName,
|
||||
FirstName: "", // not available here
|
||||
LastName: "", // not available here
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
if err := json.NewEncoder(w).Encode(b); err != nil {
|
||||
log.Printf("Failed to encode booking response: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// AdminEditBookingHandler allows an admin to modify the start time of any booking.
|
||||
// It validates the new start time and returns 404 if the booking does not exist.
|
||||
func AdminEditBookingHandler(w http.ResponseWriter, r *http.Request) {
|
||||
bookingID := chi.URLParam(r, "id")
|
||||
var req EditBookingRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
http.Error(w, "Invalid request body", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// Basic validation: ensure the new time is not in the past
|
||||
if time.Now().After(req.StartTime) {
|
||||
http.Error(w, "Start time must be in the future", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
res, err := db.DB.Exec(r.Context(), `
|
||||
UPDATE bookings
|
||||
SET start_time = $1, updated_at = $2
|
||||
WHERE id = $3
|
||||
`, req.StartTime, time.Now(), bookingID)
|
||||
if err != nil {
|
||||
log.Printf("Failed to edit booking %s: %v", bookingID, err)
|
||||
http.Error(w, "Internal server error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
rowsAffected := res.RowsAffected()
|
||||
if rowsAffected == 0 {
|
||||
http.Error(w, "Booking not found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
|
||||
// AdminCreateBookingForUserHandler creates a booking on behalf of a user.
|
||||
func AdminCreateBookingForUserHandler(w http.ResponseWriter, r *http.Request) {
|
||||
userID := chi.URLParam(r, "user_id")
|
||||
var req CreateBookingRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
http.Error(w, "Invalid request body", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
adminID, _ := r.Context().Value(mw.UserIDKey).(string)
|
||||
|
||||
tx, err := db.DB.Begin(r.Context())
|
||||
if err != nil {
|
||||
log.Printf("Failed to begin transaction: %v", err)
|
||||
http.Error(w, "Internal server error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
defer tx.Rollback(r.Context())
|
||||
|
||||
var bookingID string
|
||||
err = tx.QueryRow(r.Context(), `
|
||||
INSERT INTO bookings (id, user_id, start_time, status, created_by)
|
||||
VALUES (generate_booking_id(), $1, $2, 'pending', $3)
|
||||
RETURNING id
|
||||
`, userID, req.StartTime, adminID).Scan(&bookingID)
|
||||
if err != nil {
|
||||
log.Printf("Failed to create booking: %v", err)
|
||||
http.Error(w, "Internal server error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
for _, svcID := range req.ServiceIDs {
|
||||
if _, err := tx.Exec(r.Context(), `
|
||||
INSERT INTO booking_services (booking_id, service_id)
|
||||
VALUES ($1, $2)
|
||||
`, bookingID, svcID); err != nil {
|
||||
log.Printf("Failed to insert booking service: %v", err)
|
||||
http.Error(w, "Internal server error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if err = tx.Commit(r.Context()); err != nil {
|
||||
log.Printf("Failed to commit transaction: %v", err)
|
||||
http.Error(w, "Internal server error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusCreated)
|
||||
if err := json.NewEncoder(w).Encode(map[string]string{"id": bookingID}); err != nil {
|
||||
log.Printf("Failed to encode response: %v", err)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user