using iknow_api.Data; using iknow_api.DTOs; using iknow_api.Models; using Microsoft.EntityFrameworkCore; namespace iknow_api.Services { /// /// Backs UC012 (subjects), UC013 (active semesters) and UC014 (professor /// assignments). Every method reports constraint problems as a sentence /// rather than letting a database exception reach the controller. /// public class AdminService : IAdminService { private readonly AppDbContext _context; public AdminService(AppDbContext context) { _context = context; } private static string SemesterName(ActiveSemesters s) => $"{(s.Type == sType.winter ? "Зимски" : "Летен")}({s.Year}/{s.Year + 1})"; // ============================================================ // UC012 - subjects // ============================================================ public async Task> GetSubjectsAsync() { var subjects = await _context.Subjects.AsNoTracking().ToListAsync(); var majorLinks = await _context.MajorSubjects.AsNoTracking() .Include(ms => ms.Majors).ToListAsync(); var prerequisites = await _context.DependencySubjects.AsNoTracking().ToListAsync(); // A subject that students already took cannot be deleted. var enrolledCounts = await _context.SemesterSubjects.AsNoTracking() .GroupBy(ss => ss.SubjectId) .Select(g => new { SubjectId = g.Key, Count = g.Count() }) .ToDictionaryAsync(x => x.SubjectId, x => x.Count); var byId = subjects.ToDictionary(s => s.Id); return subjects .OrderBy(s => s.Code) .Select(s => new AdminSubjectDto { Id = s.Id, Name = s.Name, Code = s.Code, AwardedCredits = s.AwardedCredits ?? 0, DependencyCredit = s.DependencyCredit, EnrolledCount = enrolledCounts.TryGetValue(s.Id, out var c) ? c : 0, Majors = majorLinks .Where(ms => ms.SubjectId == s.Id) .Select(ms => new SubjectMajorDto { MajorId = ms.MajorId, MajorName = ms.Majors?.Name, MandatorySemester = ms.MandatorySemester }) .OrderBy(m => m.MajorName) .ToList(), Prerequisites = prerequisites .Where(d => d.SubjectId == s.Id && byId.ContainsKey(d.DependencyId)) .Select(d => new SubjectRefDto { Id = d.DependencyId, Name = byId[d.DependencyId].Name, Code = byId[d.DependencyId].Code }) .OrderBy(p => p.Code) .ToList() }) .ToList(); } public async Task CreateSubjectAsync(SubjectWriteDto request) { if (string.IsNullOrWhiteSpace(request.Name) || string.IsNullOrWhiteSpace(request.Code)) { return Fail("Name and code are required."); } if (request.AwardedCredits <= 0) { return Fail("Awarded credits must be greater than zero."); } // subjects.name and subjects.code are both UNIQUE. if (await _context.Subjects.AnyAsync(s => s.Name == request.Name)) { return Fail($"A subject named '{request.Name}' already exists."); } if (await _context.Subjects.AnyAsync(s => s.Code == request.Code)) { return Fail($"Subject code '{request.Code}' is already taken."); } var subject = new Subject { Name = request.Name, Code = request.Code, AwardedCredits = request.AwardedCredits, DependencyCredit = request.DependencyCredit }; _context.Subjects.Add(subject); await _context.SaveChangesAsync(); return Ok($"Subject '{subject.Name}' created.", subject.Id); } public async Task UpdateSubjectAsync(int subjectId, SubjectWriteDto request) { var subject = await _context.Subjects.FirstOrDefaultAsync(s => s.Id == subjectId); if (subject is null) return Fail("Subject not found."); if (string.IsNullOrWhiteSpace(request.Name) || string.IsNullOrWhiteSpace(request.Code)) { return Fail("Name and code are required."); } if (request.AwardedCredits <= 0) { return Fail("Awarded credits must be greater than zero."); } if (await _context.Subjects.AnyAsync(s => s.Name == request.Name && s.Id != subjectId)) { return Fail($"Another subject is already named '{request.Name}'."); } if (await _context.Subjects.AnyAsync(s => s.Code == request.Code && s.Id != subjectId)) { return Fail($"Subject code '{request.Code}' is already taken."); } subject.Name = request.Name; subject.Code = request.Code; subject.AwardedCredits = request.AwardedCredits; subject.DependencyCredit = request.DependencyCredit; await _context.SaveChangesAsync(); return Ok($"Subject '{subject.Name}' updated.", subject.Id); } public async Task DeleteSubjectAsync(int subjectId) { var subject = await _context.Subjects.FirstOrDefaultAsync(s => s.Id == subjectId); if (subject is null) return Fail("Subject not found."); // semesters_subjects references subjects, so a taken subject cannot go. var taken = await _context.SemesterSubjects.CountAsync(ss => ss.SubjectId == subjectId); if (taken > 0) { return Fail($"Cannot delete: {taken} enrolment(s) already contain this subject."); } await using var transaction = await _context.Database.BeginTransactionAsync(); try { // The mapping rows must go first; their foreign keys would block the delete. var prereqs = await _context.DependencySubjects .Where(d => d.SubjectId == subjectId || d.DependencyId == subjectId) .ToListAsync(); _context.DependencySubjects.RemoveRange(prereqs); var majors = await _context.MajorSubjects .Where(ms => ms.SubjectId == subjectId).ToListAsync(); _context.MajorSubjects.RemoveRange(majors); var teaching = await _context.ProfessorSubjects .Where(ps => ps.SubjectId == subjectId).ToListAsync(); _context.ProfessorSubjects.RemoveRange(teaching); _context.Subjects.Remove(subject); await _context.SaveChangesAsync(); await transaction.CommitAsync(); return Ok($"Subject '{subject.Name}' deleted."); } catch { await transaction.RollbackAsync(); throw; } } public async Task LinkMajorAsync(int subjectId, MajorLinkDto request) { if (!await _context.Subjects.AnyAsync(s => s.Id == subjectId)) return Fail("Subject not found."); if (!await _context.Majors.AnyAsync(m => m.Id == request.MajorId)) return Fail("Study programme not found."); if (request.MandatorySemester < 1) return Fail("Semester must be 1 or greater."); var existing = await _context.MajorSubjects .FirstOrDefaultAsync(ms => ms.SubjectId == subjectId && ms.MajorId == request.MajorId); if (existing is not null) { existing.MandatorySemester = request.MandatorySemester; await _context.SaveChangesAsync(); return Ok("Study programme mapping updated."); } _context.MajorSubjects.Add(new MajorSubjects { SubjectId = subjectId, MajorId = request.MajorId, MandatorySemester = request.MandatorySemester }); await _context.SaveChangesAsync(); return Ok("Subject added to the study programme."); } public async Task UnlinkMajorAsync(int subjectId, int majorId) { var row = await _context.MajorSubjects .FirstOrDefaultAsync(ms => ms.SubjectId == subjectId && ms.MajorId == majorId); if (row is null) return Fail("That subject is not in that study programme."); _context.MajorSubjects.Remove(row); await _context.SaveChangesAsync(); return Ok("Subject removed from the study programme."); } public async Task AddPrerequisiteAsync(int subjectId, PrerequisiteDto request) { if (subjectId == request.DependencyId) { // Mirrors CHECK (subject_id <> dependency_id). return Fail("A subject cannot be its own prerequisite."); } if (!await _context.Subjects.AnyAsync(s => s.Id == subjectId)) return Fail("Subject not found."); if (!await _context.Subjects.AnyAsync(s => s.Id == request.DependencyId)) return Fail("Prerequisite subject not found."); if (await _context.DependencySubjects.AnyAsync( d => d.SubjectId == subjectId && d.DependencyId == request.DependencyId)) return Fail("That prerequisite is already set."); // Refuse a direct cycle: A requires B while B already requires A. if (await _context.DependencySubjects.AnyAsync( d => d.SubjectId == request.DependencyId && d.DependencyId == subjectId)) return Fail("That would create a circular prerequisite."); _context.DependencySubjects.Add(new DependencySubject { SubjectId = subjectId, DependencyId = request.DependencyId }); await _context.SaveChangesAsync(); return Ok("Prerequisite added."); } public async Task RemovePrerequisiteAsync(int subjectId, int dependencyId) { var row = await _context.DependencySubjects .FirstOrDefaultAsync(d => d.SubjectId == subjectId && d.DependencyId == dependencyId); if (row is null) return Fail("That prerequisite is not set."); _context.DependencySubjects.Remove(row); await _context.SaveChangesAsync(); return Ok("Prerequisite removed."); } // ============================================================ // UC013 - active semesters // ============================================================ public async Task> GetSemestersAsync() { var semesters = await _context.ActiveSemesters.AsNoTracking().ToListAsync(); var subjects = await _context.Subjects.AsNoTracking().ToListAsync(); var teaching = await _context.ProfessorSubjects.AsNoTracking().ToListAsync(); var enrolments = await _context.EnrolledSemesters.AsNoTracking() .GroupBy(es => es.SemesterId) .Select(g => new { SemesterId = g.Key, Count = g.Count() }) .ToDictionaryAsync(x => x.SemesterId, x => x.Count); return semesters .OrderByDescending(s => s.Year) .ThenByDescending(s => s.Type == sType.summer) .Select(s => { var covered = teaching .Where(t => t.SemesterId == s.Id) .Select(t => t.SubjectId) .ToHashSet(); return new AdminSemesterDto { Id = s.Id, Year = s.Year, Type = s.Type.ToString(), Name = SemesterName(s), EnrolmentCount = enrolments.TryGetValue(s.Id, out var c) ? c : 0, UncoveredSubjects = subjects .Where(sub => !covered.Contains(sub.Id)) .OrderBy(sub => sub.Code) .Select(sub => new SubjectRefDto { Id = sub.Id, Name = sub.Name, Code = sub.Code }) .ToList() }; }) .ToList(); } public async Task CreateSemesterAsync(SemesterWriteDto request) { if (!Enum.TryParse(request.Type, ignoreCase: true, out var type)) { return Fail("Semester type must be 'winter' or 'summer'."); } if (request.Year < 2000 || request.Year > 2100) { return Fail("Year looks wrong; expected something between 2000 and 2100."); } // active_semesters has UNIQUE (year, type). if (await _context.ActiveSemesters.AnyAsync(a => a.Year == request.Year && a.Type == type)) { return Fail("That semester is already open."); } var semester = new ActiveSemesters { Year = request.Year, Type = type }; _context.ActiveSemesters.Add(semester); await _context.SaveChangesAsync(); return Ok($"Semester {SemesterName(semester)} opened.", semester.Id); } // ============================================================ // UC014 - professor assignments // ============================================================ public async Task GetScheduleAsync(int semesterId) { var semester = await _context.ActiveSemesters.AsNoTracking() .FirstOrDefaultAsync(a => a.Id == semesterId); if (semester is null) return null; var subjects = await _context.Subjects.AsNoTracking().ToListAsync(); var professors = await _context.User.AsNoTracking() .Where(u => u.Role == Models.UserRole.Professor).ToListAsync(); var assignments = await _context.ProfessorSubjects.AsNoTracking() .Where(ps => ps.SemesterId == semesterId).ToListAsync(); var subjectById = subjects.ToDictionary(s => s.Id); var profById = professors.ToDictionary(p => p.Id); var covered = assignments.Select(a => a.SubjectId).ToHashSet(); return new ScheduleDto { SemesterId = semester.Id, SemesterName = SemesterName(semester), Assignments = assignments .Where(a => subjectById.ContainsKey(a.SubjectId) && profById.ContainsKey(a.ProfessorId)) .Select(a => new ScheduleRowDto { SubjectId = a.SubjectId, SubjectName = subjectById[a.SubjectId].Name, SubjectCode = subjectById[a.SubjectId].Code, ProfessorId = a.ProfessorId, ProfessorName = $"{profById[a.ProfessorId].Name} {profById[a.ProfessorId].Surname}".Trim() }) .OrderBy(r => r.SubjectCode) .ToList(), Load = professors .Select(p => new ProfessorLoadDto { ProfessorId = p.Id, ProfessorName = $"{p.Name} {p.Surname}".Trim(), Subjects = assignments.Count(a => a.ProfessorId == p.Id) }) .OrderByDescending(l => l.Subjects).ThenBy(l => l.ProfessorName) .ToList(), UncoveredSubjects = subjects .Where(s => !covered.Contains(s.Id)) .OrderBy(s => s.Code) .Select(s => new SubjectRefDto { Id = s.Id, Name = s.Name, Code = s.Code }) .ToList(), AllSubjects = subjects .OrderBy(s => s.Code) .Select(s => new SubjectRefDto { Id = s.Id, Name = s.Name, Code = s.Code }) .ToList(), Professors = professors .OrderBy(p => p.Surname) .Select(p => new ProfessorRefDto { Id = p.Id, Name = $"{p.Name} {p.Surname}".Trim() }) .ToList() }; } public async Task AssignAsync(ScheduleWriteDto request) { var professor = await _context.User .FirstOrDefaultAsync(u => u.Id == request.ProfessorId); if (professor is null) return Fail("Professor not found."); if (professor.Role != Models.UserRole.Professor) return Fail("That user is not a professor."); if (!await _context.ActiveSemesters.AnyAsync(a => a.Id == request.SemesterId)) return Fail("Semester not found."); if (!await _context.Subjects.AnyAsync(s => s.Id == request.SubjectId)) return Fail("Subject not found."); var exists = await _context.ProfessorSubjects.AnyAsync( ps => ps.ProfessorId == request.ProfessorId && ps.SemesterId == request.SemesterId && ps.SubjectId == request.SubjectId); if (exists) return Ok("That assignment already exists."); _context.ProfessorSubjects.Add(new ProfessorSubjects { ProfessorId = request.ProfessorId, SemesterId = request.SemesterId, SubjectId = request.SubjectId }); await _context.SaveChangesAsync(); return Ok("Professor assigned to the subject."); } public async Task UnassignAsync(ScheduleWriteDto request) { var row = await _context.ProfessorSubjects.FirstOrDefaultAsync( ps => ps.ProfessorId == request.ProfessorId && ps.SemesterId == request.SemesterId && ps.SubjectId == request.SubjectId); if (row is null) return Fail("That assignment does not exist."); _context.ProfessorSubjects.Remove(row); await _context.SaveChangesAsync(); return Ok("Assignment removed."); } public async Task> GetMajorsAsync() => await _context.Majors.AsNoTracking() .OrderBy(m => m.Name) .Select(m => new SubjectRefDto { Id = m.Id, Name = m.Name, Code = null }) .ToListAsync(); private static AdminResultDto Ok(string message, int? id = null) => new() { Ok = true, Message = message, Id = id }; private static AdminResultDto Fail(string message) => new() { Ok = false, Message = message }; } }