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
+266
View File
@@ -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
}
}