Split admin dashboard, implement user and booking search

This commit is contained in:
2026-01-17 18:55:58 +00:00
parent 1e8995f577
commit 3a77e582e3
17 changed files with 4315 additions and 3266 deletions
+99 -85
View File
@@ -170,10 +170,11 @@ type GetAllBookingsRequest struct {
// BookingListResponse represents a paginated list of bookings
type BookingListResponse struct {
Bookings []Booking `json:"bookings"`
Page int `json:"page"`
PerPage int `json:"per_page"`
Total int `json:"total"`
Bookings []Booking `json:"bookings"`
Total int `json:"total"`
Page int `json:"page"`
PerPage int `json:"perPage"`
TotalPages int `json:"totalPages"`
}
// SearchBookingsRequest represents search parameters
@@ -573,76 +574,53 @@ func GetAllBookingsByUserHandler(w http.ResponseWriter, r *http.Request) {
return
}
// Parse query parameters
req := parseGetAllBookingsRequest(r)
// Parse pagination parameters
query := r.URL.Query()
page := 1
perPage := 5
// Build query with specific user filter
baseQuery := `
SELECT id, user_id, start_time, status, notes, created_at, updated_at, created_by,
u.fn, u.profile_pic_url, u.notes as user_notes
FROM bookings b
LEFT JOIN users u ON b.user_id = u.id
WHERE b.user_id = $1
`
countQuery := `SELECT COUNT(*) FROM bookings WHERE user_id = $1`
var args []interface{}
args = append(args, userID)
paramCount := 2
// Add filters
if req.Status != nil {
baseQuery += fmt.Sprintf(" AND b.status = $%d", paramCount)
countQuery += fmt.Sprintf(" AND status = $%d", paramCount)
args = append(args, *req.Status)
paramCount++
}
if req.StartDate != nil {
baseQuery += fmt.Sprintf(" AND b.start_time >= $%d", paramCount)
countQuery += fmt.Sprintf(" AND start_time >= $%d", paramCount)
startTime, err := time.ParseInLocation("2006-01-02", *req.StartDate, londonLocation)
if err != nil {
http.Error(w, "Invalid start_date format, use YYYY-MM-DD", http.StatusBadRequest)
return
if pageStr := query.Get("page"); pageStr != "" {
if p, err := strconv.Atoi(pageStr); err == nil && p > 0 {
page = p
}
// Ensure it's at start of day in London time
startTime = time.Date(startTime.Year(), startTime.Month(), startTime.Day(), 0, 0, 0, 0, londonLocation)
args = append(args, startTime)
paramCount++
}
if req.EndDate != nil {
baseQuery += fmt.Sprintf(" AND b.start_time <= $%d", paramCount)
countQuery += fmt.Sprintf(" AND start_time <= $%d", paramCount)
endTime, err := time.Parse("2006-01-02", *req.EndDate)
if err != nil {
http.Error(w, "Invalid end_date format, use YYYY-MM-DD", http.StatusBadRequest)
return
if perPageStr := query.Get("per_page"); perPageStr != "" {
if pp, err := strconv.Atoi(perPageStr); err == nil && pp > 0 && pp <= 100 {
perPage = pp
}
endTime = endTime.Add(23*time.Hour + 59*time.Minute + 59*time.Second)
args = append(args, endTime)
paramCount++
}
// Add ordering and pagination
baseQuery += " ORDER BY b.start_time ASC"
if req.PerPage > 0 {
baseQuery += fmt.Sprintf(" LIMIT $%d OFFSET $%d", paramCount, paramCount+1)
args = append(args, req.PerPage, (req.Page-1)*req.PerPage)
}
offset := (page - 1) * perPage
// Get total count
var total int
err := db.DB.QueryRow(r.Context(), countQuery, args[:1]...).Scan(&total)
err := db.DB.QueryRow(r.Context(), `
SELECT COUNT(*)
FROM bookings
WHERE user_id = $1
`, userID).Scan(&total)
if err != nil {
log.Printf("Failed to get booking count for user %s: %v", userID, err)
log.Printf("Failed to count bookings for user %s: %v", userID, err)
http.Error(w, "Internal server error", http.StatusInternalServerError)
return
}
// Get bookings
rows, err := db.DB.Query(r.Context(), baseQuery, args...)
// Fetch bookings with pagination, ordered by start_time DESC (future to past)
rows, err := db.DB.Query(r.Context(), `
SELECT
b.id,
b.start_time,
b.status,
b.notes,
b.created_at,
b.updated_at,
b.created_by
FROM bookings b
WHERE b.user_id = $1
ORDER BY b.start_time DESC
LIMIT $2 OFFSET $3
`, userID, perPage, offset)
if err != nil {
log.Printf("Failed to fetch bookings for user %s: %v", userID, err)
http.Error(w, "Internal server error", http.StatusInternalServerError)
@@ -653,45 +631,81 @@ func GetAllBookingsByUserHandler(w http.ResponseWriter, r *http.Request) {
var bookings []Booking
for rows.Next() {
var b Booking
b.User = &UserSummary{}
var createdBy sql.NullString
var userFullName, userPicURL, userNotes sql.NullString
err := rows.Scan(
&b.ID, &b.User.ID, &b.StartTime, &b.Status, &b.Notes,
&b.CreatedAt, &b.UpdatedAt, &createdBy,
&userFullName, &userPicURL, &userNotes,
&b.ID,
&b.StartTime,
&b.Status,
&b.Notes,
&b.CreatedAt,
&b.UpdatedAt,
&b.CreatedBy,
)
if userFullName.Valid {
b.User.FullName = userFullName.String
}
if userPicURL.Valid {
b.User.ProfilePicURL = &userPicURL.String
}
if userNotes.Valid {
b.User.Notes = &userNotes.String
}
if err != nil {
log.Printf("Failed to scan booking row: %v", err)
http.Error(w, "Internal server error", http.StatusInternalServerError)
return
}
if createdBy.Valid {
b.CreatedBy = &createdBy.String
// Fetch services for this booking
serviceRows, err := db.DB.Query(r.Context(), `
SELECT
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
`, 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 service BookingService
var price float64
var durationMinutes int
if err := serviceRows.Scan(&service.ServiceName, &price, &durationMinutes); err != nil {
log.Printf("Failed to scan service: %v", err)
continue
}
service.Price = &price
service.DurationMinutes = &durationMinutes
totalAmount += price
b.Services = append(b.Services, service)
}
serviceRows.Close()
b.TotalAmount = totalAmount
bookings = append(bookings, b)
}
if bookings == nil {
bookings = []Booking{}
}
// Calculate total pages
totalPages := (total + perPage - 1) / perPage
if totalPages == 0 {
totalPages = 1
}
response := BookingListResponse{
Bookings: bookings,
Page: req.Page,
PerPage: req.PerPage,
Total: total,
Bookings: bookings,
Total: total,
Page: page,
PerPage: perPage,
TotalPages: totalPages,
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
if err := json.NewEncoder(w).Encode(response); err != nil {
log.Printf("Failed to encode response: %v", err)
log.Printf("Failed to encode bookings response: %v", err)
http.Error(w, "Internal server error", http.StatusInternalServerError)
return
}
@@ -773,7 +787,7 @@ func GetAdminBookingHandler(w http.ResponseWriter, r *http.Request) {
defer serviceRows.Close()
var totalAmount float64
var durationMinutes int
var durationMinutesTotal int
for serviceRows.Next() {
var name string
@@ -788,7 +802,7 @@ func GetAdminBookingHandler(w http.ResponseWriter, r *http.Request) {
// Calculate totals
totalAmount += price
durationMinutes += durationMinutes
durationMinutesTotal += durationMinutes
booking.Services = append(booking.Services, BookingService{
ServiceName: &name,
Price: &price,
@@ -797,7 +811,7 @@ func GetAdminBookingHandler(w http.ResponseWriter, r *http.Request) {
}
booking.TotalAmount = totalAmount
booking.DurationMinutes = durationMinutes
booking.DurationMinutes = durationMinutesTotal
// ----------------------------
// 3. Fetch payments and calculate amount paid