feat(backend/application): add DTOs and services for Lesson Management
- Created LevelDto.cs with LevelDto, CreateLevelDto, UpdateLevelDto - Created UserProgressDto.cs with UserProgressDto, UpdateUserProgressDto, LevelCompletionDto - Created LevelService.cs with CRUD operations for CEFR levels - Created LessonService.cs with CRUD operations and filtering by level - Created ProgressService.cs with user progress tracking, completion percentage, and lesson unlocking logic - Registered all new services in Program.cs - Fixed UserProgressDto ToEntity/UpdateFromDto to use domain entity methods Generated by Mistral Vibe. Co-Authored-By: Mistral Vibe <vibe@mistral.ai>
This commit is contained in:
parent
8e05c2b6e2
commit
be8c58671d
6 changed files with 605 additions and 0 deletions
51
GermanApp/Application/DTOs/LevelDto.cs
Normal file
51
GermanApp/Application/DTOs/LevelDto.cs
Normal file
|
|
@ -0,0 +1,51 @@
|
||||||
|
using GermanApp.Domain.Entities;
|
||||||
|
|
||||||
|
namespace GermanApp.Application.DTOs;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Data Transfer Object for Level - used for API responses.
|
||||||
|
/// This is a read-only representation of a Level entity.
|
||||||
|
/// </summary>
|
||||||
|
public record LevelDto(
|
||||||
|
int Id,
|
||||||
|
string Name,
|
||||||
|
string Code,
|
||||||
|
int Order);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Data Transfer Object for creating a new Level.
|
||||||
|
/// </summary>
|
||||||
|
public record CreateLevelDto(
|
||||||
|
string Name,
|
||||||
|
string Code,
|
||||||
|
int Order);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Data Transfer Object for updating an existing Level.
|
||||||
|
/// </summary>
|
||||||
|
public record UpdateLevelDto(
|
||||||
|
string Name,
|
||||||
|
string Code,
|
||||||
|
int Order);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Extension methods for mapping between Level entity and DTOs.
|
||||||
|
/// </summary>
|
||||||
|
public static class LevelDtoExtensions
|
||||||
|
{
|
||||||
|
public static LevelDto ToDto(this Level level) => new(
|
||||||
|
level.Id,
|
||||||
|
level.Name,
|
||||||
|
level.Code,
|
||||||
|
level.Order);
|
||||||
|
|
||||||
|
public static Level ToEntity(this CreateLevelDto dto) =>
|
||||||
|
Level.Create(dto.Name, dto.Code, dto.Order);
|
||||||
|
|
||||||
|
public static void UpdateFromDto(this Level level, UpdateLevelDto dto)
|
||||||
|
{
|
||||||
|
level.UpdateName(dto.Name);
|
||||||
|
level.UpdateCode(dto.Code);
|
||||||
|
level.UpdateOrder(dto.Order);
|
||||||
|
}
|
||||||
|
}
|
||||||
80
GermanApp/Application/DTOs/UserProgressDto.cs
Normal file
80
GermanApp/Application/DTOs/UserProgressDto.cs
Normal file
|
|
@ -0,0 +1,80 @@
|
||||||
|
using GermanApp.Domain.Entities;
|
||||||
|
|
||||||
|
namespace GermanApp.Application.DTOs;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Data Transfer Object for UserProgress - used for API responses.
|
||||||
|
/// This is a read-only representation of a UserProgress entity.
|
||||||
|
/// </summary>
|
||||||
|
public record UserProgressDto(
|
||||||
|
int Id,
|
||||||
|
int UserId,
|
||||||
|
int LessonId,
|
||||||
|
bool IsCompleted,
|
||||||
|
int QuizScore,
|
||||||
|
DateTime LastAttemptDate,
|
||||||
|
string? LessonTitle = null,
|
||||||
|
string? LevelName = null,
|
||||||
|
string? LevelCode = null);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Data Transfer Object for creating/updating user progress.
|
||||||
|
/// </summary>
|
||||||
|
public record UpdateUserProgressDto(
|
||||||
|
int LessonId,
|
||||||
|
bool IsCompleted,
|
||||||
|
int QuizScore);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Data Transfer Object for level completion summary.
|
||||||
|
/// </summary>
|
||||||
|
public record LevelCompletionDto(
|
||||||
|
int LevelId,
|
||||||
|
string LevelName,
|
||||||
|
string LevelCode,
|
||||||
|
int TotalLessons,
|
||||||
|
int CompletedLessons,
|
||||||
|
double CompletionPercentage);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Extension methods for mapping between UserProgress entity and DTOs.
|
||||||
|
/// </summary>
|
||||||
|
public static class UserProgressDtoExtensions
|
||||||
|
{
|
||||||
|
public static UserProgressDto ToDto(this UserProgress userProgress) => new(
|
||||||
|
userProgress.Id,
|
||||||
|
userProgress.UserId,
|
||||||
|
userProgress.LessonId,
|
||||||
|
userProgress.IsCompleted,
|
||||||
|
userProgress.QuizScore,
|
||||||
|
userProgress.LastAttemptDate,
|
||||||
|
userProgress.Lesson?.Title,
|
||||||
|
userProgress.Lesson?.Level?.Name,
|
||||||
|
userProgress.Lesson?.Level?.Code);
|
||||||
|
|
||||||
|
public static UserProgress ToEntity(this UpdateUserProgressDto dto, int userId)
|
||||||
|
{
|
||||||
|
var progress = UserProgress.Create(userId, dto.LessonId);
|
||||||
|
if (dto.IsCompleted)
|
||||||
|
{
|
||||||
|
progress.MarkAsCompleted(dto.QuizScore);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
progress.UpdateQuizScore(dto.QuizScore);
|
||||||
|
}
|
||||||
|
return progress;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void UpdateFromDto(this UserProgress userProgress, UpdateUserProgressDto dto)
|
||||||
|
{
|
||||||
|
if (dto.IsCompleted)
|
||||||
|
{
|
||||||
|
userProgress.MarkAsCompleted(dto.QuizScore);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
userProgress.UpdateQuizScore(dto.QuizScore);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
159
GermanApp/Application/Services/LessonService.cs
Normal file
159
GermanApp/Application/Services/LessonService.cs
Normal file
|
|
@ -0,0 +1,159 @@
|
||||||
|
using GermanApp.Application.DTOs;
|
||||||
|
using GermanApp.Domain.Entities;
|
||||||
|
using GermanApp.Domain.Interfaces;
|
||||||
|
|
||||||
|
namespace GermanApp.Application.Services;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Application service for managing lessons.
|
||||||
|
/// This is part of the Application layer.
|
||||||
|
/// </summary>
|
||||||
|
public class LessonService
|
||||||
|
{
|
||||||
|
private readonly ILessonRepository _lessonRepository;
|
||||||
|
private readonly ILevelRepository _levelRepository;
|
||||||
|
|
||||||
|
public LessonService(ILessonRepository lessonRepository, ILevelRepository levelRepository)
|
||||||
|
{
|
||||||
|
_lessonRepository = lessonRepository;
|
||||||
|
_levelRepository = levelRepository;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets all lessons.
|
||||||
|
/// </summary>
|
||||||
|
public async Task<IReadOnlyList<LessonDto>> GetAllLessonsAsync(CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
var lessons = await _lessonRepository.GetAllAsync(cancellationToken);
|
||||||
|
return lessons.Select(l => l.ToDto()).ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets all lessons ordered by level and lesson order.
|
||||||
|
/// </summary>
|
||||||
|
public async Task<IReadOnlyList<LessonDto>> GetAllOrderedLessonsAsync(CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
var lessons = await _lessonRepository.GetAllOrderedAsync(cancellationToken);
|
||||||
|
return lessons.Select(l => l.ToDto()).ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets a lesson by its ID.
|
||||||
|
/// </summary>
|
||||||
|
public async Task<LessonDto?> GetLessonByIdAsync(int id, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
var lesson = await _lessonRepository.GetByIdAsync(id, cancellationToken);
|
||||||
|
return lesson?.ToDto();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets lessons by level ID.
|
||||||
|
/// </summary>
|
||||||
|
public async Task<IReadOnlyList<LessonDto>> GetLessonsByLevelAsync(int levelId, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
var lessons = await _lessonRepository.GetByLevelAsync(levelId, cancellationToken);
|
||||||
|
return lessons.Select(l => l.ToDto()).ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets beginner lessons (A1 and A2 - levels 1 and 2).
|
||||||
|
/// </summary>
|
||||||
|
public async Task<IReadOnlyList<LessonDto>> GetBeginnerLessonsAsync(CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
var level1Lessons = await _lessonRepository.GetByLevelAsync(1, cancellationToken);
|
||||||
|
var level2Lessons = await _lessonRepository.GetByLevelAsync(2, cancellationToken);
|
||||||
|
var allLessons = level1Lessons.Concat(level2Lessons)
|
||||||
|
.OrderBy(l => l.LevelId)
|
||||||
|
.ThenBy(l => l.Order)
|
||||||
|
.ToList();
|
||||||
|
return allLessons.Select(l => l.ToDto()).ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets advanced lessons (B2 and C1 - levels 4 and 5).
|
||||||
|
/// </summary>
|
||||||
|
public async Task<IReadOnlyList<LessonDto>> GetAdvancedLessonsAsync(CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
var level4Lessons = await _lessonRepository.GetByLevelAsync(4, cancellationToken);
|
||||||
|
var level5Lessons = await _lessonRepository.GetByLevelAsync(5, cancellationToken);
|
||||||
|
var allLessons = level4Lessons.Concat(level5Lessons)
|
||||||
|
.OrderBy(l => l.LevelId)
|
||||||
|
.ThenBy(l => l.Order)
|
||||||
|
.ToList();
|
||||||
|
return allLessons.Select(l => l.ToDto()).ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a new lesson.
|
||||||
|
/// </summary>
|
||||||
|
public async Task<LessonDto> CreateLessonAsync(CreateLessonDto dto, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
// Validate that the level exists
|
||||||
|
var level = await _levelRepository.GetByIdAsync(dto.LevelId, cancellationToken);
|
||||||
|
if (level == null)
|
||||||
|
throw new ArgumentException("Level does not exist");
|
||||||
|
|
||||||
|
var lesson = dto.ToEntity();
|
||||||
|
var createdLesson = await _lessonRepository.AddAsync(lesson, cancellationToken);
|
||||||
|
return createdLesson.ToDto();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Updates an existing lesson.
|
||||||
|
/// </summary>
|
||||||
|
public async Task<LessonDto?> UpdateLessonAsync(int id, UpdateLessonDto dto, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
var lesson = await _lessonRepository.GetByIdAsync(id, cancellationToken);
|
||||||
|
if (lesson == null)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
// Validate that the level exists
|
||||||
|
var level = await _levelRepository.GetByIdAsync(dto.LevelId, cancellationToken);
|
||||||
|
if (level == null)
|
||||||
|
throw new ArgumentException("Level does not exist");
|
||||||
|
|
||||||
|
lesson.UpdateFromDto(dto);
|
||||||
|
await _lessonRepository.UpdateAsync(lesson, cancellationToken);
|
||||||
|
return lesson.ToDto();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Deletes a lesson by its ID.
|
||||||
|
/// </summary>
|
||||||
|
public async Task<bool> DeleteLessonAsync(int id, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
var lesson = await _lessonRepository.GetByIdAsync(id, cancellationToken);
|
||||||
|
if (lesson == null)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
await _lessonRepository.DeleteAsync(lesson, cancellationToken);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the first lesson in a level.
|
||||||
|
/// </summary>
|
||||||
|
public async Task<LessonDto?> GetFirstLessonInLevelAsync(int levelId, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
var lesson = await _lessonRepository.GetFirstLessonInLevelAsync(levelId, cancellationToken);
|
||||||
|
return lesson?.ToDto();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the next lesson after the specified one.
|
||||||
|
/// </summary>
|
||||||
|
public async Task<LessonDto?> GetNextLessonAsync(int currentLessonId, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
var lesson = await _lessonRepository.GetNextLessonAsync(currentLessonId, cancellationToken);
|
||||||
|
return lesson?.ToDto();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets lessons that the user can access (based on completion of previous lessons).
|
||||||
|
/// </summary>
|
||||||
|
public async Task<IReadOnlyList<LessonDto>> GetAccessibleLessonsAsync(int userId, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
var lessons = await _lessonRepository.GetAccessibleLessonsAsync(userId, cancellationToken);
|
||||||
|
return lessons.Select(l => l.ToDto()).ToList();
|
||||||
|
}
|
||||||
|
}
|
||||||
101
GermanApp/Application/Services/LevelService.cs
Normal file
101
GermanApp/Application/Services/LevelService.cs
Normal file
|
|
@ -0,0 +1,101 @@
|
||||||
|
using GermanApp.Application.DTOs;
|
||||||
|
using GermanApp.Domain.Entities;
|
||||||
|
using GermanApp.Domain.Interfaces;
|
||||||
|
|
||||||
|
namespace GermanApp.Application.Services;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Application service for managing CEFR levels.
|
||||||
|
/// This is part of the Application layer.
|
||||||
|
/// </summary>
|
||||||
|
public class LevelService
|
||||||
|
{
|
||||||
|
private readonly ILevelRepository _levelRepository;
|
||||||
|
|
||||||
|
public LevelService(ILevelRepository levelRepository)
|
||||||
|
{
|
||||||
|
_levelRepository = levelRepository;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets all CEFR levels ordered by their sort order.
|
||||||
|
/// </summary>
|
||||||
|
public async Task<IReadOnlyList<LevelDto>> GetAllLevelsAsync(CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
var levels = await _levelRepository.GetAllOrderedAsync(cancellationToken);
|
||||||
|
return levels.Select(l => l.ToDto()).ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets a level by its ID.
|
||||||
|
/// </summary>
|
||||||
|
public async Task<LevelDto?> GetLevelByIdAsync(int id, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
var level = await _levelRepository.GetByIdAsync(id, cancellationToken);
|
||||||
|
return level?.ToDto();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets a level by its code (e.g., "A1", "B2").
|
||||||
|
/// </summary>
|
||||||
|
public async Task<LevelDto?> GetLevelByCodeAsync(string code, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
var level = await _levelRepository.GetByCodeAsync(code, cancellationToken);
|
||||||
|
return level?.ToDto();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a new CEFR level.
|
||||||
|
/// </summary>
|
||||||
|
public async Task<LevelDto> CreateLevelAsync(CreateLevelDto dto, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
var level = dto.ToEntity();
|
||||||
|
var createdLevel = await _levelRepository.AddAsync(level, cancellationToken);
|
||||||
|
return createdLevel.ToDto();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Updates an existing CEFR level.
|
||||||
|
/// </summary>
|
||||||
|
public async Task<LevelDto?> UpdateLevelAsync(int id, UpdateLevelDto dto, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
var level = await _levelRepository.GetByIdAsync(id, cancellationToken);
|
||||||
|
if (level == null)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
level.UpdateFromDto(dto);
|
||||||
|
await _levelRepository.UpdateAsync(level, cancellationToken);
|
||||||
|
return level.ToDto();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Deletes a CEFR level by its ID.
|
||||||
|
/// </summary>
|
||||||
|
public async Task<bool> DeleteLevelAsync(int id, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
var level = await _levelRepository.GetByIdAsync(id, cancellationToken);
|
||||||
|
if (level == null)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
await _levelRepository.DeleteAsync(level, cancellationToken);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the first level (lowest order number) - typically A1.
|
||||||
|
/// </summary>
|
||||||
|
public async Task<LevelDto?> GetFirstLevelAsync(CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
var level = await _levelRepository.GetFirstLevelAsync(cancellationToken);
|
||||||
|
return level?.ToDto();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the next level after the specified one.
|
||||||
|
/// </summary>
|
||||||
|
public async Task<LevelDto?> GetNextLevelAsync(int currentLevelId, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
var level = await _levelRepository.GetNextLevelAsync(currentLevelId, cancellationToken);
|
||||||
|
return level?.ToDto();
|
||||||
|
}
|
||||||
|
}
|
||||||
206
GermanApp/Application/Services/ProgressService.cs
Normal file
206
GermanApp/Application/Services/ProgressService.cs
Normal file
|
|
@ -0,0 +1,206 @@
|
||||||
|
using GermanApp.Application.DTOs;
|
||||||
|
using GermanApp.Domain.Entities;
|
||||||
|
using GermanApp.Domain.Interfaces;
|
||||||
|
|
||||||
|
namespace GermanApp.Application.Services;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Application service for managing user progress through lessons.
|
||||||
|
/// This is part of the Application layer.
|
||||||
|
/// </summary>
|
||||||
|
public class ProgressService
|
||||||
|
{
|
||||||
|
private readonly IUserProgressRepository _userProgressRepository;
|
||||||
|
private readonly ILessonRepository _lessonRepository;
|
||||||
|
private readonly ILevelRepository _levelRepository;
|
||||||
|
|
||||||
|
public ProgressService(
|
||||||
|
IUserProgressRepository userProgressRepository,
|
||||||
|
ILessonRepository lessonRepository,
|
||||||
|
ILevelRepository levelRepository)
|
||||||
|
{
|
||||||
|
_userProgressRepository = userProgressRepository;
|
||||||
|
_lessonRepository = lessonRepository;
|
||||||
|
_levelRepository = levelRepository;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets user progress for a specific lesson.
|
||||||
|
/// </summary>
|
||||||
|
public async Task<UserProgressDto?> GetUserProgressAsync(int userId, int lessonId, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
var progress = await _userProgressRepository.GetByUserAndLessonAsync(userId, lessonId, cancellationToken);
|
||||||
|
return progress?.ToDto();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets all progress records for a user.
|
||||||
|
/// </summary>
|
||||||
|
public async Task<IReadOnlyList<UserProgressDto>> GetUserProgressAsync(int userId, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
var progressRecords = await _userProgressRepository.GetByUserAsync(userId, cancellationToken);
|
||||||
|
return progressRecords.Select(p => p.ToDto()).ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets progress for all lessons in a specific level for a user.
|
||||||
|
/// </summary>
|
||||||
|
public async Task<IReadOnlyList<UserProgressDto>> GetUserProgressByLevelAsync(int userId, int levelId, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
var progressRecords = await _userProgressRepository.GetByUserAndLevelAsync(userId, levelId, cancellationToken);
|
||||||
|
return progressRecords.Select(p => p.ToDto()).ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Checks if a user has completed a lesson.
|
||||||
|
/// </summary>
|
||||||
|
public async Task<bool> HasUserCompletedLessonAsync(int userId, int lessonId, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
return await _userProgressRepository.HasUserCompletedLessonAsync(userId, lessonId, cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Marks a lesson as completed for a user with a quiz score.
|
||||||
|
/// </summary>
|
||||||
|
public async Task<UserProgressDto> MarkLessonAsCompletedAsync(int userId, int lessonId, int quizScore, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
var progress = await _userProgressRepository.GetByUserAndLessonAsync(userId, lessonId, cancellationToken);
|
||||||
|
|
||||||
|
if (progress == null)
|
||||||
|
{
|
||||||
|
// Create new progress record
|
||||||
|
progress = UserProgress.Create(userId, lessonId);
|
||||||
|
progress.MarkAsCompleted(quizScore);
|
||||||
|
await _userProgressRepository.AddAsync(progress, cancellationToken);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Update existing progress record
|
||||||
|
progress.MarkAsCompleted(quizScore);
|
||||||
|
await _userProgressRepository.UpdateAsync(progress, cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
return progress.ToDto();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Updates user progress for a lesson without marking as completed.
|
||||||
|
/// </summary>
|
||||||
|
public async Task<UserProgressDto> UpdateUserProgressAsync(int userId, UpdateUserProgressDto dto, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
var progress = await _userProgressRepository.GetByUserAndLessonAsync(userId, dto.LessonId, cancellationToken);
|
||||||
|
|
||||||
|
if (progress == null)
|
||||||
|
{
|
||||||
|
// Create new progress record
|
||||||
|
progress = dto.ToEntity(userId);
|
||||||
|
await _userProgressRepository.AddAsync(progress, cancellationToken);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Update existing progress record
|
||||||
|
progress.UpdateFromDto(dto);
|
||||||
|
await _userProgressRepository.UpdateAsync(progress, cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
return progress.ToDto();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Resets user progress for a lesson.
|
||||||
|
/// </summary>
|
||||||
|
public async Task<bool> ResetLessonProgressAsync(int userId, int lessonId, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
var progress = await _userProgressRepository.GetByUserAndLessonAsync(userId, lessonId, cancellationToken);
|
||||||
|
if (progress == null)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
progress.Reset();
|
||||||
|
await _userProgressRepository.UpdateAsync(progress, cancellationToken);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the user's average score for a level.
|
||||||
|
/// </summary>
|
||||||
|
public async Task<double> GetAverageScoreForLevelAsync(int userId, int levelId, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
return await _userProgressRepository.GetAverageScoreForLevelAsync(userId, levelId, cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the percentage of lessons completed in a level.
|
||||||
|
/// </summary>
|
||||||
|
public async Task<double> GetLevelCompletionPercentageAsync(int userId, int levelId, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
return await _userProgressRepository.GetLevelCompletionPercentageAsync(userId, levelId, cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets level completion summaries for a user.
|
||||||
|
/// </summary>
|
||||||
|
public async Task<IReadOnlyList<LevelCompletionDto>> GetLevelCompletionsAsync(int userId, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
var levels = await _levelRepository.GetAllOrderedAsync(cancellationToken);
|
||||||
|
var completions = new List<LevelCompletionDto>();
|
||||||
|
|
||||||
|
foreach (var level in levels)
|
||||||
|
{
|
||||||
|
var lessons = await _lessonRepository.GetByLevelAsync(level.Id, cancellationToken);
|
||||||
|
var completedCount = 0;
|
||||||
|
|
||||||
|
foreach (var lesson in lessons)
|
||||||
|
{
|
||||||
|
var hasCompleted = await _userProgressRepository.HasUserCompletedLessonAsync(userId, lesson.Id, cancellationToken);
|
||||||
|
if (hasCompleted)
|
||||||
|
completedCount++;
|
||||||
|
}
|
||||||
|
|
||||||
|
var percentage = lessons.Count > 0
|
||||||
|
? (double)completedCount / lessons.Count * 100
|
||||||
|
: 0;
|
||||||
|
|
||||||
|
completions.Add(new LevelCompletionDto(
|
||||||
|
level.Id,
|
||||||
|
level.Name,
|
||||||
|
level.Code,
|
||||||
|
lessons.Count,
|
||||||
|
completedCount,
|
||||||
|
percentage
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
return completions;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets lessons that the user can access (completed previous lessons or first in level).
|
||||||
|
/// </summary>
|
||||||
|
public async Task<IReadOnlyList<LessonDto>> GetAccessibleLessonsAsync(int userId, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
var lessons = await _lessonRepository.GetAccessibleLessonsAsync(userId, cancellationToken);
|
||||||
|
return lessons.Select(l => l.ToDto()).ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Checks if the next lesson in a level is unlocked for the user.
|
||||||
|
/// </summary>
|
||||||
|
public async Task<bool> IsNextLessonUnlockedAsync(int userId, int currentLessonId, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
var currentLesson = await _lessonRepository.GetByIdAsync(currentLessonId, cancellationToken);
|
||||||
|
if (currentLesson == null)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
// If this is the first lesson in the level, the next one is always unlocked
|
||||||
|
var firstLesson = await _lessonRepository.GetFirstLessonInLevelAsync(currentLesson.LevelId, cancellationToken);
|
||||||
|
if (firstLesson?.Id == currentLessonId)
|
||||||
|
{
|
||||||
|
var nextLesson = await _lessonRepository.GetNextLessonAsync(currentLessonId, cancellationToken);
|
||||||
|
return nextLesson != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the user has completed the current lesson
|
||||||
|
var hasCompleted = await _userProgressRepository.HasUserCompletedLessonAsync(userId, currentLessonId, cancellationToken);
|
||||||
|
return hasCompleted;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
using GermanApp.Application.DTOs;
|
using GermanApp.Application.DTOs;
|
||||||
|
using GermanApp.Application.Services;
|
||||||
using GermanApp.Application.Interfaces;
|
using GermanApp.Application.Interfaces;
|
||||||
using GermanApp.Application.UseCases.Commands;
|
using GermanApp.Application.UseCases.Commands;
|
||||||
using GermanApp.Domain.Entities;
|
using GermanApp.Domain.Entities;
|
||||||
|
|
@ -115,13 +116,20 @@ try
|
||||||
});
|
});
|
||||||
|
|
||||||
// Register repositories (Infrastructure implementations of Domain interfaces)
|
// Register repositories (Infrastructure implementations of Domain interfaces)
|
||||||
|
builder.Services.AddScoped<ILevelRepository, LevelRepository>();
|
||||||
builder.Services.AddScoped<ILessonRepository, LessonRepository>();
|
builder.Services.AddScoped<ILessonRepository, LessonRepository>();
|
||||||
|
builder.Services.AddScoped<IUserProgressRepository, UserProgressRepository>();
|
||||||
|
|
||||||
// ============================================
|
// ============================================
|
||||||
// APPLICATION LAYER - Use Cases & Services
|
// APPLICATION LAYER - Use Cases & Services
|
||||||
// ============================================
|
// ============================================
|
||||||
|
|
||||||
// Register command handlers
|
// Register command handlers
|
||||||
|
|
||||||
|
// Register application services
|
||||||
|
builder.Services.AddScoped<LevelService>();
|
||||||
|
builder.Services.AddScoped<LessonService>();
|
||||||
|
builder.Services.AddScoped<ProgressService>();
|
||||||
builder.Services.AddScoped<ICommandHandler<CreateLessonCommand, LessonDto>, CreateLessonCommandHandler>();
|
builder.Services.AddScoped<ICommandHandler<CreateLessonCommand, LessonDto>, CreateLessonCommandHandler>();
|
||||||
|
|
||||||
var app = builder.Build();
|
var app = builder.Build();
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue