mirror of
https://github.com/Dvorinka/MyClubServer.git
synced 2026-06-04 02:32:57 +00:00
724 lines
21 KiB
Go
724 lines
21 KiB
Go
package controllers
|
|
|
|
import (
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"fmt"
|
|
"net/http"
|
|
"strconv"
|
|
"time"
|
|
|
|
"fotbal-club/internal/models"
|
|
"fotbal-club/internal/services"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
"gorm.io/gorm"
|
|
)
|
|
|
|
type PollController struct {
|
|
DB *gorm.DB
|
|
}
|
|
|
|
func NewPollController(db *gorm.DB) *PollController {
|
|
return &PollController{DB: db}
|
|
}
|
|
|
|
// GetPolls returns list of polls (admin or public)
|
|
func (pc *PollController) GetPolls(c *gin.Context) {
|
|
var polls []models.Poll
|
|
query := pc.DB.Preload("Options").Preload("Category").Preload("Creator").
|
|
Preload("RelatedArticle").Preload("RelatedEvent")
|
|
|
|
// Check if admin request
|
|
if c.GetBool("isAdmin") {
|
|
// Admin sees all polls
|
|
status := c.Query("status")
|
|
if status != "" {
|
|
query = query.Where("status = ?", status)
|
|
}
|
|
} else {
|
|
// Public sees only active polls
|
|
query = query.Where("status = ?", "active")
|
|
now := time.Now()
|
|
query = query.Where("(start_date IS NULL OR start_date <= ?) AND (end_date IS NULL OR end_date >= ?)", now, now)
|
|
}
|
|
|
|
// Featured filter
|
|
if c.Query("featured") == "true" {
|
|
query = query.Where("featured = ?", true)
|
|
}
|
|
|
|
// Filter by relationships
|
|
if articleID := c.Query("article_id"); articleID != "" {
|
|
query = query.Where("related_article_id = ?", articleID)
|
|
}
|
|
if eventID := c.Query("event_id"); eventID != "" {
|
|
query = query.Where("related_event_id = ?", eventID)
|
|
}
|
|
if videoURL := c.Query("video_url"); videoURL != "" {
|
|
query = query.Where("related_video_url = ?", videoURL)
|
|
}
|
|
|
|
// Order
|
|
query = query.Order("created_at DESC")
|
|
|
|
if err := query.Find(&polls).Error; err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch polls"})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, polls)
|
|
}
|
|
|
|
// GetPoll returns a single poll by ID
|
|
func (pc *PollController) GetPoll(c *gin.Context) {
|
|
id := c.Param("id")
|
|
|
|
var poll models.Poll
|
|
query := pc.DB.Preload("Options", func(db *gorm.DB) *gorm.DB {
|
|
return db.Order("display_order ASC, id ASC")
|
|
}).Preload("Options.Player").Preload("Category")
|
|
|
|
if err := query.First(&poll, id).Error; err != nil {
|
|
if err == gorm.ErrRecordNotFound {
|
|
c.JSON(http.StatusNotFound, gin.H{"error": "Poll not found"})
|
|
return
|
|
}
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch poll"})
|
|
return
|
|
}
|
|
|
|
// Check if user has voted
|
|
hasVoted := false
|
|
if userID, exists := c.Get("userID"); exists && userID != nil {
|
|
var count int64
|
|
pc.DB.Model(&models.PollVote{}).Where("poll_id = ? AND user_id = ?", poll.ID, userID).Count(&count)
|
|
hasVoted = count > 0
|
|
} else {
|
|
// Check by IP hash or session token
|
|
ipHash := pc.hashIP(c.ClientIP())
|
|
sessionToken := c.GetHeader("X-Session-Token")
|
|
if sessionToken == "" {
|
|
sessionToken = c.Query("session_token")
|
|
}
|
|
|
|
var count int64
|
|
query := pc.DB.Model(&models.PollVote{}).Where("poll_id = ?", poll.ID)
|
|
if sessionToken != "" {
|
|
query = query.Where("session_token = ?", sessionToken)
|
|
} else {
|
|
query = query.Where("ip_hash = ?", ipHash)
|
|
}
|
|
query.Count(&count)
|
|
hasVoted = count > 0
|
|
}
|
|
|
|
// Add metadata
|
|
response := gin.H{
|
|
"poll": poll,
|
|
"has_voted": hasVoted,
|
|
"is_active": poll.IsActive(),
|
|
"can_show_results": poll.CanShowResults(hasVoted),
|
|
}
|
|
|
|
c.JSON(http.StatusOK, response)
|
|
}
|
|
|
|
// CreatePoll creates a new poll (admin only)
|
|
func (pc *PollController) CreatePoll(c *gin.Context) {
|
|
var input struct {
|
|
Title string `json:"title" binding:"required"`
|
|
Description string `json:"description"`
|
|
Type string `json:"type"`
|
|
Style string `json:"style"`
|
|
Status string `json:"status"`
|
|
StartDate *time.Time `json:"start_date"`
|
|
EndDate *time.Time `json:"end_date"`
|
|
AllowMultiple bool `json:"allow_multiple"`
|
|
MaxChoices int `json:"max_choices"`
|
|
ShowResults string `json:"show_results"`
|
|
RequireAuth bool `json:"require_auth"`
|
|
AllowGuestVote bool `json:"allow_guest_vote"`
|
|
Featured bool `json:"featured"`
|
|
CategoryID *uint `json:"category_id"`
|
|
RelatedMatchID *uint `json:"related_match_id"`
|
|
RelatedArticleID *uint `json:"related_article_id"`
|
|
RelatedEventID *uint `json:"related_event_id"`
|
|
RelatedVideoURL string `json:"related_video_url"`
|
|
ImageURL string `json:"image_url"`
|
|
Options []struct {
|
|
Text string `json:"text" binding:"required"`
|
|
Description string `json:"description"`
|
|
ImageURL string `json:"image_url"`
|
|
DisplayOrder int `json:"display_order"`
|
|
PlayerID *uint `json:"player_id"`
|
|
} `json:"options" binding:"required,min=2"`
|
|
}
|
|
|
|
if err := c.ShouldBindJSON(&input); err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
// Get user ID
|
|
userID, _ := c.Get("userID")
|
|
|
|
// Create poll
|
|
poll := models.Poll{
|
|
Title: input.Title,
|
|
Description: input.Description,
|
|
Type: input.Type,
|
|
Style: input.Style,
|
|
Status: input.Status,
|
|
StartDate: input.StartDate,
|
|
EndDate: input.EndDate,
|
|
AllowMultiple: input.AllowMultiple,
|
|
MaxChoices: input.MaxChoices,
|
|
ShowResults: input.ShowResults,
|
|
RequireAuth: input.RequireAuth,
|
|
AllowGuestVote: input.AllowGuestVote,
|
|
Featured: input.Featured,
|
|
CategoryID: input.CategoryID,
|
|
RelatedMatchID: input.RelatedMatchID,
|
|
RelatedArticleID: input.RelatedArticleID,
|
|
RelatedEventID: input.RelatedEventID,
|
|
RelatedVideoURL: input.RelatedVideoURL,
|
|
ImageURL: input.ImageURL,
|
|
CreatedBy: userID.(uint),
|
|
}
|
|
|
|
// Set defaults
|
|
if poll.Type == "" {
|
|
poll.Type = "single"
|
|
}
|
|
if poll.Style == "" {
|
|
poll.Style = "auto"
|
|
}
|
|
if poll.Status == "" {
|
|
poll.Status = "draft"
|
|
}
|
|
if poll.ShowResults == "" {
|
|
poll.ShowResults = "after_vote"
|
|
}
|
|
if poll.MaxChoices == 0 {
|
|
poll.MaxChoices = 1
|
|
}
|
|
|
|
// Start transaction
|
|
tx := pc.DB.Begin()
|
|
|
|
if err := tx.Create(&poll).Error; err != nil {
|
|
tx.Rollback()
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create poll"})
|
|
return
|
|
}
|
|
|
|
// Create options
|
|
for _, opt := range input.Options {
|
|
option := models.PollOption{
|
|
PollID: poll.ID,
|
|
Text: opt.Text,
|
|
Description: opt.Description,
|
|
ImageURL: opt.ImageURL,
|
|
DisplayOrder: opt.DisplayOrder,
|
|
PlayerID: opt.PlayerID,
|
|
}
|
|
if err := tx.Create(&option).Error; err != nil {
|
|
tx.Rollback()
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create poll option"})
|
|
return
|
|
}
|
|
}
|
|
|
|
tx.Commit()
|
|
|
|
// Reload with relations
|
|
pc.DB.Preload("Options").Preload("Category").First(&poll, poll.ID)
|
|
|
|
c.JSON(http.StatusCreated, poll)
|
|
}
|
|
|
|
// UpdatePoll updates an existing poll (admin only)
|
|
func (pc *PollController) UpdatePoll(c *gin.Context) {
|
|
id := c.Param("id")
|
|
|
|
var poll models.Poll
|
|
if err := pc.DB.First(&poll, id).Error; err != nil {
|
|
if err == gorm.ErrRecordNotFound {
|
|
c.JSON(http.StatusNotFound, gin.H{"error": "Poll not found"})
|
|
return
|
|
}
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch poll"})
|
|
return
|
|
}
|
|
|
|
var input struct {
|
|
Title *string `json:"title"`
|
|
Description *string `json:"description"`
|
|
Type *string `json:"type"`
|
|
Style *string `json:"style"`
|
|
Status *string `json:"status"`
|
|
StartDate *time.Time `json:"start_date"`
|
|
EndDate *time.Time `json:"end_date"`
|
|
AllowMultiple *bool `json:"allow_multiple"`
|
|
MaxChoices *int `json:"max_choices"`
|
|
ShowResults *string `json:"show_results"`
|
|
RequireAuth *bool `json:"require_auth"`
|
|
AllowGuestVote *bool `json:"allow_guest_vote"`
|
|
Featured *bool `json:"featured"`
|
|
CategoryID *uint `json:"category_id"`
|
|
RelatedMatchID *uint `json:"related_match_id"`
|
|
RelatedArticleID *uint `json:"related_article_id"`
|
|
RelatedEventID *uint `json:"related_event_id"`
|
|
RelatedVideoURL *string `json:"related_video_url"`
|
|
ImageURL *string `json:"image_url"`
|
|
}
|
|
|
|
if err := c.ShouldBindJSON(&input); err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
// Update fields
|
|
if input.Title != nil {
|
|
poll.Title = *input.Title
|
|
}
|
|
if input.Description != nil {
|
|
poll.Description = *input.Description
|
|
}
|
|
if input.Type != nil {
|
|
poll.Type = *input.Type
|
|
}
|
|
if input.Style != nil {
|
|
poll.Style = *input.Style
|
|
}
|
|
if input.Status != nil {
|
|
poll.Status = *input.Status
|
|
}
|
|
if input.StartDate != nil {
|
|
poll.StartDate = input.StartDate
|
|
}
|
|
if input.EndDate != nil {
|
|
poll.EndDate = input.EndDate
|
|
}
|
|
if input.AllowMultiple != nil {
|
|
poll.AllowMultiple = *input.AllowMultiple
|
|
}
|
|
if input.MaxChoices != nil {
|
|
poll.MaxChoices = *input.MaxChoices
|
|
}
|
|
if input.ShowResults != nil {
|
|
poll.ShowResults = *input.ShowResults
|
|
}
|
|
if input.RequireAuth != nil {
|
|
poll.RequireAuth = *input.RequireAuth
|
|
}
|
|
if input.AllowGuestVote != nil {
|
|
poll.AllowGuestVote = *input.AllowGuestVote
|
|
}
|
|
if input.Featured != nil {
|
|
poll.Featured = *input.Featured
|
|
}
|
|
if input.CategoryID != nil {
|
|
poll.CategoryID = input.CategoryID
|
|
}
|
|
// For relationships, directly set the values (including nil to unlink)
|
|
// GORM's Save will handle NULL values correctly
|
|
poll.RelatedMatchID = input.RelatedMatchID
|
|
poll.RelatedArticleID = input.RelatedArticleID
|
|
poll.RelatedEventID = input.RelatedEventID
|
|
if input.RelatedVideoURL != nil {
|
|
poll.RelatedVideoURL = *input.RelatedVideoURL
|
|
}
|
|
if input.ImageURL != nil {
|
|
poll.ImageURL = *input.ImageURL
|
|
}
|
|
|
|
if err := pc.DB.Save(&poll).Error; err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update poll"})
|
|
return
|
|
}
|
|
|
|
// Reload with relations
|
|
pc.DB.Preload("Options").Preload("Category").First(&poll, poll.ID)
|
|
|
|
c.JSON(http.StatusOK, poll)
|
|
}
|
|
|
|
// DeletePoll deletes a poll (admin only)
|
|
func (pc *PollController) DeletePoll(c *gin.Context) {
|
|
id := c.Param("id")
|
|
|
|
var poll models.Poll
|
|
if err := pc.DB.First(&poll, id).Error; err != nil {
|
|
if err == gorm.ErrRecordNotFound {
|
|
c.JSON(http.StatusNotFound, gin.H{"error": "Poll not found"})
|
|
return
|
|
}
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch poll"})
|
|
return
|
|
}
|
|
|
|
// Soft delete
|
|
if err := pc.DB.Delete(&poll).Error; err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete poll"})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{"message": "Poll deleted successfully"})
|
|
}
|
|
func (pc *PollController) Vote(c *gin.Context) {
|
|
id := c.Param("id")
|
|
|
|
var input struct {
|
|
OptionIDs []uint `json:"option_ids" binding:"required,min=1"`
|
|
SessionToken string `json:"session_token"`
|
|
VoterName string `json:"voter_name"`
|
|
VoterEmail string `json:"voter_email"`
|
|
}
|
|
|
|
if err := c.ShouldBindJSON(&input); err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid input: " + err.Error()})
|
|
return
|
|
}
|
|
|
|
// Get poll
|
|
var poll models.Poll
|
|
if err := pc.DB.Preload("Options").First(&poll, id).Error; err != nil {
|
|
if err == gorm.ErrRecordNotFound {
|
|
c.JSON(http.StatusNotFound, gin.H{"error": "Poll not found"})
|
|
return
|
|
}
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch poll"})
|
|
return
|
|
}
|
|
|
|
// Check if poll is active
|
|
if !poll.IsActive() {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "Poll is not currently accepting votes"})
|
|
return
|
|
}
|
|
|
|
// Check authentication requirement
|
|
userID, hasUser := c.Get("userID")
|
|
if poll.RequireAuth && !hasUser {
|
|
c.JSON(http.StatusUnauthorized, gin.H{"error": "Login required to vote"})
|
|
return
|
|
}
|
|
|
|
if !poll.AllowGuestVote && !hasUser {
|
|
c.JSON(http.StatusUnauthorized, gin.H{"error": "Guest voting not allowed"})
|
|
return
|
|
}
|
|
|
|
// Check multiple choice limits
|
|
if !poll.AllowMultiple && len(input.OptionIDs) > 1 {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "Only one choice allowed"})
|
|
return
|
|
}
|
|
|
|
if poll.AllowMultiple && len(input.OptionIDs) > poll.MaxChoices {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("Maximum %d choices allowed", poll.MaxChoices)})
|
|
return
|
|
}
|
|
|
|
// Normalize voter info. Allow guests to optionally provide name/email.
|
|
// If authenticated and no voter_name/email provided, fallback to user's profile.
|
|
var derivedName string
|
|
var derivedEmail string
|
|
if hasUser {
|
|
if uID, ok := userID.(uint); ok {
|
|
var u models.User
|
|
if err := pc.DB.First(&u, uID).Error; err == nil {
|
|
if u.FirstName != "" || u.LastName != "" {
|
|
derivedName = fmt.Sprintf("%s %s", u.FirstName, u.LastName)
|
|
}
|
|
derivedEmail = u.Email
|
|
}
|
|
}
|
|
}
|
|
// Final values to persist
|
|
voterName := input.VoterName
|
|
voterEmail := input.VoterEmail
|
|
if voterName == "" && derivedName != "" {
|
|
voterName = derivedName
|
|
}
|
|
if voterEmail == "" && derivedEmail != "" {
|
|
voterEmail = derivedEmail
|
|
}
|
|
|
|
// Check if already voted
|
|
ipHash := pc.hashIP(c.ClientIP())
|
|
sessionToken := input.SessionToken
|
|
if sessionToken == "" {
|
|
sessionToken = c.GetHeader("X-Session-Token")
|
|
}
|
|
|
|
var existingVoteCount int64
|
|
query := pc.DB.Model(&models.PollVote{}).Where("poll_id = ?", poll.ID)
|
|
|
|
if hasUser {
|
|
query = query.Where("user_id = ?", userID)
|
|
} else if sessionToken != "" {
|
|
query = query.Where("session_token = ?", sessionToken)
|
|
} else {
|
|
query = query.Where("ip_hash = ?", ipHash)
|
|
}
|
|
|
|
query.Count(&existingVoteCount)
|
|
|
|
if existingVoteCount > 0 {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "You have already voted in this poll"})
|
|
return
|
|
}
|
|
|
|
// Validate option IDs belong to this poll
|
|
validOptions := make(map[uint]bool)
|
|
for _, opt := range poll.Options {
|
|
validOptions[opt.ID] = true
|
|
}
|
|
|
|
for _, optID := range input.OptionIDs {
|
|
if !validOptions[optID] {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid option ID"})
|
|
return
|
|
}
|
|
}
|
|
|
|
// Start transaction
|
|
tx := pc.DB.Begin()
|
|
|
|
// Create votes
|
|
userAgent := c.Request.UserAgent()
|
|
for _, optionID := range input.OptionIDs {
|
|
vote := models.PollVote{
|
|
PollID: poll.ID,
|
|
OptionID: optionID,
|
|
IPHash: ipHash,
|
|
UserAgent: userAgent,
|
|
SessionToken: sessionToken,
|
|
VoterName: voterName,
|
|
VoterEmail: voterEmail,
|
|
}
|
|
|
|
if hasUser {
|
|
uid := userID.(uint)
|
|
vote.UserID = &uid
|
|
}
|
|
|
|
if err := tx.Create(&vote).Error; err != nil {
|
|
tx.Rollback()
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to record vote"})
|
|
return
|
|
}
|
|
|
|
// Increment option vote count
|
|
if err := tx.Model(&models.PollOption{}).Where("id = ?", optionID).UpdateColumn("vote_count", gorm.Expr("vote_count + ?", 1)).Error; err != nil {
|
|
tx.Rollback()
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update vote count"})
|
|
return
|
|
}
|
|
}
|
|
|
|
// Increment poll total votes
|
|
if err := tx.Model(&models.Poll{}).Where("id = ?", poll.ID).UpdateColumn("total_votes", gorm.Expr("total_votes + ?", 1)).Error; err != nil {
|
|
tx.Rollback()
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update total votes"})
|
|
return
|
|
}
|
|
|
|
tx.Commit()
|
|
|
|
// Reload poll with updated counts
|
|
pc.DB.Preload("Options", func(db *gorm.DB) *gorm.DB {
|
|
return db.Order("display_order ASC, id ASC")
|
|
}).First(&poll, poll.ID)
|
|
|
|
// Engagement: award points to authenticated user
|
|
if hasUser {
|
|
uid := userID.(uint)
|
|
svc := services.NewEngagementService(pc.DB)
|
|
_, _ = svc.AwardPointsCapped(uid, 3, "poll_vote", map[string]interface{}{"poll_id": poll.ID})
|
|
_ = svc.CheckAndAwardAchievements(uid)
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"message": "Vote recorded successfully",
|
|
"poll": poll,
|
|
})
|
|
}
|
|
|
|
// GetPollResults returns poll results
|
|
func (pc *PollController) GetPollResults(c *gin.Context) {
|
|
id := c.Param("id")
|
|
|
|
var poll models.Poll
|
|
if err := pc.DB.Preload("Options", func(db *gorm.DB) *gorm.DB {
|
|
return db.Order("display_order ASC, id ASC")
|
|
}).First(&poll, id).Error; err != nil {
|
|
if err == gorm.ErrRecordNotFound {
|
|
c.JSON(http.StatusNotFound, gin.H{"error": "Poll not found"})
|
|
return
|
|
}
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch poll"})
|
|
return
|
|
}
|
|
|
|
// Check if user can see results
|
|
hasVoted := false
|
|
if userID, exists := c.Get("userID"); exists && userID != nil {
|
|
var count int64
|
|
pc.DB.Model(&models.PollVote{}).Where("poll_id = ? AND user_id = ?", poll.ID, userID).Count(&count)
|
|
hasVoted = count > 0
|
|
} else {
|
|
ipHash := pc.hashIP(c.ClientIP())
|
|
sessionToken := c.GetHeader("X-Session-Token")
|
|
if sessionToken == "" {
|
|
sessionToken = c.Query("session_token")
|
|
}
|
|
|
|
var count int64
|
|
query := pc.DB.Model(&models.PollVote{}).Where("poll_id = ?", poll.ID)
|
|
if sessionToken != "" {
|
|
query = query.Where("session_token = ?", sessionToken)
|
|
} else {
|
|
query = query.Where("ip_hash = ?", ipHash)
|
|
}
|
|
query.Count(&count)
|
|
hasVoted = count > 0
|
|
}
|
|
|
|
if !poll.CanShowResults(hasVoted) && !c.GetBool("isAdmin") {
|
|
c.JSON(http.StatusForbidden, gin.H{"error": "Results are not available yet"})
|
|
return
|
|
}
|
|
|
|
// Calculate percentages
|
|
results := make([]gin.H, len(poll.Options))
|
|
for i, option := range poll.Options {
|
|
percentage := 0.0
|
|
if poll.TotalVotes > 0 {
|
|
percentage = float64(option.VoteCount) / float64(poll.TotalVotes) * 100
|
|
}
|
|
|
|
results[i] = gin.H{
|
|
"option_id": option.ID,
|
|
"text": option.Text,
|
|
"vote_count": option.VoteCount,
|
|
"percentage": percentage,
|
|
"image_url": option.ImageURL,
|
|
"player_id": option.PlayerID,
|
|
}
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"poll_id": poll.ID,
|
|
"title": poll.Title,
|
|
"total_votes": poll.TotalVotes,
|
|
"results": results,
|
|
})
|
|
}
|
|
|
|
// Helper function to hash IP addresses
|
|
func (pc *PollController) hashIP(ip string) string {
|
|
hash := sha256.Sum256([]byte(ip + "poll-salt-2025"))
|
|
return hex.EncodeToString(hash[:])
|
|
}
|
|
|
|
// GetPollStats returns statistics for admin (admin only)
|
|
func (pc *PollController) GetPollStats(c *gin.Context) {
|
|
id := c.Param("id")
|
|
pollID, err := strconv.Atoi(id)
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid poll ID"})
|
|
return
|
|
}
|
|
|
|
var poll models.Poll
|
|
if err := pc.DB.Preload("Options").First(&poll, pollID).Error; err != nil {
|
|
c.JSON(http.StatusNotFound, gin.H{"error": "Poll not found"})
|
|
return
|
|
}
|
|
|
|
// Get vote distribution over time
|
|
var votesByDay []struct {
|
|
Date string `json:"date"`
|
|
Count int `json:"count"`
|
|
}
|
|
|
|
pc.DB.Model(&models.PollVote{}).
|
|
Select("DATE(created_at) as date, COUNT(*) as count").
|
|
Where("poll_id = ?", pollID).
|
|
Group("DATE(created_at)").
|
|
Order("date ASC").
|
|
Scan(&votesByDay)
|
|
|
|
// Get authenticated vs guest votes
|
|
var authVotes, guestVotes int64
|
|
pc.DB.Model(&models.PollVote{}).Where("poll_id = ? AND user_id IS NOT NULL", pollID).Count(&authVotes)
|
|
pc.DB.Model(&models.PollVote{}).Where("poll_id = ? AND user_id IS NULL", pollID).Count(&guestVotes)
|
|
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"poll": poll,
|
|
"votes_by_day": votesByDay,
|
|
"authenticated_votes": authVotes,
|
|
"guest_votes": guestVotes,
|
|
})
|
|
}
|
|
|
|
// AdminListVotes returns detailed list of votes for a poll (admin only)
|
|
func (pc *PollController) AdminListVotes(c *gin.Context) {
|
|
id := c.Param("id")
|
|
|
|
var votes []models.PollVote
|
|
if err := pc.DB.Preload("Option").Preload("User").Where("poll_id = ?", id).Order("created_at DESC").Find(&votes).Error; err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch votes"})
|
|
return
|
|
}
|
|
|
|
type VoteDTO struct {
|
|
ID uint `json:"id"`
|
|
PollID uint `json:"poll_id"`
|
|
OptionID uint `json:"option_id"`
|
|
OptionText string `json:"option_text"`
|
|
UserID *uint `json:"user_id"`
|
|
UserEmail string `json:"user_email"`
|
|
UserFirstName string `json:"user_first_name"`
|
|
UserLastName string `json:"user_last_name"`
|
|
VoterName string `json:"voter_name"`
|
|
VoterEmail string `json:"voter_email"`
|
|
SessionToken string `json:"session_token"`
|
|
CreatedAt time.Time `json:"created_at"`
|
|
}
|
|
|
|
result := make([]VoteDTO, 0, len(votes))
|
|
for _, v := range votes {
|
|
optionText := ""
|
|
if v.Option != nil {
|
|
optionText = v.Option.Text
|
|
}
|
|
var userEmail, firstName, lastName string
|
|
if v.User != nil {
|
|
userEmail = v.User.Email
|
|
firstName = v.User.FirstName
|
|
lastName = v.User.LastName
|
|
}
|
|
result = append(result, VoteDTO{
|
|
ID: v.ID,
|
|
PollID: v.PollID,
|
|
OptionID: v.OptionID,
|
|
OptionText: optionText,
|
|
UserID: v.UserID,
|
|
UserEmail: userEmail,
|
|
UserFirstName: firstName,
|
|
UserLastName: lastName,
|
|
VoterName: v.VoterName,
|
|
VoterEmail: v.VoterEmail,
|
|
SessionToken: v.SessionToken,
|
|
CreatedAt: v.CreatedAt,
|
|
})
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{"votes": result})
|
|
}
|