using System.Text.Json; using Microsoft.EntityFrameworkCore; using Npgsql; using Tiku.Application.Learning; using Tiku.Domain.Common; using Tiku.Domain.Content; using Tiku.Domain.Learning; namespace Tiku.Infrastructure.Learning; internal sealed class PracticeReportService(LearningServiceDependencies dependencies) : LearningActivityServiceBase(dependencies), IPracticeReportService { public async Task GetPracticeSessionReportAsync( LearningActor actor, PracticeSessionFilter filter, CancellationToken cancellationToken = default) { if (!filter.PracticeSessionId.HasValue) throw new LearningValidationException("practice_session_id_required", "Practice session id is required."); var report = await learningPersistence.PracticeSessionReports .AsNoTracking() .SingleOrDefaultAsync( item => item.TenantId == actor.TenantId && item.UserId == actor.UserId && item.PracticeSessionId == filter.PracticeSessionId.Value, cancellationToken); if (report is null) throw new LearningResourceNotFoundException("practice_report_not_found", "Practice session report was not found."); return ToItem(report); } public async Task> GetPracticeReportsAsync( LearningActor actor, PracticeSessionFilter filter, CancellationToken cancellationToken = default) { var query = learningPersistence.PracticeSessionReports .AsNoTracking() .Where(report => report.TenantId == actor.TenantId && report.UserId == actor.UserId); if (filter.BlueprintId.HasValue) query = query.Where(report => report.BlueprintId == filter.BlueprintId.Value); if (!string.IsNullOrWhiteSpace(filter.Mode)) query = query.Where(report => report.Mode == filter.Mode.Trim()); var items = await query .OrderByDescending(report => report.SubmittedAt) .Take(ResolveLimit(filter.Limit)) .ToArrayAsync(cancellationToken); return new LearningList(items.Select(ToItem).ToArray()); } public async Task> GetPracticeHistoryAsync( LearningActor actor, PracticeSessionFilter filter, CancellationToken cancellationToken = default) { var query = learningPersistence.PracticeSessions .AsNoTracking() .Where(session => session.TenantId == actor.TenantId && session.UserId == actor.UserId); if (!string.IsNullOrWhiteSpace(filter.Mode)) query = query.Where(session => session.Mode == filter.Mode.Trim()); var rows = await query .GroupJoin( learningPersistence.PracticeSessionReports.AsNoTracking(), session => new { session.TenantId, PracticeSessionId = session.Id }, report => new { report.TenantId, report.PracticeSessionId }, (session, reports) => new { session, report = reports.FirstOrDefault() }) .OrderByDescending(row => row.session.FinishedAt ?? row.session.StartedAt) .Take(ResolveLimit(filter.Limit)) .ToArrayAsync(cancellationToken); var now = DateTimeOffset.UtcNow; var items = rows .Select(row => new PracticeHistoryItem( row.session.Id, row.session.Mode, row.session.TargetType, row.session.TargetId, row.session.BlueprintId, row.session.CollectionId, row.session.EntryId, row.session.ContentNodeId, row.session.QuestionCount, row.report?.AnsweredCount ?? 0, row.report?.CorrectCount ?? 0, row.report?.WrongCount ?? 0, row.session.StartedAt, row.session.FinishedAt, row.session.ExpiresAt, ResolvePracticeSessionHistoryStatus(row.session, now), row.report?.Id, row.report?.Score, row.report?.TotalScore, row.report?.Accuracy)) .Where(item => string.IsNullOrWhiteSpace(filter.Status) || item.Status == filter.Status.Trim()) .ToArray(); return new LearningList(items); } }