Split admin dashboard, implement user and booking search
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
package portfolio
|
||||
|
||||
// import (
|
||||
// "crussell/db"
|
||||
// "net/http"
|
||||
// )
|
||||
|
||||
// func GetImages(w http.ResponseWriter, r *http.Request) {
|
||||
// rows, err := db.DB.Query(r.Context(), `
|
||||
// select id, r2_url
|
||||
// from images
|
||||
// where
|
||||
// ($1::text[] is null or tag_names @> $1)
|
||||
// and ($2::text is null or exists (
|
||||
// select 1 from unnest(tag_names) as tag
|
||||
// where tag ilike '%' || $2 || '%'
|
||||
// ))
|
||||
// order by created_at desc
|
||||
// limit $3 offset $4
|
||||
// `,
|
||||
// semanticTags,
|
||||
// searchTerm,
|
||||
// limit,
|
||||
// offset,
|
||||
// )
|
||||
// }
|
||||
|
||||
// func getAutoCompleteAdmin(w http.ResponseWriter, r *http.Request) {
|
||||
// rows, err := db.DB.Query(r.Context(), `
|
||||
// select name
|
||||
// from tags
|
||||
// where name ilike '%' || $1 || '%'
|
||||
// order by similarity(name, $1) desc
|
||||
// limit 10
|
||||
// `, query)
|
||||
// }
|
||||
|
||||
// func getAutoCompleteUser(w http.ResponseWriter, r *http.Request) {
|
||||
// rows, err := db.DB.Query(r.Context(), `
|
||||
// select name
|
||||
// from tags
|
||||
// where
|
||||
// name not like '%:%'
|
||||
// and name ilike '%' || $1 || '%'
|
||||
// order by similarity(name, $1) desc
|
||||
// limit 10
|
||||
// `, query)
|
||||
// }
|
||||
@@ -2,13 +2,17 @@ package user
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"golang.org/x/text/cases"
|
||||
"golang.org/x/text/language"
|
||||
|
||||
@@ -39,6 +43,55 @@ type UpdateProfileRequest struct {
|
||||
Phone string `json:"phone"`
|
||||
}
|
||||
|
||||
type AdminUserDetail struct {
|
||||
ID string `json:"id"`
|
||||
Email *string `json:"email,omitempty"`
|
||||
FirstName string `json:"firstName"`
|
||||
LastName string `json:"lastName"`
|
||||
FullName string `json:"fullName"`
|
||||
Phone *string `json:"phone,omitempty"`
|
||||
DateOfBirth *string `json:"dateOfBirth,omitempty"`
|
||||
ProfilePicURL *string `json:"profilePicUrl,omitempty"`
|
||||
AccountRole string `json:"accountRole"`
|
||||
AccountType string `json:"accountType"`
|
||||
LoyaltyStamps int `json:"loyaltyStamps"`
|
||||
ReferralCode string `json:"referralCode"`
|
||||
ReferralCodeUses int `json:"referralCodeUses"`
|
||||
LastLoginAt *string `json:"lastLoginAt,omitempty"`
|
||||
CreatedAt string `json:"createdAt"`
|
||||
UpdatedAt string `json:"updatedAt"`
|
||||
Notes *string `json:"notes,omitempty"`
|
||||
|
||||
// GDPR consent fields
|
||||
PrivacyPolicyConsent bool `json:"privacyPolicyConsent"`
|
||||
PolicyConsentUpdatedAt *string `json:"policyConsentUpdatedAt,omitempty"`
|
||||
DataRetentionConsent bool `json:"dataRetentionConsent"`
|
||||
DataConsentUpdatedAt *string `json:"dataConsentUpdatedAt,omitempty"`
|
||||
|
||||
// Social logins
|
||||
SocialLogins []SocialLogin `json:"socialLogins,omitempty"`
|
||||
}
|
||||
|
||||
type SocialLogin struct {
|
||||
Provider string `json:"provider"`
|
||||
CreatedAt string `json:"createdAt"`
|
||||
}
|
||||
|
||||
type UserListItem struct {
|
||||
ID string `json:"id"`
|
||||
FullName string `json:"fullName"`
|
||||
Email *string `json:"email,omitempty"`
|
||||
Phone *string `json:"phone,omitempty"`
|
||||
}
|
||||
|
||||
type UserListResponse struct {
|
||||
Users []UserListItem `json:"users"`
|
||||
Total int `json:"total"`
|
||||
Page int `json:"page"`
|
||||
PerPage int `json:"perPage"`
|
||||
TotalPages int `json:"totalPages"`
|
||||
}
|
||||
|
||||
// GET /api/user/profile
|
||||
func GetProfileHandler(w http.ResponseWriter, r *http.Request) {
|
||||
userID, ok := mw.GetUserID(r.Context())
|
||||
@@ -209,3 +262,216 @@ func UpdateProfileHandler(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}
|
||||
|
||||
// GET /api/admin/users/{id}
|
||||
func GetAdminUserHandler(w http.ResponseWriter, r *http.Request) {
|
||||
userID := chi.URLParam(r, "id")
|
||||
if userID == "" {
|
||||
http.Error(w, "User ID is required", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// Fetch user details
|
||||
var user AdminUserDetail
|
||||
err := db.DB.QueryRow(r.Context(), `
|
||||
SELECT
|
||||
id, email, n_first_name, n_last_name, fn, phone,
|
||||
date_of_birth::text, profile_pic_url,
|
||||
account_role, account_type, loyalty_stamps, referral_code,
|
||||
last_login_at::text, created_at::text, updated_at::text, notes,
|
||||
privacy_policy_and_terms_consent,
|
||||
policy_consent_updated_at::text,
|
||||
data_retention_consent,
|
||||
data_consent_updated_at::text
|
||||
FROM users
|
||||
WHERE id = $1
|
||||
`, userID).Scan(
|
||||
&user.ID, &user.Email, &user.FirstName, &user.LastName, &user.FullName,
|
||||
&user.Phone, &user.DateOfBirth, &user.ProfilePicURL,
|
||||
&user.AccountRole, &user.AccountType, &user.LoyaltyStamps, &user.ReferralCode,
|
||||
&user.LastLoginAt, &user.CreatedAt, &user.UpdatedAt, &user.Notes,
|
||||
&user.PrivacyPolicyConsent, &user.PolicyConsentUpdatedAt,
|
||||
&user.DataRetentionConsent, &user.DataConsentUpdatedAt,
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
http.Error(w, "User not found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
log.Printf("Failed to fetch user %s: %v", userID, err)
|
||||
http.Error(w, "Internal server error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// Fetch referral code uses count
|
||||
err = db.DB.QueryRow(r.Context(), `
|
||||
SELECT COUNT(*)
|
||||
FROM user_referrals
|
||||
WHERE referrer_id = $1 AND claimed_booking_id IS NOT NULL
|
||||
`, userID).Scan(&user.ReferralCodeUses)
|
||||
if err != nil {
|
||||
log.Printf("Failed to fetch referral code uses for user %s: %v", userID, err)
|
||||
user.ReferralCodeUses = 0
|
||||
}
|
||||
|
||||
// Fetch social logins
|
||||
socialRows, err := db.DB.Query(r.Context(), `
|
||||
SELECT provider, created_at::text
|
||||
FROM user_social_logins
|
||||
WHERE user_id = $1
|
||||
ORDER BY created_at ASC
|
||||
`, userID)
|
||||
if err != nil {
|
||||
log.Printf("Failed to fetch social logins for user %s: %v", userID, err)
|
||||
} else {
|
||||
defer socialRows.Close()
|
||||
|
||||
var socialLogins []SocialLogin
|
||||
for socialRows.Next() {
|
||||
var sl SocialLogin
|
||||
if err := socialRows.Scan(&sl.Provider, &sl.CreatedAt); err != nil {
|
||||
log.Printf("Failed to scan social login for user %s: %v", userID, err)
|
||||
continue
|
||||
}
|
||||
socialLogins = append(socialLogins, sl)
|
||||
}
|
||||
|
||||
if len(socialLogins) > 0 {
|
||||
user.SocialLogins = socialLogins
|
||||
}
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
if err := json.NewEncoder(w).Encode(user); err != nil {
|
||||
log.Printf("Failed to encode user response: %v", err)
|
||||
http.Error(w, "Internal server error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// GET /api/admin/users
|
||||
func ListAdminUsersHandler(w http.ResponseWriter, r *http.Request) {
|
||||
// Parse query parameters
|
||||
query := r.URL.Query()
|
||||
searchTerm := query.Get("q")
|
||||
|
||||
// Pagination parameters
|
||||
page := 1
|
||||
perPage := 10
|
||||
|
||||
if pageStr := query.Get("page"); pageStr != "" {
|
||||
if p, err := strconv.Atoi(pageStr); err == nil && p > 0 {
|
||||
page = p
|
||||
}
|
||||
}
|
||||
|
||||
if perPageStr := query.Get("per_page"); perPageStr != "" {
|
||||
if pp, err := strconv.Atoi(perPageStr); err == nil && pp > 0 && pp <= 100 {
|
||||
perPage = pp
|
||||
}
|
||||
}
|
||||
|
||||
offset := (page - 1) * perPage
|
||||
|
||||
// Build query based on whether search is provided
|
||||
var countQuery string
|
||||
var listQuery string
|
||||
var countArgs []interface{}
|
||||
var listArgs []interface{}
|
||||
|
||||
if searchTerm != "" {
|
||||
// Search in name, email, or phone
|
||||
searchPattern := "%" + searchTerm + "%"
|
||||
|
||||
countQuery = `
|
||||
SELECT COUNT(*)
|
||||
FROM users
|
||||
WHERE fn ILIKE $1
|
||||
OR email ILIKE $1
|
||||
OR phone ILIKE $1
|
||||
`
|
||||
countArgs = []interface{}{searchPattern}
|
||||
|
||||
listQuery = `
|
||||
SELECT id, fn, email, phone
|
||||
FROM users
|
||||
WHERE fn ILIKE $1
|
||||
OR email ILIKE $1
|
||||
OR phone ILIKE $1
|
||||
ORDER BY created_at DESC
|
||||
LIMIT $2 OFFSET $3
|
||||
`
|
||||
listArgs = []interface{}{searchPattern, perPage, offset}
|
||||
} else {
|
||||
// No search - get all users
|
||||
countQuery = `SELECT COUNT(*) FROM users`
|
||||
countArgs = []interface{}{}
|
||||
|
||||
listQuery = `
|
||||
SELECT id, fn, email, phone
|
||||
FROM users
|
||||
ORDER BY created_at DESC
|
||||
LIMIT $1 OFFSET $2
|
||||
`
|
||||
listArgs = []interface{}{perPage, offset}
|
||||
}
|
||||
|
||||
// Get total count
|
||||
var total int
|
||||
err := db.DB.QueryRow(r.Context(), countQuery, countArgs...).Scan(&total)
|
||||
if err != nil {
|
||||
log.Printf("Failed to count users: %v", err)
|
||||
http.Error(w, "Internal server error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// Get users list
|
||||
rows, err := db.DB.Query(r.Context(), listQuery, listArgs...)
|
||||
if err != nil {
|
||||
log.Printf("Failed to fetch users: %v", err)
|
||||
http.Error(w, "Internal server error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var users []UserListItem
|
||||
for rows.Next() {
|
||||
var user UserListItem
|
||||
err := rows.Scan(&user.ID, &user.FullName, &user.Email, &user.Phone)
|
||||
if err != nil {
|
||||
log.Printf("Failed to scan user row: %v", err)
|
||||
http.Error(w, "Internal server error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
users = append(users, user)
|
||||
}
|
||||
|
||||
// Handle empty results
|
||||
if users == nil {
|
||||
users = []UserListItem{}
|
||||
}
|
||||
|
||||
// Calculate total pages
|
||||
totalPages := (total + perPage - 1) / perPage
|
||||
if totalPages == 0 {
|
||||
totalPages = 1
|
||||
}
|
||||
|
||||
response := UserListResponse{
|
||||
Users: users,
|
||||
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 users response: %v", err)
|
||||
http.Error(w, "Internal server error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
@@ -132,6 +132,12 @@ func main() {
|
||||
r.Get("/{id}", bookings.GetAdminBookingHandler)
|
||||
r.Put("/{id}/progress", bookings.ProgressBookingHandler)
|
||||
r.Post("/{id}/confirm", bookings.ConfirmBookingHandler)
|
||||
r.Post("/{id}/cancel", bookings.ConfirmBookingHandler)
|
||||
})
|
||||
|
||||
r.Route("/admin/users", func(r chi.Router) {
|
||||
r.Get("/", user.ListAdminUsersHandler)
|
||||
r.Get("/{id}", user.GetAdminUserHandler)
|
||||
})
|
||||
|
||||
// --- Admin Notifications ---
|
||||
|
||||
Reference in New Issue
Block a user