Compare commits
No commits in common. "a9e9a6e457479106be01a4ade5bb2ef091a7305f" and "8e05c2b6e2cc5c517ccd85f02d9125c665f38c1a" have entirely different histories.
a9e9a6e457
...
8e05c2b6e2
8 changed files with 8 additions and 618 deletions
|
|
@ -1,51 +0,0 @@
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,80 +0,0 @@
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,159 +0,0 @@
|
||||||
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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,101 +0,0 @@
|
||||||
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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,206 +0,0 @@
|
||||||
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,5 +1,4 @@
|
||||||
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;
|
||||||
|
|
@ -116,20 +115,13 @@ 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();
|
||||||
|
|
|
||||||
|
|
@ -91,7 +91,7 @@ Implement the core backend functionality, including lesson management, AI servic
|
||||||
### Features
|
### Features
|
||||||
| # | Feature | Description | Hours | Status | Dependencies |
|
| # | Feature | Description | Hours | Status | Dependencies |
|
||||||
|---|---------|-------------|-------|--------|--------------|
|
|---|---------|-------------|-------|--------|--------------|
|
||||||
| 2.1 | [Lesson Management](features/lesson-management.md) | Lessons, levels, progress tracking | 10-16h | 🚀 In Progress (Phase 1 & 2 ✅) | Phase 1 |
|
| 2.1 | [Lesson Management](features/lesson-management.md) | Lessons, levels, progress tracking | 10-16h | 🚀 In Progress (Phase 1: DB & Models ✅) | Phase 1 |
|
||||||
| 2.2 | [AI Services](features/ai-services.md) | Mistral, Vosk, Coqui TTS integration | 10-16h | ⏳ Planned | Phase 1 |
|
| 2.2 | [AI Services](features/ai-services.md) | Mistral, Vosk, Coqui TTS integration | 10-16h | ⏳ Planned | Phase 1 |
|
||||||
| 2.3 | [Vocabulary System](features/vocabulary-system.md) | Word storage, audio, import | 8-12h | ⏳ Planned | Phase 1, 2.1 |
|
| 2.3 | [Vocabulary System](features/vocabulary-system.md) | Word storage, audio, import | 8-12h | ⏳ Planned | Phase 1, 2.1 |
|
||||||
| 2.4 | [Quiz System](features/quiz-system.md) | Multiple question types, scoring | 6-10h | ⏳ Planned | Phase 1, 2.1 |
|
| 2.4 | [Quiz System](features/quiz-system.md) | Multiple question types, scoring | 6-10h | ⏳ Planned | Phase 1, 2.1 |
|
||||||
|
|
|
||||||
|
|
@ -12,8 +12,6 @@
|
||||||
|
|
||||||
**Phase 1: Database & Models - COMPLETED ✅**
|
**Phase 1: Database & Models - COMPLETED ✅**
|
||||||
|
|
||||||
**Phase 2: Backend Services - COMPLETED ✅**
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 📌 Overview
|
## 📌 Overview
|
||||||
|
|
@ -144,12 +142,12 @@ Each lesson contains:
|
||||||
- [x] Seed initial A1 level and lessons
|
- [x] Seed initial A1 level and lessons
|
||||||
|
|
||||||
### Phase 2: Backend Services (3-5 hours)
|
### Phase 2: Backend Services (3-5 hours)
|
||||||
- [x] Create LevelService with CRUD operations
|
- [ ] Create LevelService with CRUD operations
|
||||||
- [x] Create LessonService with CRUD operations
|
- [ ] Create LessonService with CRUD operations
|
||||||
- [x] Create ProgressService to track user progress
|
- [ ] Create ProgressService to track user progress
|
||||||
- [x] Implement sequential unlocking logic
|
- [ ] Implement sequential unlocking logic
|
||||||
- [x] Create DTOs for Level, Lesson, UserProgress
|
- [ ] Create DTOs for Level, Lesson, UserProgress
|
||||||
- [x] Create mapping profiles (manual extension methods)
|
- [ ] Create mapping profiles (AutoMapper or manual)
|
||||||
|
|
||||||
### Phase 3: API Controllers (2-3 hours)
|
### Phase 3: API Controllers (2-3 hours)
|
||||||
- [ ] Create LevelsController
|
- [ ] Create LevelsController
|
||||||
|
|
@ -194,10 +192,7 @@ Each lesson contains:
|
||||||
- [x] Create Infrastructure/Data/Repositories/UserProgressRepository.cs
|
- [x] Create Infrastructure/Data/Repositories/UserProgressRepository.cs
|
||||||
- [x] Create Application/DTOs/LevelDto.cs
|
- [x] Create Application/DTOs/LevelDto.cs
|
||||||
- [x] Create Application/DTOs/LessonDto.cs
|
- [x] Create Application/DTOs/LessonDto.cs
|
||||||
- [x] Create Application/DTOs/UserProgressDto.cs
|
- [ ] Create Application/DTOs/UserProgressDto.cs
|
||||||
- [x] Create Application/Services/LevelService.cs
|
|
||||||
- [x] Create Application/Services/LessonService.cs
|
|
||||||
- [x] Create Application/Services/ProgressService.cs
|
|
||||||
- [ ] Create Application/Services/LevelService.cs
|
- [ ] Create Application/Services/LevelService.cs
|
||||||
- [ ] Create Application/Services/LessonService.cs
|
- [ ] Create Application/Services/LessonService.cs
|
||||||
- [ ] Create Application/Services/ProgressService.cs
|
- [ ] Create Application/Services/ProgressService.cs
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue