215 lines
11 KiB
C#
215 lines
11 KiB
C#
using Microsoft.EntityFrameworkCore;
|
|
using Tiku.Application.Catalog;
|
|
using Tiku.Application.Content;
|
|
using Tiku.Application.TenantAdmin;
|
|
using Tiku.Domain.Tenancy;
|
|
using Tiku.Infrastructure.Security;
|
|
|
|
namespace Tiku.Infrastructure.TenantAdmin;
|
|
|
|
internal sealed class TenantStudentEngagementService(TenantAdminServiceDependencies dependencies)
|
|
: TenantAdminServiceBase(dependencies), ITenantStudentEngagementService
|
|
{
|
|
public async Task<CatalogList<TenantAdminStudentNoteItem>> GetStudentNotesAsync(
|
|
TenantAdminActor actor,
|
|
TenantAdminStudentActivityFilter filter,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
var scope = await RequireDataScopeAsync(actor, cancellationToken);
|
|
var regionIds = scope.RegionIds.ToArray();
|
|
var classIds = scope.ClassIds.ToArray();
|
|
var query = dbContext.TenantStudentNotes.AsNoTracking()
|
|
.Where(item => item.TenantId == actor.TenantId)
|
|
.ApplyDataScope(
|
|
scope,
|
|
item => item.StudentUserId == actor.UserId || item.CreatedBy == actor.UserId,
|
|
item => dbContext.StudentProfiles.Any(profile =>
|
|
profile.TenantId == actor.TenantId &&
|
|
profile.UserId == item.StudentUserId &&
|
|
profile.RegionId.HasValue &&
|
|
regionIds.Contains(profile.RegionId.Value)) ||
|
|
dbContext.TenantClassMembers.Any(member =>
|
|
member.TenantId == actor.TenantId &&
|
|
member.UserId == item.StudentUserId &&
|
|
member.Status == TenantClassMemberStatus.Active &&
|
|
classIds.Contains(member.ClassId)));
|
|
if (filter.StudentUserId.HasValue)
|
|
query = query.Where(item => item.StudentUserId == filter.StudentUserId.Value);
|
|
|
|
var items = await query
|
|
.OrderByDescending(item => item.IsPinned)
|
|
.ThenByDescending(item => item.CreatedAt)
|
|
.Take(ResolveLimit(filter.Limit))
|
|
.Select(item => ToNoteItem(item))
|
|
.ToArrayAsync(cancellationToken);
|
|
return new CatalogList<TenantAdminStudentNoteItem>(items);
|
|
}
|
|
|
|
public async Task<ContentManagementResult<TenantAdminStudentNoteItem>> UpsertStudentNoteAsync(
|
|
TenantAdminActor actor,
|
|
UpsertTenantAdminStudentNoteCommand command,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
var scope = await RequireDataScopeAsync(actor, cancellationToken);
|
|
ArgumentException.ThrowIfNullOrWhiteSpace(command.Content);
|
|
await AssertStudentAsync(actor, scope, command.StudentUserId, cancellationToken);
|
|
TenantStudentNote? item = null;
|
|
if (command.Id.HasValue)
|
|
{
|
|
var regionIds = scope.RegionIds.ToArray();
|
|
var classIds = scope.ClassIds.ToArray();
|
|
item = await dbContext.TenantStudentNotes
|
|
.Where(note => note.TenantId == actor.TenantId && note.Id == command.Id.Value)
|
|
.ApplyDataScope(
|
|
scope,
|
|
note => note.StudentUserId == actor.UserId || note.CreatedBy == actor.UserId,
|
|
note => dbContext.StudentProfiles.Any(profile =>
|
|
profile.TenantId == actor.TenantId &&
|
|
profile.UserId == note.StudentUserId &&
|
|
profile.RegionId.HasValue &&
|
|
regionIds.Contains(profile.RegionId.Value)) ||
|
|
dbContext.TenantClassMembers.Any(member =>
|
|
member.TenantId == actor.TenantId &&
|
|
member.UserId == note.StudentUserId &&
|
|
member.Status == TenantClassMemberStatus.Active &&
|
|
classIds.Contains(member.ClassId)))
|
|
.FirstOrDefaultAsync(cancellationToken);
|
|
if (item is null)
|
|
throw new TenantAdminDirectException("Student note was not found.", "student_note_not_found");
|
|
}
|
|
|
|
var isNew = item is null;
|
|
item ??= new TenantStudentNote
|
|
{
|
|
Id = command.Id ?? Guid.NewGuid(),
|
|
TenantId = actor.TenantId,
|
|
StudentUserId = command.StudentUserId,
|
|
CreatedBy = actor.UserId
|
|
};
|
|
item.NoteType = ParseEnum(command.NoteType, StudentNoteType.General, "invalid_student_note_type");
|
|
item.Content = command.Content.Trim();
|
|
item.Visibility = ParseEnum(command.Visibility, StudentNoteVisibility.TenantStaff,
|
|
"invalid_student_note_visibility");
|
|
item.IsPinned = command.IsPinned ?? item.IsPinned;
|
|
item.Metadata = JsonObjectOrDefault(command.Metadata);
|
|
item.UpdatedBy = actor.UserId;
|
|
if (isNew) dbContext.TenantStudentNotes.Add(item);
|
|
|
|
await AddAuditAsync(actor, "tenant.student_note.upserted", "tenant_student_notes", item.Id, cancellationToken);
|
|
await dbContext.SaveChangesAsync(cancellationToken);
|
|
return new ContentManagementResult<TenantAdminStudentNoteItem>(ToNoteItem(item));
|
|
}
|
|
|
|
public async Task<CatalogList<TenantAdminStudentFollowupItem>> GetStudentFollowupsAsync(
|
|
TenantAdminActor actor,
|
|
TenantAdminStudentActivityFilter filter,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
var scope = await RequireDataScopeAsync(actor, cancellationToken);
|
|
var regionIds = scope.RegionIds.ToArray();
|
|
var classIds = scope.ClassIds.ToArray();
|
|
var query = dbContext.TenantStudentFollowups.AsNoTracking()
|
|
.Where(item => item.TenantId == actor.TenantId)
|
|
.ApplyDataScope(
|
|
scope,
|
|
item => item.StudentUserId == actor.UserId || item.AssignedToUserId == actor.UserId ||
|
|
item.CreatedBy == actor.UserId,
|
|
item => (item.ClassId.HasValue && classIds.Contains(item.ClassId.Value)) ||
|
|
dbContext.StudentProfiles.Any(profile =>
|
|
profile.TenantId == actor.TenantId &&
|
|
profile.UserId == item.StudentUserId &&
|
|
profile.RegionId.HasValue &&
|
|
regionIds.Contains(profile.RegionId.Value)) ||
|
|
dbContext.TenantClassMembers.Any(member =>
|
|
member.TenantId == actor.TenantId &&
|
|
member.UserId == item.StudentUserId &&
|
|
member.Status == TenantClassMemberStatus.Active &&
|
|
classIds.Contains(member.ClassId)));
|
|
if (filter.StudentUserId.HasValue)
|
|
query = query.Where(item => item.StudentUserId == filter.StudentUserId.Value);
|
|
|
|
if (!string.IsNullOrWhiteSpace(filter.Status))
|
|
query = query.Where(item =>
|
|
item.Status == ParseEnum<StudentFollowupStatus>(filter.Status, "invalid_student_followup_status"));
|
|
|
|
var items = await query
|
|
.OrderBy(item =>
|
|
item.Status == StudentFollowupStatus.Done || item.Status == StudentFollowupStatus.Cancelled)
|
|
.ThenBy(item => item.DueAt ?? DateTimeOffset.MaxValue)
|
|
.ThenByDescending(item => item.CreatedAt)
|
|
.Take(ResolveLimit(filter.Limit))
|
|
.Select(item => ToFollowupItem(item))
|
|
.ToArrayAsync(cancellationToken);
|
|
return new CatalogList<TenantAdminStudentFollowupItem>(items);
|
|
}
|
|
|
|
public async Task<ContentManagementResult<TenantAdminStudentFollowupItem>> UpsertStudentFollowupAsync(
|
|
TenantAdminActor actor,
|
|
UpsertTenantAdminStudentFollowupCommand command,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
var scope = await RequireDataScopeAsync(actor, cancellationToken);
|
|
ArgumentException.ThrowIfNullOrWhiteSpace(command.Title);
|
|
await AssertStudentAsync(actor, scope, command.StudentUserId, cancellationToken);
|
|
await AssertClassAsync(actor, scope, command.ClassId, cancellationToken);
|
|
await AssertTenantMemberAsync(actor.TenantId, command.AssignedToUserId, cancellationToken);
|
|
|
|
TenantStudentFollowup? item = null;
|
|
if (command.Id.HasValue)
|
|
{
|
|
var regionIds = scope.RegionIds.ToArray();
|
|
var classIds = scope.ClassIds.ToArray();
|
|
item = await dbContext.TenantStudentFollowups
|
|
.Where(followup => followup.TenantId == actor.TenantId && followup.Id == command.Id.Value)
|
|
.ApplyDataScope(
|
|
scope,
|
|
followup => followup.StudentUserId == actor.UserId ||
|
|
followup.AssignedToUserId == actor.UserId ||
|
|
followup.CreatedBy == actor.UserId,
|
|
followup => (followup.ClassId.HasValue && classIds.Contains(followup.ClassId.Value)) ||
|
|
dbContext.StudentProfiles.Any(profile =>
|
|
profile.TenantId == actor.TenantId &&
|
|
profile.UserId == followup.StudentUserId &&
|
|
profile.RegionId.HasValue &&
|
|
regionIds.Contains(profile.RegionId.Value)) ||
|
|
dbContext.TenantClassMembers.Any(member =>
|
|
member.TenantId == actor.TenantId &&
|
|
member.UserId == followup.StudentUserId &&
|
|
member.Status == TenantClassMemberStatus.Active &&
|
|
classIds.Contains(member.ClassId)))
|
|
.FirstOrDefaultAsync(cancellationToken);
|
|
if (item is null)
|
|
throw new TenantAdminDirectException("Student followup was not found.", "student_followup_not_found");
|
|
}
|
|
|
|
var isNew = item is null;
|
|
item ??= new TenantStudentFollowup
|
|
{
|
|
Id = command.Id ?? Guid.NewGuid(),
|
|
TenantId = actor.TenantId,
|
|
StudentUserId = command.StudentUserId,
|
|
CreatedBy = actor.UserId
|
|
};
|
|
item.AssignedToUserId = command.AssignedToUserId;
|
|
item.ClassId = command.ClassId;
|
|
item.Title = command.Title.Trim();
|
|
item.Description = Normalize(command.Description);
|
|
item.FollowupType = ParseEnum(command.FollowupType, StudentFollowupType.Learning,
|
|
"invalid_student_followup_type");
|
|
item.Priority = ParseEnum(command.Priority, StudentFollowupPriority.Normal,
|
|
"invalid_student_followup_priority");
|
|
item.Status = ParseEnum(command.Status, StudentFollowupStatus.Open, "invalid_student_followup_status");
|
|
item.DueAt = command.DueAt;
|
|
item.CompletedAt = item.Status == StudentFollowupStatus.Done ? DateTimeOffset.UtcNow : null;
|
|
item.CompletedBy = item.Status == StudentFollowupStatus.Done ? actor.UserId : null;
|
|
item.Metadata = JsonObjectOrDefault(command.Metadata);
|
|
item.UpdatedBy = actor.UserId;
|
|
if (isNew) dbContext.TenantStudentFollowups.Add(item);
|
|
|
|
await AddAuditAsync(actor, "tenant.student_followup.upserted", "tenant_student_followups", item.Id,
|
|
cancellationToken);
|
|
await dbContext.SaveChangesAsync(cancellationToken);
|
|
return new ContentManagementResult<TenantAdminStudentFollowupItem>(ToFollowupItem(item));
|
|
}
|
|
}
|