Files
MyClub/internal/controllers/engagement_controller.go
T
Tomas Dvorak b9cea0cd77 dev day #79
2025-11-02 01:04:02 +01:00

271 lines
12 KiB
Go

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})
}