package controllers import ( "net/http" "strings" "github.com/gin-gonic/gin" "gorm.io/gorm" "fotbal-club/internal/models" "fotbal-club/internal/services" ) type EngagementController struct{ DB *gorm.DB } func NewEngagementController(db *gorm.DB) *EngagementController { return &EngagementController{DB: db} } // GET /api/v1/engagement/profile (auth) func (ec *EngagementController) GetProfile(c *gin.Context) { uid, _ := c.Get("userID") userID := uid.(uint) svc := services.NewEngagementService(ec.DB) up, err := svc.EnsureProfile(userID) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error":"Failed to load profile"}); return } // Achievements count var achCount int64 _ = ec.DB.Model(&models.UserAchievement{}).Where("user_id = ?", userID).Count(&achCount).Error c.JSON(http.StatusOK, gin.H{ "user_id": userID, "points": up.Points, "level": up.Level, "xp": up.XP, "avatar_url": up.AvatarURL, "animated_avatar_url": up.AnimatedAvatarURL, "achievements": achCount, }) } // PATCH /api/v1/engagement/avatar (auth) func (ec *EngagementController) PatchAvatar(c *gin.Context) { uid, _ := c.Get("userID") userID := uid.(uint) var body struct{ AvatarURL *string `json:"avatar_url"`; AnimatedAvatarURL *string `json:"animated_avatar_url"` } if err := c.ShouldBindJSON(&body); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error":"Invalid payload"}); return } updates := map[string]interface{}{} if body.AvatarURL != nil { updates["avatar_url"] = strings.TrimSpace(*body.AvatarURL) } if body.AnimatedAvatarURL != nil { updates["animated_avatar_url"] = strings.TrimSpace(*body.AnimatedAvatarURL) } if len(updates) == 0 { c.JSON(http.StatusBadRequest, gin.H{"error":"No changes"}); return } if err := ec.DB.Model(&models.UserProfile{}).Where("user_id = ?", userID).Updates(updates).Error; err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error":"Failed to update avatar"}); return } c.JSON(http.StatusOK, gin.H{"ok": true}) } // GET /api/v1/engagement/rewards (public) func (ec *EngagementController) GetRewards(c *gin.Context) { var items []models.RewardItem q := ec.DB.Where("active = ?", true) if err := q.Order("created_at DESC").Find(&items).Error; err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error":"Failed to load rewards"}); return } c.JSON(http.StatusOK, items) } // POST /api/v1/engagement/redeem (auth) func (ec *EngagementController) Redeem(c *gin.Context) { uid, _ := c.Get("userID") userID := uid.(uint) var body struct{ RewardID uint `json:"reward_id"` } if err := c.ShouldBindJSON(&body); err != nil || body.RewardID == 0 { c.JSON(http.StatusBadRequest, gin.H{"error":"Invalid payload"}); return } var item models.RewardItem if err := ec.DB.First(&item, body.RewardID).Error; err != nil { c.JSON(http.StatusNotFound, gin.H{"error":"Reward not found"}); return } if !item.Active { c.JSON(http.StatusBadRequest, gin.H{"error":"Reward is not active"}); return } if item.Stock == 0 { c.JSON(http.StatusBadRequest, gin.H{"error":"Out of stock"}); return } // Ensure profile svc := services.NewEngagementService(ec.DB) up, err := svc.EnsureProfile(userID) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error":"Failed to load profile"}); return } if up.Points < item.CostPoints { c.JSON(http.StatusBadRequest, gin.H{"error":"Nedostatek bodů"}); return } // Transaction: deduct points, reduce stock, create redemption tx := ec.DB.Begin() if err := tx.Model(&models.UserProfile{}).Where("user_id = ? AND points >= ?", userID, item.CostPoints).UpdateColumn("points", gorm.Expr("points - ?", item.CostPoints)).Error; err != nil { tx.Rollback(); c.JSON(http.StatusInternalServerError, gin.H{"error":"Failed to deduct points"}); return } if item.Stock > 0 { if err := tx.Model(&models.RewardItem{}).Where("id = ? AND stock > 0", item.ID).UpdateColumn("stock", gorm.Expr("stock - 1")).Error; err != nil { tx.Rollback(); c.JSON(http.StatusInternalServerError, gin.H{"error":"Failed to update stock"}); return } } red := models.RewardRedemption{ UserID: userID, RewardID: item.ID, Status: "approved" } if strings.HasPrefix(item.Type, "merch_") || item.Type == "custom" { red.Status = "pending" } if err := tx.Create(&red).Error; err != nil { tx.Rollback(); c.JSON(http.StatusInternalServerError, gin.H{"error":"Failed to create redemption"}); return } // If avatar reward, update profile immediately if item.Type == "avatar_static" { _ = tx.Model(&models.UserProfile{}).Where("user_id = ?", userID).Update("avatar_url", item.ImageURL).Error } if item.Type == "avatar_animated" { _ = tx.Model(&models.UserProfile{}).Where("user_id = ?", userID).Update("animated_avatar_url", item.ImageURL).Error } if err := tx.Commit().Error; err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error":"Failed to commit redemption"}); return } c.JSON(http.StatusOK, gin.H{"ok": true, "status": red.Status}) } // GET /api/v1/engagement/achievements (auth) func (ec *EngagementController) GetAchievements(c *gin.Context) { uid, _ := c.Get("userID") userID := uid.(uint) // Ensure defaults and award any newly satisfied achievements svc := services.NewEngagementService(ec.DB) _ = svc.CheckAndAwardAchievements(userID) // Load active achievement definitions var defs []models.Achievement _ = ec.DB.Where("active = ?", true).Order("id ASC").Find(&defs).Error // Load user's completed achievements var userAch []models.UserAchievement _ = ec.DB.Where("user_id = ?", userID).Find(&userAch).Error achieved := map[uint]models.UserAchievement{} for _, ua := range userAch { achieved[ua.AchievementID] = ua } // Counters for progress var commentCount int64 _ = ec.DB.Model(&models.Comment{}).Where("user_id = ?", userID).Count(&commentCount).Error var voteCount int64 _ = ec.DB.Model(&models.PollVote{}).Where("user_id = ?", userID).Count(&voteCount).Error hasNewsletter := false _ = ec.DB.Model(&models.NewsletterSubscription{}).Select("1").Where("LOWER(email) = (SELECT LOWER(email) FROM users WHERE id = ?) AND is_active = ?", userID, true).Limit(1).Scan(&hasNewsletter).Error // Build response items := make([]gin.H, 0, len(defs)) for _, d := range defs { if ua, ok := achieved[d.ID]; ok { items = append(items, gin.H{ "id": d.ID, "code": d.Code, "title": d.Title, "description": d.Description, "points": d.Points, "xp": d.XP, "icon": d.Icon, "achieved": true, "achieved_at": ua.CreatedAt, }) } else { items = append(items, gin.H{ "id": d.ID, "code": d.Code, "title": d.Title, "description": d.Description, "points": d.Points, "xp": d.XP, "icon": d.Icon, "achieved": false, }) } } c.JSON(http.StatusOK, gin.H{ "achievements": items, "counters": gin.H{ "comments": commentCount, "votes": voteCount, "newsletter": hasNewsletter, }, }) } // Admin: list rewards // GET /api/v1/admin/engagement/rewards func (ec *EngagementController) AdminListRewards(c *gin.Context) { var items []models.RewardItem q := ec.DB.Model(&models.RewardItem{}) if v := strings.TrimSpace(c.Query("active")); v != "" { if v == "true" || v == "1" { q = q.Where("active = ?", true) } if v == "false" || v == "0" { q = q.Where("active = ?", false) } } if err := q.Order("created_at DESC").Find(&items).Error; err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error":"Failed to load rewards"}); return } c.JSON(http.StatusOK, gin.H{"items": items}) } // Admin: create reward // POST /api/v1/admin/engagement/rewards func (ec *EngagementController) AdminCreateReward(c *gin.Context) { var body struct{ Name string `json:"name"` Type string `json:"type"` CostPoints int64 `json:"cost_points"` ImageURL string `json:"image_url"` Stock int `json:"stock"` Active *bool `json:"active"` Metadata map[string]interface{} `json:"metadata"` } if err := c.ShouldBindJSON(&body); err != nil || strings.TrimSpace(body.Name) == "" || strings.TrimSpace(body.Type) == "" { c.JSON(http.StatusBadRequest, gin.H{"error":"Invalid payload"}); return } item := models.RewardItem{ Name: strings.TrimSpace(body.Name), Type: strings.TrimSpace(body.Type), CostPoints: body.CostPoints, ImageURL: strings.TrimSpace(body.ImageURL), Stock: body.Stock, Active: true } if body.Active != nil { item.Active = *body.Active } if body.Metadata != nil { item.Metadata = body.Metadata } if err := ec.DB.Create(&item).Error; err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error":"Failed to create reward"}); return } c.JSON(http.StatusOK, item) } // Admin: update reward // PUT /api/v1/admin/engagement/rewards/:id func (ec *EngagementController) AdminUpdateReward(c *gin.Context) { id := c.Param("id") var body struct{ Name *string `json:"name"` Type *string `json:"type"` CostPoints *int64 `json:"cost_points"` ImageURL *string `json:"image_url"` Stock *int `json:"stock"` Active *bool `json:"active"` Metadata map[string]interface{} `json:"metadata"` } if err := c.ShouldBindJSON(&body); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error":"Invalid payload"}); return } updates := map[string]interface{}{} if body.Name != nil { updates["name"] = strings.TrimSpace(*body.Name) } if body.Type != nil { updates["type"] = strings.TrimSpace(*body.Type) } if body.CostPoints != nil { updates["cost_points"] = *body.CostPoints } if body.ImageURL != nil { updates["image_url"] = strings.TrimSpace(*body.ImageURL) } if body.Stock != nil { updates["stock"] = *body.Stock } if body.Active != nil { updates["active"] = *body.Active } if body.Metadata != nil { updates["metadata"] = body.Metadata } if len(updates) == 0 { c.JSON(http.StatusBadRequest, gin.H{"error":"No changes"}); return } if err := ec.DB.Model(&models.RewardItem{}).Where("id = ?", id).Updates(updates).Error; err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error":"Failed to update reward"}); return } c.JSON(http.StatusOK, gin.H{"ok": true}) } // Admin: delete reward // DELETE /api/v1/admin/engagement/rewards/:id func (ec *EngagementController) AdminDeleteReward(c *gin.Context) { id := c.Param("id") if err := ec.DB.Delete(&models.RewardItem{}, "id = ?", id).Error; err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error":"Failed to delete reward"}); return } c.JSON(http.StatusOK, gin.H{"ok": true}) } // Admin: list redemptions // GET /api/v1/admin/engagement/redemptions func (ec *EngagementController) AdminListRedemptions(c *gin.Context) { var items []models.RewardRedemption q := ec.DB.Model(&models.RewardRedemption{}) if v := strings.TrimSpace(c.Query("status")); v != "" { q = q.Where("status = ?", v) } if err := q.Order("created_at DESC").Find(&items).Error; err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error":"Failed to load redemptions"}); return } c.JSON(http.StatusOK, gin.H{"items": items}) } // Admin: update redemption status (approve/reject/fulfill) // PATCH /api/v1/admin/engagement/redemptions/:id func (ec *EngagementController) AdminUpdateRedemptionStatus(c *gin.Context) { id := c.Param("id") var body struct{ Action string `json:"action"` } if err := c.ShouldBindJSON(&body); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error":"Invalid payload"}); return } action := strings.ToLower(strings.TrimSpace(body.Action)) var newStatus string switch action { case "approve": newStatus = "approved" case "reject": newStatus = "rejected" case "fulfill": newStatus = "fulfilled" default: c.JSON(http.StatusBadRequest, gin.H{"error":"Invalid action"}); return } if err := ec.DB.Model(&models.RewardRedemption{}).Where("id = ?", id).Update("status", newStatus).Error; err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error":"Failed to update status"}); return } c.JSON(http.StatusOK, gin.H{"ok": true, "status": newStatus}) }