using Microsoft.EntityFrameworkCore; using Tiku.Application.Catalog; using Tiku.Application.Content; using Tiku.Application.Security; using Tiku.Application.TenantAdmin; using Tiku.Domain.Catalog; using Tiku.Domain.Common; using Tiku.Domain.Tenancy; using Tiku.Infrastructure.Security; namespace Tiku.Infrastructure.TenantAdmin; public sealed partial class TenantAdminDirectService { public async Task GetClassesAsync( TenantAdminActor actor, TenantAdminClassFilter filter, CancellationToken cancellationToken = default) { var scope = await RequireDataScopeAsync(actor, cancellationToken); var regionIds = scope.RegionIds.ToArray(); var classIds = scope.ClassIds.ToArray(); var query = dbContext.TenantClasses.AsNoTracking() .Where(item => item.TenantId == actor.TenantId) .ApplyDataScope( scope, item => item.CreatedBy == actor.UserId, item => classIds.Contains(item.Id) || (item.RegionId.HasValue && regionIds.Contains(item.RegionId.Value))); if (filter.RegionId.HasValue) query = query.Where(item => item.RegionId == filter.RegionId.Value); if (!string.IsNullOrWhiteSpace(filter.Status)) query = query.Where(item => item.Status == ParseEnum(filter.Status, "invalid_class_status")); else query = query.Where(item => item.Status != TenantRecordStatus.Archived); if (!string.IsNullOrWhiteSpace(filter.Keyword)) { var keyword = filter.Keyword.Trim(); query = query.Where(item => item.Name.Contains(keyword) || (item.Code != null && item.Code.Contains(keyword))); } var items = await query .OrderBy(item => item.SortOrder) .ThenByDescending(item => item.CreatedAt) .Take(ResolveLimit(filter.Limit)) .Select(item => new { Class = item, RegionName = dbContext.Regions .Where(region => region.TenantId == actor.TenantId && region.Id == item.RegionId) .Select(region => region.Name) .FirstOrDefault(), StudentCount = dbContext.TenantClassMembers.Count(member => member.TenantId == actor.TenantId && member.ClassId == item.Id && member.Status == TenantClassMemberStatus.Active && member.MemberType == TenantClassMemberType.Student), StaffCount = dbContext.TenantClassMembers.Count(member => member.TenantId == actor.TenantId && member.ClassId == item.Id && member.Status == TenantClassMemberStatus.Active && member.MemberType != TenantClassMemberType.Student) }) .ToArrayAsync(cancellationToken); return new TenantAdminClassList( items.Select(item => ToClassItem(item.Class, item.RegionName, item.StudentCount, item.StaffCount)) .ToArray(), scope.Mode != DataScopeMode.All); } public async Task> UpsertClassAsync( TenantAdminActor actor, UpsertTenantAdminClassCommand command, CancellationToken cancellationToken = default) { var scope = await RequireDataScopeAsync(actor, cancellationToken); ArgumentException.ThrowIfNullOrWhiteSpace(command.Name); await AssertReferenceAsync(actor.TenantId, command.RegionId, "region_not_found", cancellationToken); var item = await ResolveTenantEntityAsync(dbContext.TenantClasses, actor.TenantId, command.Id, command.LegacyId, cancellationToken); var isNew = item is null; if (item is not null && !scope.AllowsResource(actor.UserId, item.CreatedBy, item.RegionId, item.Id)) throw new TenantAdminDirectException("Class was not found.", "class_not_found"); if (item is null && !scope.AllowsResource(actor.UserId, actor.UserId, command.RegionId, command.Id)) throw new TenantAdminDirectException("Class was not found.", "class_not_found"); item ??= new TenantClass { Id = command.Id ?? Guid.NewGuid(), TenantId = actor.TenantId, CreatedBy = actor.UserId }; item.RegionId = command.RegionId; item.LegacyId = Normalize(command.LegacyId); item.Code = Normalize(command.Code); item.Name = command.Name.Trim(); item.Description = Normalize(command.Description); item.Status = ParseEnum(command.Status, TenantRecordStatus.Active, "invalid_class_status"); item.SortOrder = command.Order ?? item.SortOrder; item.Metadata = JsonObjectOrDefault(command.Metadata); item.UpdatedBy = actor.UserId; if (isNew) dbContext.TenantClasses.Add(item); await AddAuditAsync(actor, "tenant.class.upserted", "tenant_classes", item.Id, cancellationToken); await dbContext.SaveChangesAsync(cancellationToken); var regionName = item.RegionId.HasValue ? await dbContext.Regions .Where(region => region.TenantId == actor.TenantId && region.Id == item.RegionId.Value) .Select(region => region.Name) .FirstOrDefaultAsync(cancellationToken) : null; return new ContentManagementResult(ToClassItem(item, regionName, 0, 0)); } public async Task> DisableClassAsync( TenantAdminActor actor, Guid classId, CancellationToken cancellationToken = default) { var scope = await RequireDataScopeAsync(actor, cancellationToken); var regionIds = scope.RegionIds.ToArray(); var classIds = scope.ClassIds.ToArray(); var item = await dbContext.TenantClasses .Where(entity => entity.TenantId == actor.TenantId && entity.Id == classId) .ApplyDataScope( scope, entity => entity.CreatedBy == actor.UserId, entity => classIds.Contains(entity.Id) || (entity.RegionId.HasValue && regionIds.Contains(entity.RegionId.Value))) .FirstOrDefaultAsync(cancellationToken); if (item is null) throw new TenantAdminDirectException("Class was not found.", "class_not_found"); item.Status = TenantRecordStatus.Disabled; item.UpdatedBy = actor.UserId; await AddAuditAsync(actor, "tenant.class.disabled", "tenant_classes", item.Id, cancellationToken); await dbContext.SaveChangesAsync(cancellationToken); return new ContentManagementResult(ToClassItem(item, null, 0, 0)); } public async Task> GetClassMembersAsync( TenantAdminActor actor, TenantAdminClassMemberFilter filter, CancellationToken cancellationToken = default) { var scope = await RequireDataScopeAsync(actor, cancellationToken); await AssertClassAsync(actor, scope, filter.ClassId, cancellationToken); var classIds = scope.ClassIds.ToArray(); var regionIds = scope.RegionIds.ToArray(); var query = dbContext.TenantClassMembers.AsNoTracking() .Where(item => item.TenantId == actor.TenantId && item.ClassId == filter.ClassId) .ApplyDataScope( scope, item => item.UserId == actor.UserId || item.CreatedBy == actor.UserId, item => classIds.Contains(item.ClassId) || dbContext.TenantClasses.Any(tenantClass => tenantClass.TenantId == actor.TenantId && tenantClass.Id == item.ClassId && tenantClass.RegionId.HasValue && regionIds.Contains(tenantClass.RegionId.Value))); if (!string.IsNullOrWhiteSpace(filter.MemberType)) query = query.Where(item => item.MemberType == ParseEnum(filter.MemberType, "invalid_class_member_type")); query = query.Where(item => item.Status == ParseEnum(filter.Status, TenantClassMemberStatus.Active, "invalid_class_member_status")); var items = await query .OrderBy(item => item.MemberType == TenantClassMemberType.HeadTeacher ? 0 : item.MemberType == TenantClassMemberType.Teacher ? 1 : item.MemberType == TenantClassMemberType.Assistant ? 2 : 9) .ThenBy(item => item.JoinedAt) .Take(ResolveLimit(filter.Limit)) .Join( dbContext.Users.AsNoTracking(), member => member.UserId, user => user.Id, (member, user) => ToClassMemberItem(member, ToUserSummary(user))) .ToArrayAsync(cancellationToken); return new CatalogList(items); } public async Task> UpsertClassMemberAsync( TenantAdminActor actor, UpsertTenantAdminClassMemberCommand command, CancellationToken cancellationToken = default) { var scope = await RequireDataScopeAsync(actor, cancellationToken); await AssertClassAsync(actor, scope, command.ClassId, cancellationToken); await using var transaction = dbContext.Database.CurrentTransaction is null ? await dbContext.Database.BeginTransactionAsync(cancellationToken) : null; var memberType = ParseEnum(command.MemberType, TenantClassMemberType.Student, "invalid_class_member_type"); var status = ParseEnum(command.Status, TenantClassMemberStatus.Active, "invalid_class_member_status"); var user = await ResolveUserAsync(command.User, memberType == TenantClassMemberType.Student ? "student" : "teacher", cancellationToken); await EnsureMembershipAsync(actor.TenantId, user.Id, memberType == TenantClassMemberType.Student ? TenantRole.Student : TenantRole.Teacher, cancellationToken); if (memberType == TenantClassMemberType.Student) await EnsureStudentProfileAsync(actor.TenantId, user.Id, null, null, null, null, JsonDefaults.Object(), JsonDefaults.Object(), JsonDefaults.Object(), cancellationToken); var item = await dbContext.TenantClassMembers.FirstOrDefaultAsync(member => member.TenantId == actor.TenantId && member.ClassId == command.ClassId && member.UserId == user.Id && member.MemberType == memberType, cancellationToken); var isNew = item is null; item ??= new TenantClassMember { TenantId = actor.TenantId, ClassId = command.ClassId, UserId = user.Id, MemberType = memberType, CreatedBy = actor.UserId }; item.Status = status; item.LeftAt = status is TenantClassMemberStatus.Active ? null : DateTimeOffset.UtcNow; item.Metadata = JsonObjectOrDefault(command.Metadata); item.UpdatedBy = actor.UserId; if (isNew) dbContext.TenantClassMembers.Add(item); await AddAuditAsync(actor, "tenant.class_member.upserted", "tenant_class_members", item.Id, cancellationToken); await dbContext.SaveChangesAsync(cancellationToken); if (transaction is not null) await transaction.CommitAsync(cancellationToken); return new ContentManagementResult(ToClassMemberItem(item, ToUserSummary(user))); } public async Task> RemoveClassMemberAsync( TenantAdminActor actor, Guid classMemberId, CancellationToken cancellationToken = default) { var scope = await RequireDataScopeAsync(actor, cancellationToken); var classIds = scope.ClassIds.ToArray(); var regionIds = scope.RegionIds.ToArray(); var item = await dbContext.TenantClassMembers .Where(member => member.TenantId == actor.TenantId && member.Id == classMemberId) .ApplyDataScope( scope, member => member.UserId == actor.UserId || member.CreatedBy == actor.UserId, member => classIds.Contains(member.ClassId) || dbContext.TenantClasses.Any(tenantClass => tenantClass.TenantId == actor.TenantId && tenantClass.Id == member.ClassId && tenantClass.RegionId.HasValue && regionIds.Contains(tenantClass.RegionId.Value))) .FirstOrDefaultAsync(cancellationToken); if (item is null) throw new TenantAdminDirectException("Class member was not found.", "class_member_not_found"); var user = await dbContext.Users.SingleAsync(user => user.Id == item.UserId, cancellationToken); item.Status = TenantClassMemberStatus.Removed; item.LeftAt = DateTimeOffset.UtcNow; item.UpdatedBy = actor.UserId; await AddAuditAsync(actor, "tenant.class_member.removed", "tenant_class_members", item.Id, cancellationToken); await dbContext.SaveChangesAsync(cancellationToken); return new ContentManagementResult(ToClassMemberItem(item, ToUserSummary(user))); } }