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.Services;
|
||||
using GermanApp.Application.Interfaces;
|
||||
using GermanApp.Application.UseCases.Commands;
|
||||
using GermanApp.Domain.Entities;
|
||||
|
|
@ -116,20 +115,13 @@ try
|
|||
});
|
||||
|
||||
// Register repositories (Infrastructure implementations of Domain interfaces)
|
||||
builder.Services.AddScoped<ILevelRepository, LevelRepository>();
|
||||
builder.Services.AddScoped<ILessonRepository, LessonRepository>();
|
||||
builder.Services.AddScoped<IUserProgressRepository, UserProgressRepository>();
|
||||
|
||||
// ============================================
|
||||
// APPLICATION LAYER - Use Cases & Services
|
||||
// ============================================
|
||||
|
||||
// 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>();
|
||||
|
||||
var app = builder.Build();
|
||||
|
|
|
|||
|
|
@ -91,7 +91,7 @@ Implement the core backend functionality, including lesson management, AI servic
|
|||
### Features
|
||||
| # | 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.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 |
|
||||
|
|
|
|||
|
|
@ -12,8 +12,6 @@
|
|||
|
||||
**Phase 1: Database & Models - COMPLETED ✅**
|
||||
|
||||
**Phase 2: Backend Services - COMPLETED ✅**
|
||||
|
||||
---
|
||||
|
||||
## 📌 Overview
|
||||
|
|
@ -144,12 +142,12 @@ Each lesson contains:
|
|||
- [x] Seed initial A1 level and lessons
|
||||
|
||||
### Phase 2: Backend Services (3-5 hours)
|
||||
- [x] Create LevelService with CRUD operations
|
||||
- [x] Create LessonService with CRUD operations
|
||||
- [x] Create ProgressService to track user progress
|
||||
- [x] Implement sequential unlocking logic
|
||||
- [x] Create DTOs for Level, Lesson, UserProgress
|
||||
- [x] Create mapping profiles (manual extension methods)
|
||||
- [ ] Create LevelService with CRUD operations
|
||||
- [ ] Create LessonService with CRUD operations
|
||||
- [ ] Create ProgressService to track user progress
|
||||
- [ ] Implement sequential unlocking logic
|
||||
- [ ] Create DTOs for Level, Lesson, UserProgress
|
||||
- [ ] Create mapping profiles (AutoMapper or manual)
|
||||
|
||||
### Phase 3: API Controllers (2-3 hours)
|
||||
- [ ] Create LevelsController
|
||||
|
|
@ -194,10 +192,7 @@ Each lesson contains:
|
|||
- [x] Create Infrastructure/Data/Repositories/UserProgressRepository.cs
|
||||
- [x] Create Application/DTOs/LevelDto.cs
|
||||
- [x] Create Application/DTOs/LessonDto.cs
|
||||
- [x] 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/DTOs/UserProgressDto.cs
|
||||
- [ ] Create Application/Services/LevelService.cs
|
||||
- [ ] Create Application/Services/LessonService.cs
|
||||
- [ ] Create Application/Services/ProgressService.cs
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue