Split admin dashboard, implement user and booking search
This commit is contained in:
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user