mirror of
https://github.com/Dvorinka/MyClubServer.git
synced 2026-06-04 02:32:57 +00:00
271 lines
12 KiB
Go
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})
|
|
}
|