mirror of
https://github.com/Dvorinka/MyClubServer.git
synced 2026-06-04 02:32:57 +00:00
dev day #79
This commit is contained in:
@@ -0,0 +1,270 @@
|
||||
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})
|
||||
}
|
||||
Reference in New Issue
Block a user