package user import ( "database/sql" "encoding/json" "errors" "fmt" "log" "math" "net/http" "time" "github.com/go-chi/chi/v5" "crussell/db" "crussell/internal/validators" ) type CustomerRelationship struct { TotalSpend float64 `json:"totalSpend"` TotalSaved float64 `json:"totalSaved"` TotalTips float64 `json:"totalTips"` TotalVisits int `json:"totalVisits"` CustomerFor string `json:"customerFor"` FirstVisitDate *string `json:"firstVisitDate,omitempty"` LastVisitDate *string `json:"lastVisitDate,omitempty"` TopServices []TopService `json:"topServices"` } type TopService struct { Name string `json:"name"` Count int `json:"count"` } // GET /api/admin/users/{id}/relationship func GetCustomerRelationshipHandler(w http.ResponseWriter, r *http.Request) { userID := chi.URLParam(r, "id") if userID == "" || !validators.IsValidID(userID) { http.Error(w, "user not found", http.StatusNotFound) return } var result CustomerRelationship var firstVisit sql.NullTime var lastVisit sql.NullTime var exists bool err := db.DB.QueryRow(r.Context(), `SELECT EXISTS(SELECT 1 FROM users WHERE id = $1)`, userID).Scan(&exists) if err != nil { log.Printf("Failed to check user existence for %s: %v", userID, err) http.Error(w, "Internal server error", http.StatusInternalServerError) return } if !exists { http.Error(w, "user not found", http.StatusNotFound) return } err = db.DB.QueryRow(r.Context(), ` SELECT COALESCE(SUM(p.amount) FILTER (WHERE p.status = 'completed' AND p.payment_type IN ('full','partial','balance','deposit') AND p.payment_method != 'discount'), 0), COALESCE(SUM(p.amount) FILTER (WHERE p.status = 'completed' AND p.payment_type IN ('full','partial','balance','deposit') AND p.payment_method = 'discount'), 0), COALESCE(SUM(p.amount) FILTER (WHERE p.status = 'completed' AND p.payment_type = 'tip'), 0), COUNT(DISTINCT b.id) FILTER (WHERE b.status = 'completed'), MIN(b.start_time) FILTER (WHERE b.status = 'completed'), MAX(b.start_time) FILTER (WHERE b.status = 'completed') FROM bookings b LEFT JOIN payments p ON p.booking_id = b.id WHERE b.user_id = $1 `, userID).Scan(&result.TotalSpend, &result.TotalSaved, &result.TotalTips, &result.TotalVisits, &firstVisit, &lastVisit) if err != nil && !errors.Is(err, sql.ErrNoRows) { log.Printf("Failed to get customer relationship data for user %s: %v", userID, err) } if firstVisit.Valid { firstVisitStr := firstVisit.Time.Format("2006-01-02T15:04:05Z07:00") result.FirstVisitDate = &firstVisitStr days := int(time.Since(firstVisit.Time).Hours() / 24) switch { case days < 7: result.CustomerFor = formatPlural(days, "day") case days < 30: result.CustomerFor = formatPlural(int(math.Round(float64(days)/7)), "week") case days < 365: result.CustomerFor = formatPlural(int(math.Round(float64(days)/30)), "month") default: result.CustomerFor = formatPlural(int(math.Round(float64(days)/365.25)), "year") } } if lastVisit.Valid { lastVisitStr := lastVisit.Time.Format("2006-01-02T15:04:05Z07:00") result.LastVisitDate = &lastVisitStr } rows, err := db.DB.Query(r.Context(), ` SELECT name, cnt FROM ( SELECT s.name, COUNT(*) as count, COUNT(*) as cnt FROM booking_services bsvc JOIN bookings b ON bsvc.booking_id = b.id JOIN services s ON bsvc.service_id = s.id WHERE b.user_id = $1 AND b.status = 'completed' GROUP BY s.name UNION ALL SELECT cs.name, COUNT(*) as count, COUNT(*) as cnt FROM booking_custom_services bcs JOIN bookings b ON bcs.booking_id = b.id JOIN custom_services cs ON bcs.custom_service_id = cs.id WHERE b.user_id = $1 AND b.status = 'completed' GROUP BY cs.name ) combined ORDER BY count DESC LIMIT 5 `, userID) if err != nil && !errors.Is(err, sql.ErrNoRows) { log.Printf("Failed to get top services for user %s: %v", userID, err) } else { defer rows.Close() for rows.Next() { var ts TopService if err := rows.Scan(&ts.Name, &ts.Count); err != nil { log.Printf("Failed to scan top service: %v", err) continue } result.TopServices = append(result.TopServices, ts) } } if result.TopServices == nil { result.TopServices = []TopService{} } w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) if err := json.NewEncoder(w).Encode(result); err != nil { log.Printf("Failed to encode customer relationship response: %v", err) } } func formatPlural(n int, unit string) string { if n == 1 { return fmt.Sprintf("1 %s", unit) } return fmt.Sprintf("%d %ss", n, unit) }