first commit
This commit is contained in:
58
backend/Controllers/AdminController.cs
Normal file
58
backend/Controllers/AdminController.cs
Normal file
@@ -0,0 +1,58 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Options;
|
||||
using backend.Data;
|
||||
using backend.DTOs;
|
||||
using backend.Models;
|
||||
|
||||
namespace backend.Controllers;
|
||||
|
||||
[ApiController]
|
||||
[Route("api/[controller]")]
|
||||
public class AdminController : ControllerBase
|
||||
{
|
||||
private readonly AppDbContext _db;
|
||||
private readonly AppConfig _appConfig;
|
||||
|
||||
public AdminController(AppDbContext db, IOptions<AppConfig> appConfig)
|
||||
{
|
||||
_db = db;
|
||||
_appConfig = appConfig.Value;
|
||||
}
|
||||
|
||||
[HttpGet("report")]
|
||||
public async Task<IActionResult> GetReport([FromQuery] string password)
|
||||
{
|
||||
if (password != _appConfig.AdminPassword)
|
||||
{
|
||||
return StatusCode(403, new { message = "رمز عبور اشتباه است." });
|
||||
}
|
||||
|
||||
var users = await _db.Users
|
||||
.Include(u => u.QuizAttempt)
|
||||
.Include(u => u.KhatmSelections)
|
||||
.ThenInclude(ks => ks.KhatmItem)
|
||||
.Include(u => u.TotalScore)
|
||||
.OrderBy(u => u.Id)
|
||||
.ToListAsync();
|
||||
|
||||
var report = users.Select(u => new AdminReportItem
|
||||
{
|
||||
UserId = u.Id,
|
||||
FullName = u.FullName,
|
||||
Contact = u.Contact,
|
||||
TrackingCode = u.TrackingCode,
|
||||
HasTakenQuiz = u.QuizAttempt != null,
|
||||
ParticipationPoints = u.QuizAttempt?.ParticipationPoints ?? 0,
|
||||
CorrectCount = u.QuizAttempt?.CorrectAnswersCount ?? 0,
|
||||
TotalQuizPoints = u.QuizAttempt?.TotalQuizPoints ?? 0,
|
||||
KhatmTitles = string.Join("، ",
|
||||
u.KhatmSelections
|
||||
.Where(ks => ks.IsSelected)
|
||||
.Select(ks => ks.KhatmItem.Title)),
|
||||
TotalScore = u.TotalScore?.TotalPoints ?? 0
|
||||
}).ToList();
|
||||
|
||||
return Ok(report);
|
||||
}
|
||||
}
|
||||
31
backend/Controllers/ConfigController.cs
Normal file
31
backend/Controllers/ConfigController.cs
Normal file
@@ -0,0 +1,31 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Options;
|
||||
using backend.Models;
|
||||
|
||||
namespace backend.Controllers;
|
||||
|
||||
[ApiController]
|
||||
[Route("api/[controller]")]
|
||||
public class ConfigController : ControllerBase
|
||||
{
|
||||
private readonly AppConfig _appConfig;
|
||||
|
||||
public ConfigController(IOptions<AppConfig> appConfig)
|
||||
{
|
||||
_appConfig = appConfig.Value;
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
public IActionResult GetConfig()
|
||||
{
|
||||
return Ok(new
|
||||
{
|
||||
siteTitle = _appConfig.SiteTitle,
|
||||
subtitle = _appConfig.Subtitle,
|
||||
channels = _appConfig.Channels.Select(c => new { name = c.Name, url = c.Url }),
|
||||
shortText = _appConfig.ShortText,
|
||||
quizParticipationPoints = _appConfig.Quiz.ParticipationPoints,
|
||||
quizCorrectAnswerPoints = _appConfig.Quiz.CorrectAnswerPoints
|
||||
});
|
||||
}
|
||||
}
|
||||
105
backend/Controllers/KhatmController.cs
Normal file
105
backend/Controllers/KhatmController.cs
Normal file
@@ -0,0 +1,105 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using backend.Data;
|
||||
using backend.DTOs;
|
||||
using backend.Models;
|
||||
|
||||
namespace backend.Controllers;
|
||||
|
||||
[ApiController]
|
||||
[Route("api/[controller]")]
|
||||
public class KhatmController : ControllerBase
|
||||
{
|
||||
private readonly AppDbContext _db;
|
||||
|
||||
public KhatmController(AppDbContext db)
|
||||
{
|
||||
_db = db;
|
||||
}
|
||||
|
||||
[HttpGet("items")]
|
||||
public async Task<IActionResult> GetItems()
|
||||
{
|
||||
var items = await _db.KhatmItems
|
||||
.OrderBy(k => k.Id)
|
||||
.Select(k => new KhatmItemDto
|
||||
{
|
||||
Id = k.Id,
|
||||
Title = k.Title,
|
||||
Url = k.Url,
|
||||
Points = k.Points
|
||||
})
|
||||
.ToListAsync();
|
||||
|
||||
return Ok(items);
|
||||
}
|
||||
|
||||
[HttpPost("save")]
|
||||
public async Task<IActionResult> SaveSelections([FromBody] KhatmSaveRequest request)
|
||||
{
|
||||
var user = await _db.Users.FindAsync(request.UserId);
|
||||
if (user == null)
|
||||
{
|
||||
return NotFound(new { message = "کاربر یافت نشد." });
|
||||
}
|
||||
|
||||
foreach (var selection in request.Selections)
|
||||
{
|
||||
var existing = await _db.UserKhatmSelections
|
||||
.FirstOrDefaultAsync(ks => ks.UserId == request.UserId && ks.KhatmItemId == selection.KhatmItemId);
|
||||
|
||||
if (existing != null)
|
||||
{
|
||||
existing.IsSelected = selection.IsSelected;
|
||||
existing.LastModified = DateTime.UtcNow;
|
||||
}
|
||||
else
|
||||
{
|
||||
_db.UserKhatmSelections.Add(new UserKhatmSelection
|
||||
{
|
||||
UserId = request.UserId,
|
||||
KhatmItemId = selection.KhatmItemId,
|
||||
IsSelected = selection.IsSelected,
|
||||
LastModified = DateTime.UtcNow
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Recalculate total score
|
||||
await RecalculateTotalScore(request.UserId);
|
||||
await _db.SaveChangesAsync();
|
||||
|
||||
var totalScore = await _db.UserTotalScores
|
||||
.Where(ts => ts.UserId == request.UserId)
|
||||
.Select(ts => ts.TotalPoints)
|
||||
.FirstOrDefaultAsync();
|
||||
|
||||
return Ok(new KhatmSaveResponse
|
||||
{
|
||||
Success = true,
|
||||
TotalScore = totalScore
|
||||
});
|
||||
}
|
||||
|
||||
private async Task RecalculateTotalScore(int userId)
|
||||
{
|
||||
var quizPoints = await _db.UserQuizAttempts
|
||||
.Where(qa => qa.UserId == userId)
|
||||
.Select(qa => (int?)qa.TotalQuizPoints)
|
||||
.FirstOrDefaultAsync() ?? 0;
|
||||
|
||||
var khatmPoints = await _db.UserKhatmSelections
|
||||
.Where(ks => ks.UserId == userId && ks.IsSelected)
|
||||
.SumAsync(ks => ks.KhatmItem.Points);
|
||||
|
||||
int total = quizPoints + khatmPoints;
|
||||
|
||||
var totalScore = await _db.UserTotalScores
|
||||
.FirstOrDefaultAsync(ts => ts.UserId == userId);
|
||||
|
||||
if (totalScore != null)
|
||||
{
|
||||
totalScore.TotalPoints = total;
|
||||
}
|
||||
}
|
||||
}
|
||||
165
backend/Controllers/QuizController.cs
Normal file
165
backend/Controllers/QuizController.cs
Normal file
@@ -0,0 +1,165 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Options;
|
||||
using backend.Data;
|
||||
using backend.DTOs;
|
||||
using backend.Models;
|
||||
|
||||
namespace backend.Controllers;
|
||||
|
||||
[ApiController]
|
||||
[Route("api/[controller]")]
|
||||
public class QuizController : ControllerBase
|
||||
{
|
||||
private readonly AppDbContext _db;
|
||||
private readonly AppConfig _appConfig;
|
||||
|
||||
public QuizController(AppDbContext db, IOptions<AppConfig> appConfig)
|
||||
{
|
||||
_db = db;
|
||||
_appConfig = appConfig.Value;
|
||||
}
|
||||
|
||||
[HttpGet("questions")]
|
||||
public async Task<IActionResult> GetQuestions()
|
||||
{
|
||||
var questions = await _db.QuizQuestions
|
||||
.OrderBy(q => q.Id)
|
||||
.Select(q => new QuestionDto
|
||||
{
|
||||
Id = q.Id,
|
||||
QuestionText = q.QuestionText,
|
||||
Options = new List<string> { q.Option1, q.Option2, q.Option3, q.Option4 }
|
||||
})
|
||||
.ToListAsync();
|
||||
|
||||
return Ok(questions);
|
||||
}
|
||||
|
||||
[HttpPost("submit")]
|
||||
public async Task<IActionResult> SubmitQuiz([FromBody] QuizSubmitRequest request)
|
||||
{
|
||||
// Check if user already attempted
|
||||
var existingAttempt = await _db.UserQuizAttempts
|
||||
.AnyAsync(qa => qa.UserId == request.UserId);
|
||||
|
||||
if (existingAttempt)
|
||||
{
|
||||
return BadRequest(new { message = "شما قبلاً در مسابقه شرکت کردهاید." });
|
||||
}
|
||||
|
||||
var user = await _db.Users.FindAsync(request.UserId);
|
||||
if (user == null)
|
||||
{
|
||||
return NotFound(new { message = "کاربر یافت نشد." });
|
||||
}
|
||||
|
||||
var questions = await _db.QuizQuestions.ToListAsync();
|
||||
var participationPoints = _appConfig.Quiz.ParticipationPoints;
|
||||
var correctAnswerPoints = _appConfig.Quiz.CorrectAnswerPoints;
|
||||
|
||||
int correctCount = 0;
|
||||
var answers = new List<UserQuizAnswer>();
|
||||
|
||||
foreach (var answer in request.Answers)
|
||||
{
|
||||
var question = questions.FirstOrDefault(q => q.Id == answer.QuestionId);
|
||||
if (question == null) continue;
|
||||
|
||||
bool isCorrect = question.CorrectOption == answer.SelectedOption;
|
||||
if (isCorrect) correctCount++;
|
||||
|
||||
answers.Add(new UserQuizAnswer
|
||||
{
|
||||
QuestionId = answer.QuestionId,
|
||||
SelectedOption = answer.SelectedOption,
|
||||
IsCorrect = isCorrect
|
||||
});
|
||||
}
|
||||
|
||||
int totalQuizPoints = participationPoints + (correctCount * correctAnswerPoints);
|
||||
|
||||
var attempt = new UserQuizAttempt
|
||||
{
|
||||
UserId = request.UserId,
|
||||
ParticipationPoints = participationPoints,
|
||||
CorrectAnswersCount = correctCount,
|
||||
TotalQuizPoints = totalQuizPoints,
|
||||
CompletedAt = DateTime.UtcNow,
|
||||
Answers = answers
|
||||
};
|
||||
|
||||
_db.UserQuizAttempts.Add(attempt);
|
||||
|
||||
// Update total score
|
||||
await RecalculateTotalScore(request.UserId);
|
||||
|
||||
await _db.SaveChangesAsync();
|
||||
|
||||
var totalScore = await _db.UserTotalScores
|
||||
.Where(ts => ts.UserId == request.UserId)
|
||||
.Select(ts => ts.TotalPoints)
|
||||
.FirstOrDefaultAsync();
|
||||
|
||||
return Ok(new QuizSubmitResponse
|
||||
{
|
||||
ParticipationPoints = participationPoints,
|
||||
CorrectCount = correctCount,
|
||||
TotalQuizPoints = totalQuizPoints,
|
||||
TotalScore = totalScore
|
||||
});
|
||||
}
|
||||
|
||||
[HttpGet("user/{userId}")]
|
||||
public async Task<IActionResult> GetUserQuiz(int userId)
|
||||
{
|
||||
var attempt = await _db.UserQuizAttempts
|
||||
.Include(qa => qa.Answers)
|
||||
.ThenInclude(a => a.Question)
|
||||
.FirstOrDefaultAsync(qa => qa.UserId == userId);
|
||||
|
||||
if (attempt == null)
|
||||
{
|
||||
return NotFound(new { message = "کاربر هنوز در مسابقه شرکت نکرده است." });
|
||||
}
|
||||
|
||||
var questions = attempt.Answers.Select(a => new UserQuestionDto
|
||||
{
|
||||
Id = a.Question.Id,
|
||||
QuestionText = a.Question.QuestionText,
|
||||
Options = new List<string> { a.Question.Option1, a.Question.Option2, a.Question.Option3, a.Question.Option4 },
|
||||
SelectedOption = a.SelectedOption,
|
||||
IsCorrect = a.IsCorrect
|
||||
}).ToList();
|
||||
|
||||
return Ok(new UserQuizResponse
|
||||
{
|
||||
Questions = questions,
|
||||
ParticipationPoints = attempt.ParticipationPoints,
|
||||
CorrectCount = attempt.CorrectAnswersCount,
|
||||
TotalQuizPoints = attempt.TotalQuizPoints
|
||||
});
|
||||
}
|
||||
|
||||
private async Task RecalculateTotalScore(int userId)
|
||||
{
|
||||
var quizPoints = await _db.UserQuizAttempts
|
||||
.Where(qa => qa.UserId == userId)
|
||||
.Select(qa => (int?)qa.TotalQuizPoints)
|
||||
.FirstOrDefaultAsync() ?? 0;
|
||||
|
||||
var khatmPoints = await _db.UserKhatmSelections
|
||||
.Where(ks => ks.UserId == userId && ks.IsSelected)
|
||||
.SumAsync(ks => ks.KhatmItem.Points);
|
||||
|
||||
int total = quizPoints + khatmPoints;
|
||||
|
||||
var totalScore = await _db.UserTotalScores
|
||||
.FirstOrDefaultAsync(ts => ts.UserId == userId);
|
||||
|
||||
if (totalScore != null)
|
||||
{
|
||||
totalScore.TotalPoints = total;
|
||||
}
|
||||
}
|
||||
}
|
||||
129
backend/Controllers/UserController.cs
Normal file
129
backend/Controllers/UserController.cs
Normal file
@@ -0,0 +1,129 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using backend.Data;
|
||||
using backend.DTOs;
|
||||
using backend.Models;
|
||||
|
||||
namespace backend.Controllers;
|
||||
|
||||
[ApiController]
|
||||
[Route("api/[controller]")]
|
||||
public class UserController : ControllerBase
|
||||
{
|
||||
private readonly AppDbContext _db;
|
||||
|
||||
public UserController(AppDbContext db)
|
||||
{
|
||||
_db = db;
|
||||
}
|
||||
|
||||
[HttpPost("register")]
|
||||
public async Task<IActionResult> Register([FromBody] RegisterRequest request)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(request.FullName) || string.IsNullOrWhiteSpace(request.Contact))
|
||||
{
|
||||
return BadRequest(new { message = "نام و شناسه الزامی هستند." });
|
||||
}
|
||||
|
||||
if (request.Contact.Length < 5)
|
||||
{
|
||||
return BadRequest(new { message = "شناسه باید حداقل ۵ کاراکتر باشد." });
|
||||
}
|
||||
|
||||
// Check if contact already exists
|
||||
var existingUser = await _db.Users.FirstOrDefaultAsync(u => u.Contact == request.Contact);
|
||||
if (existingUser != null)
|
||||
{
|
||||
return Conflict(new { message = "این شناسه قبلاً ثبت شده است. لطفاً با کد رهگیری وارد شوید." });
|
||||
}
|
||||
|
||||
// Generate unique tracking code
|
||||
string trackingCode;
|
||||
do
|
||||
{
|
||||
trackingCode = GenerateTrackingCode();
|
||||
} while (await _db.Users.AnyAsync(u => u.TrackingCode == trackingCode));
|
||||
|
||||
var user = new User
|
||||
{
|
||||
FullName = request.FullName.Trim(),
|
||||
Contact = request.Contact.Trim(),
|
||||
TrackingCode = trackingCode
|
||||
};
|
||||
|
||||
_db.Users.Add(user);
|
||||
|
||||
// Save the user first so that user.Id is populated by the database
|
||||
await _db.SaveChangesAsync();
|
||||
|
||||
// Create initial total score record with the now-populated user.Id
|
||||
var totalScore = new UserTotalScore
|
||||
{
|
||||
UserId = user.Id,
|
||||
TotalPoints = 0
|
||||
};
|
||||
_db.UserTotalScores.Add(totalScore);
|
||||
|
||||
await _db.SaveChangesAsync();
|
||||
|
||||
return Ok(new RegisterResponse
|
||||
{
|
||||
UserId = user.Id,
|
||||
TrackingCode = trackingCode
|
||||
});
|
||||
}
|
||||
|
||||
[HttpPost("login")]
|
||||
public async Task<IActionResult> Login([FromBody] LoginRequest request)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(request.TrackingCode))
|
||||
{
|
||||
return BadRequest(new { message = "کد رهگیری الزامی است." });
|
||||
}
|
||||
|
||||
var user = await _db.Users
|
||||
.Include(u => u.QuizAttempt)
|
||||
.Include(u => u.KhatmSelections)
|
||||
.FirstOrDefaultAsync(u => u.TrackingCode == request.TrackingCode.Trim());
|
||||
|
||||
if (user == null)
|
||||
{
|
||||
return NotFound(new { message = "کد رهگیری نامعتبر است." });
|
||||
}
|
||||
|
||||
return Ok(new LoginResponse
|
||||
{
|
||||
UserId = user.Id,
|
||||
FullName = user.FullName,
|
||||
Contact = user.Contact,
|
||||
IsQuizCompleted = user.QuizAttempt != null,
|
||||
KhatmSelections = user.KhatmSelections
|
||||
.Where(ks => ks.IsSelected)
|
||||
.Select(ks => ks.KhatmItemId)
|
||||
.ToList()
|
||||
});
|
||||
}
|
||||
|
||||
[HttpGet("{userId}/score")]
|
||||
public async Task<IActionResult> GetScore(int userId)
|
||||
{
|
||||
var totalScore = await _db.UserTotalScores
|
||||
.FirstOrDefaultAsync(ts => ts.UserId == userId);
|
||||
|
||||
if (totalScore == null)
|
||||
{
|
||||
return NotFound(new { message = "کاربر یافت نشد." });
|
||||
}
|
||||
|
||||
return Ok(new UserScoreResponse
|
||||
{
|
||||
TotalScore = totalScore.TotalPoints
|
||||
});
|
||||
}
|
||||
|
||||
private static string GenerateTrackingCode()
|
||||
{
|
||||
var random = new Random();
|
||||
return random.Next(1000, 10000).ToString();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user