using Bsevita.Library.Api.Data.Generated; using Bsevita.Library.Models.Common; using Bsevita.Library.Models.Loans; using Bsevita.Library.Models.Reports; using Microsoft.EntityFrameworkCore; namespace Bsevita.Library.Api.Services; public sealed class ReportService(LibraryDbContext dbContext, TimeProvider timeProvider) { public async Task GetReportsAsync(CancellationToken cancellationToken) { var activeLoans = await GetActiveLoansAsync(0, LibraryRules.DefaultPageSize, cancellationToken); var overdueLoans = await GetOverdueAsync(0, LibraryRules.DefaultPageSize, cancellationToken); return new ReportsDto( activeLoans, overdueLoans, await GetActiveStudentsAsync(0, LibraryRules.DefaultPageSize, cancellationToken), await GetStatisticsAsync(cancellationToken)); } public async Task> GetActiveLoansAsync( int skip, int take, CancellationToken cancellationToken) { var now = timeProvider.GetUtcNow(); var query = dbContext.Loans.AsNoTracking() .Where(loan => loan.ReturnedAt == null) .OrderBy(loan => loan.DueAt) .Skip(Math.Max(0, skip)) .Take(Math.Clamp(take, 1, LibraryRules.MaxPageSize)); var loans = await query .Select(loan => new { loan.LoanId, loan.StudentId, StudentName = loan.Student.FirstName + " " + loan.Student.LastName, loan.Student.CardNumber, loan.BookId, BookTitle = loan.Book.Title, loan.Book.BookNumber, loan.LoanedAt, loan.DueAt, loan.ReturnedAt }) .ToListAsync(cancellationToken); return loans.Select(loan => ToLoanDto( loan.LoanId, loan.StudentId, loan.StudentName, loan.CardNumber, loan.BookId, loan.BookTitle, loan.BookNumber, loan.LoanedAt, loan.DueAt, loan.ReturnedAt, now)).ToList(); } public Task> GetActiveLoansAsync(CancellationToken cancellationToken) => GetActiveLoansAsync(0, LibraryRules.DefaultPageSize, cancellationToken); public async Task> GetOverdueAsync( int skip, int take, CancellationToken cancellationToken) { var now = timeProvider.GetUtcNow(); var query = dbContext.Loans.AsNoTracking() .Where(loan => loan.ReturnedAt == null && loan.DueAt < now) .OrderBy(loan => loan.DueAt) .Skip(Math.Max(0, skip)) .Take(Math.Clamp(take, 1, LibraryRules.MaxPageSize)); var loans = await query .Select(loan => new { loan.LoanId, loan.StudentId, StudentName = loan.Student.FirstName + " " + loan.Student.LastName, loan.Student.CardNumber, loan.BookId, BookTitle = loan.Book.Title, loan.Book.BookNumber, loan.LoanedAt, loan.DueAt, loan.ReturnedAt }) .ToListAsync(cancellationToken); return loans.Select(loan => ToLoanDto( loan.LoanId, loan.StudentId, loan.StudentName, loan.CardNumber, loan.BookId, loan.BookTitle, loan.BookNumber, loan.LoanedAt, loan.DueAt, loan.ReturnedAt, now)).ToList(); } public Task> GetOverdueAsync(CancellationToken cancellationToken) => GetOverdueAsync(0, LibraryRules.DefaultPageSize, cancellationToken); public async Task> GetActiveStudentsAsync( int skip, int take, CancellationToken cancellationToken) => await dbContext.Students.AsNoTracking() .Where(student => student.IsActive && student.Loans.Any(loan => loan.ReturnedAt == null)) .OrderBy(student => student.LastName) .ThenBy(student => student.FirstName) .Skip(Math.Max(0, skip)) .Take(Math.Clamp(take, 1, LibraryRules.MaxPageSize)) .Select(student => new ActiveStudentDto( student.StudentId, student.FirstName + " " + student.LastName, student.CardNumber, student.ClassName, student.Loans.Count(loan => loan.ReturnedAt == null))) .ToListAsync(cancellationToken); public Task> GetActiveStudentsAsync(CancellationToken cancellationToken) => GetActiveStudentsAsync(0, LibraryRules.DefaultPageSize, cancellationToken); public async Task GetStatisticsAsync(CancellationToken cancellationToken) { var now = timeProvider.GetUtcNow(); var totalBooks = await dbContext.Books.CountAsync(book => book.IsActive, cancellationToken); var activeLoans = await dbContext.Loans.CountAsync(loan => loan.ReturnedAt == null, cancellationToken); var availableBooks = await dbContext.Books.CountAsync( book => book.IsActive && !book.Loans.Any(loan => loan.ReturnedAt == null), cancellationToken); var totalStudents = await dbContext.Students.CountAsync(student => student.IsActive, cancellationToken); var overdueLoans = await dbContext.Loans.CountAsync(loan => loan.ReturnedAt == null && loan.DueAt < now, cancellationToken); return new LibraryStatisticsDto(totalBooks, availableBooks, totalStudents, activeLoans, overdueLoans); } private static LoanDto ToLoanDto( Guid Id, Guid StudentId, string StudentName, string CardNumber, Guid BookId, string BookTitle, string BookNumber, DateTimeOffset LoanedAt, DateTimeOffset DueAt, DateTimeOffset? ReturnedAt, DateTimeOffset now) { var isOverdue = ReturnedAt is null && DueAt < now; var daysOverdue = isOverdue ? Math.Max(1, (int)Math.Floor((now - DueAt).TotalDays)) : 0; return new LoanDto(Id, StudentId, StudentName, CardNumber, BookId, BookTitle, BookNumber, LoanedAt, DueAt, ReturnedAt, isOverdue, daysOverdue); } }