“教务总览”改成可实际工作的分级工作台:
校级管理员查看全校数据,学院管理员仅查看本学院数据。 待办按当前审批阶段统计,涵盖授课资格、成绩、调停课、学籍异动、教室借用等。 增加学期进度、教学任务发布、课表覆盖、成绩发布状态。 快捷入口根据管理员角色自动调整。 完善未配置学期、无待办、加载失败等状态。 适配桌面和 390px 移动端,无横向溢出。
This commit is contained in:
@@ -1,11 +1,10 @@
|
||||
using System.Text.Json;
|
||||
using Jiaowu.Api.Domain.Academic;
|
||||
using Jiaowu.Api.Infrastructure.Caching;
|
||||
using Jiaowu.Api.Domain.Identity;
|
||||
using Jiaowu.Api.Infrastructure.Auth;
|
||||
using Jiaowu.Api.Infrastructure.Persistence;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace Jiaowu.Api.Controllers;
|
||||
|
||||
@@ -14,89 +13,301 @@ namespace Jiaowu.Api.Controllers;
|
||||
[Route("api/dashboard")]
|
||||
public sealed class DashboardController(
|
||||
AppDbContext db,
|
||||
IAppCache appCache,
|
||||
IOptions<JsonOptions> jsonOptions) : ControllerBase
|
||||
ICurrentUserDataScope currentUserDataScope) : ControllerBase
|
||||
{
|
||||
[HttpGet]
|
||||
public async Task<ActionResult<object>> Get(CancellationToken cancellationToken)
|
||||
public async Task<ActionResult<DashboardResponse>> Get(
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var response = await appCache.GetOrCreateAsync(
|
||||
AppCacheKeys.Dashboard,
|
||||
LoadAsync,
|
||||
AppCacheProfile.Analytics,
|
||||
[AppCacheTags.Analytics],
|
||||
cancellationToken);
|
||||
return response;
|
||||
}
|
||||
var scope = currentUserDataScope.Current;
|
||||
Guid? restrictedCollegeId = scope.Scope == DataScope.All
|
||||
? null
|
||||
: scope.CollegeId ?? Guid.Empty;
|
||||
var collegeName = restrictedCollegeId.HasValue &&
|
||||
restrictedCollegeId.Value != Guid.Empty
|
||||
? await db.Colleges.AsNoTracking()
|
||||
.Where(x => x.Id == restrictedCollegeId.Value)
|
||||
.Select(x => x.Name)
|
||||
.FirstOrDefaultAsync(cancellationToken)
|
||||
: null;
|
||||
|
||||
private async Task<JsonElement> LoadAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
var currentTerm = await db.AcademicTerms
|
||||
.AsNoTracking()
|
||||
.Where(x => x.IsCurrent)
|
||||
.Select(x => new { x.Id, x.Name, x.StartDate, x.EndDate })
|
||||
.Select(x => new DashboardTerm(
|
||||
x.Id,
|
||||
x.Name,
|
||||
x.StartDate,
|
||||
x.EndDate))
|
||||
.FirstOrDefaultAsync(cancellationToken);
|
||||
var currentTermId = currentTerm?.Id;
|
||||
|
||||
return JsonSerializer.SerializeToElement(
|
||||
new
|
||||
{
|
||||
CurrentTerm = currentTerm,
|
||||
Counts = new
|
||||
{
|
||||
Campuses = await db.Campuses.CountAsync(cancellationToken),
|
||||
Colleges = await db.Colleges.CountAsync(cancellationToken),
|
||||
Majors = await db.Majors.CountAsync(cancellationToken),
|
||||
Classes = await db.AdministrativeClasses.CountAsync(cancellationToken),
|
||||
Classrooms = await db.Classrooms.CountAsync(cancellationToken),
|
||||
Teachers = await db.Teachers.CountAsync(cancellationToken),
|
||||
Students = await db.Students.CountAsync(cancellationToken),
|
||||
Courses = await db.Courses.CountAsync(cancellationToken),
|
||||
CurriculumPlans = await db.CurriculumPlans.CountAsync(cancellationToken),
|
||||
TeachingTasks = await db.TeachingTasks.CountAsync(cancellationToken),
|
||||
SchedulePlans = await db.SchedulePlans.CountAsync(cancellationToken),
|
||||
CourseSelectionRounds = await db.CourseSelectionRounds
|
||||
.CountAsync(cancellationToken),
|
||||
CourseSelectionOfferings = await db.CourseSelectionOfferings
|
||||
.CountAsync(cancellationToken),
|
||||
CourseEnrollments = await db.CourseEnrollments
|
||||
.CountAsync(
|
||||
x => x.Status == CourseEnrollmentStatus.Enrolled,
|
||||
cancellationToken),
|
||||
GradeSheets = await db.GradeSheets.CountAsync(cancellationToken),
|
||||
PublishedGradeSheets = await db.GradeSheets.CountAsync(
|
||||
x => x.Status == GradeSheetStatus.Published,
|
||||
cancellationToken),
|
||||
GradeRecords = await db.GradeRecords.CountAsync(cancellationToken),
|
||||
ExamPlans = await db.ExamPlans.CountAsync(cancellationToken),
|
||||
ExamSessions = await db.ExamSessions.CountAsync(cancellationToken),
|
||||
StudentStatusChanges = await db.StudentStatusChanges
|
||||
.CountAsync(cancellationToken),
|
||||
PendingStudentStatusChanges = await db.StudentStatusChanges.CountAsync(
|
||||
x => x.State == StudentStatusChangeState.Submitted ||
|
||||
x.State == StudentStatusChangeState.CounselorApproved ||
|
||||
x.State == StudentStatusChangeState.CollegeApproved,
|
||||
cancellationToken),
|
||||
GraduationAuditBatches = await db.GraduationAuditBatches
|
||||
.CountAsync(cancellationToken),
|
||||
PublishedGraduationAuditBatches = await db.GraduationAuditBatches
|
||||
.CountAsync(
|
||||
x => x.Status == GraduationAuditBatchStatus.Published,
|
||||
cancellationToken),
|
||||
DegreeAwardBatches = await db.DegreeAwardBatches
|
||||
.CountAsync(cancellationToken),
|
||||
PublishedDegreeAwardBatches = await db.DegreeAwardBatches
|
||||
.CountAsync(
|
||||
x => x.Status == DegreeAwardBatchStatus.Published,
|
||||
cancellationToken),
|
||||
GraduationClearanceBatches = await db.GraduationClearanceBatches
|
||||
.CountAsync(cancellationToken),
|
||||
OpenGraduationClearanceBatches = await db.GraduationClearanceBatches
|
||||
.CountAsync(
|
||||
x => x.Status == GraduationClearanceBatchStatus.Open,
|
||||
cancellationToken),
|
||||
Users = await db.Users.CountAsync(cancellationToken)
|
||||
}
|
||||
},
|
||||
jsonOptions.Value.JsonSerializerOptions);
|
||||
var students = db.Students.AsNoTracking()
|
||||
.Where(x =>
|
||||
!restrictedCollegeId.HasValue ||
|
||||
x.AdministrativeClass!.Major!.CollegeId ==
|
||||
restrictedCollegeId.Value);
|
||||
var teachers = db.Teachers.AsNoTracking()
|
||||
.Where(x =>
|
||||
!restrictedCollegeId.HasValue ||
|
||||
x.CollegeId == restrictedCollegeId.Value);
|
||||
var courses = db.Courses.AsNoTracking()
|
||||
.Where(x =>
|
||||
!restrictedCollegeId.HasValue ||
|
||||
x.CollegeId == restrictedCollegeId.Value);
|
||||
var teachingTasks = db.TeachingTasks.AsNoTracking()
|
||||
.Where(x =>
|
||||
currentTermId.HasValue &&
|
||||
x.AcademicTermId == currentTermId.Value &&
|
||||
(!restrictedCollegeId.HasValue ||
|
||||
x.Course!.CollegeId == restrictedCollegeId.Value));
|
||||
var gradeSheets = db.GradeSheets.AsNoTracking()
|
||||
.Where(x =>
|
||||
currentTermId.HasValue &&
|
||||
x.TeachingTask!.AcademicTermId == currentTermId.Value &&
|
||||
(!restrictedCollegeId.HasValue ||
|
||||
x.TeachingTask.Course!.CollegeId ==
|
||||
restrictedCollegeId.Value));
|
||||
var enrollments = db.CourseEnrollments.AsNoTracking()
|
||||
.Where(x =>
|
||||
currentTermId.HasValue &&
|
||||
x.Status == CourseEnrollmentStatus.Enrolled &&
|
||||
x.CourseSelectionOffering!.CourseSelectionRound!
|
||||
.AcademicTermId == currentTermId.Value &&
|
||||
(!restrictedCollegeId.HasValue ||
|
||||
x.CourseSelectionOffering.TeachingTask!.Course!.CollegeId ==
|
||||
restrictedCollegeId.Value));
|
||||
|
||||
var taskCount = await teachingTasks.CountAsync(cancellationToken);
|
||||
var publishedTaskCount = await teachingTasks.CountAsync(
|
||||
x => x.Status == TeachingTaskStatus.Published,
|
||||
cancellationToken);
|
||||
var scheduledTaskCount = currentTermId.HasValue
|
||||
? await db.ScheduleEntries.AsNoTracking()
|
||||
.Where(x =>
|
||||
x.SchedulePlan!.AcademicTermId == currentTermId.Value &&
|
||||
x.SchedulePlan.Status == SchedulePlanStatus.Published &&
|
||||
(!restrictedCollegeId.HasValue ||
|
||||
x.TeachingTask!.Course!.CollegeId ==
|
||||
restrictedCollegeId.Value))
|
||||
.Select(x => x.TeachingTaskId)
|
||||
.Distinct()
|
||||
.CountAsync(cancellationToken)
|
||||
: 0;
|
||||
var gradeSheetCount = await gradeSheets.CountAsync(cancellationToken);
|
||||
var publishedGradeSheetCount = await gradeSheets.CountAsync(
|
||||
x => x.Status == GradeSheetStatus.Published,
|
||||
cancellationToken);
|
||||
|
||||
var counts = new DashboardCounts(
|
||||
await students.CountAsync(
|
||||
x => x.Status == StudentStatus.Active,
|
||||
cancellationToken),
|
||||
await teachers.CountAsync(
|
||||
x => x.Status == TeacherStatus.Active,
|
||||
cancellationToken),
|
||||
await courses.CountAsync(
|
||||
x => x.IsEnabled,
|
||||
cancellationToken),
|
||||
taskCount,
|
||||
publishedTaskCount,
|
||||
scheduledTaskCount,
|
||||
await enrollments.CountAsync(cancellationToken),
|
||||
gradeSheetCount,
|
||||
publishedGradeSheetCount,
|
||||
await gradeSheets.CountAsync(
|
||||
x => x.Status == GradeSheetStatus.Submitted,
|
||||
cancellationToken),
|
||||
currentTermId.HasValue
|
||||
? await db.CourseSelectionRounds.AsNoTracking().CountAsync(
|
||||
x => x.AcademicTermId == currentTermId.Value &&
|
||||
x.Status == CourseSelectionRoundStatus.Open,
|
||||
cancellationToken)
|
||||
: 0);
|
||||
|
||||
var pending = await LoadPendingAsync(
|
||||
scope,
|
||||
restrictedCollegeId,
|
||||
cancellationToken);
|
||||
|
||||
return Ok(new DashboardResponse(
|
||||
BuildAudience(scope, collegeName),
|
||||
currentTerm,
|
||||
counts,
|
||||
pending,
|
||||
DateTime.UtcNow));
|
||||
}
|
||||
|
||||
private async Task<DashboardPending> LoadPendingAsync(
|
||||
CurrentUserScope scope,
|
||||
Guid? restrictedCollegeId,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var isSchoolManager =
|
||||
scope.IsInRole(SystemRoles.SuperAdmin) ||
|
||||
scope.IsInRole(SystemRoles.AcademicAdmin);
|
||||
var isCollegeManager =
|
||||
!isSchoolManager && scope.IsInRole(SystemRoles.CollegeAdmin);
|
||||
if (!isSchoolManager && !isCollegeManager)
|
||||
return new DashboardPending(0, 0, 0, 0, 0, 0, 0);
|
||||
|
||||
var teacherApplications = db.TeacherCourseApplications.AsNoTracking()
|
||||
.Where(x =>
|
||||
x.Status == TeacherCourseApplicationStatus.Pending &&
|
||||
(!restrictedCollegeId.HasValue ||
|
||||
x.Teacher!.CollegeId == restrictedCollegeId.Value));
|
||||
var gradeSheets = db.GradeSheets.AsNoTracking()
|
||||
.Where(x =>
|
||||
x.Status == GradeSheetStatus.Submitted &&
|
||||
(!restrictedCollegeId.HasValue ||
|
||||
x.TeachingTask!.Course!.CollegeId ==
|
||||
restrictedCollegeId.Value));
|
||||
var courseAdjustments = db.CourseAdjustments.AsNoTracking()
|
||||
.Where(x =>
|
||||
x.Status == CourseAdjustmentStatus.Submitted &&
|
||||
(!restrictedCollegeId.HasValue ||
|
||||
x.TeachingTask!.Course!.CollegeId ==
|
||||
restrictedCollegeId.Value));
|
||||
var studentStatusChanges = db.StudentStatusChanges.AsNoTracking()
|
||||
.Where(x =>
|
||||
x.State == (isCollegeManager
|
||||
? StudentStatusChangeState.CounselorApproved
|
||||
: StudentStatusChangeState.CollegeApproved) &&
|
||||
(!restrictedCollegeId.HasValue ||
|
||||
x.Student!.AdministrativeClass!.Major!.CollegeId ==
|
||||
restrictedCollegeId.Value));
|
||||
var gradeModifications = db.GradeModifications.AsNoTracking()
|
||||
.Where(x =>
|
||||
x.Status == (isCollegeManager
|
||||
? GradeModificationStatus.TeacherSubmitted
|
||||
: GradeModificationStatus.CollegeApproved) &&
|
||||
(!restrictedCollegeId.HasValue ||
|
||||
x.GradeRecord!.GradeSheet!.TeachingTask!.Course!.CollegeId ==
|
||||
restrictedCollegeId.Value));
|
||||
|
||||
var generalApprovals =
|
||||
await db.CourseExemptions.AsNoTracking().CountAsync(
|
||||
x => x.Status == ApprovalStatus.Submitted &&
|
||||
(!restrictedCollegeId.HasValue ||
|
||||
x.TeachingTask!.Course!.CollegeId ==
|
||||
restrictedCollegeId.Value),
|
||||
cancellationToken) +
|
||||
await db.DeferredExams.AsNoTracking().CountAsync(
|
||||
x => x.Status == ApprovalStatus.Submitted &&
|
||||
(!restrictedCollegeId.HasValue ||
|
||||
x.TeachingTask!.Course!.CollegeId ==
|
||||
restrictedCollegeId.Value),
|
||||
cancellationToken) +
|
||||
await db.CourseSubstitutions.AsNoTracking().CountAsync(
|
||||
x => x.Status == ApprovalStatus.Submitted &&
|
||||
(!restrictedCollegeId.HasValue ||
|
||||
x.Student!.AdministrativeClass!.Major!.CollegeId ==
|
||||
restrictedCollegeId.Value),
|
||||
cancellationToken) +
|
||||
await db.AttendanceRecords.AsNoTracking().CountAsync(
|
||||
x => x.AppealStatus == AttendanceAppealStatus.Pending &&
|
||||
(!restrictedCollegeId.HasValue ||
|
||||
x.Student!.AdministrativeClass!.Major!.CollegeId ==
|
||||
restrictedCollegeId.Value),
|
||||
cancellationToken);
|
||||
|
||||
var classroomReservations = isCollegeManager &&
|
||||
restrictedCollegeId.HasValue
|
||||
? await db.ClassroomReservations.AsNoTracking().CountAsync(
|
||||
x => x.Status == ClassroomReservationStatus.Submitted &&
|
||||
x.ApplicantCollegeId == restrictedCollegeId.Value,
|
||||
cancellationToken)
|
||||
: 0;
|
||||
|
||||
return new DashboardPending(
|
||||
await teacherApplications.CountAsync(cancellationToken),
|
||||
await gradeSheets.CountAsync(cancellationToken),
|
||||
await courseAdjustments.CountAsync(cancellationToken),
|
||||
await studentStatusChanges.CountAsync(cancellationToken),
|
||||
await gradeModifications.CountAsync(cancellationToken),
|
||||
classroomReservations,
|
||||
generalApprovals);
|
||||
}
|
||||
|
||||
private static DashboardAudience BuildAudience(
|
||||
CurrentUserScope scope,
|
||||
string? collegeName)
|
||||
{
|
||||
if (scope.IsInRole(SystemRoles.SuperAdmin))
|
||||
return new DashboardAudience(
|
||||
"System",
|
||||
"全域教务工作台",
|
||||
"全校",
|
||||
"统筹基础数据、教学运行与系统治理");
|
||||
if (scope.IsInRole(SystemRoles.AcademicAdmin))
|
||||
return new DashboardAudience(
|
||||
"School",
|
||||
"校级教务工作台",
|
||||
"全校",
|
||||
"聚焦跨学院教学运行与校级审核");
|
||||
if (scope.IsInRole(SystemRoles.CollegeAdmin))
|
||||
return new DashboardAudience(
|
||||
"College",
|
||||
"学院教务工作台",
|
||||
collegeName ?? "本学院",
|
||||
"聚焦本学院教学准备、过程审核与成绩归档");
|
||||
if (scope.IsInRole(SystemRoles.Leader))
|
||||
return new DashboardAudience(
|
||||
"Leadership",
|
||||
"教学运行观察台",
|
||||
"全校",
|
||||
"查看全校教学运行与质量数据");
|
||||
if (scope.IsInRole(SystemRoles.Counselor))
|
||||
return new DashboardAudience(
|
||||
"Counselor",
|
||||
"班级工作台",
|
||||
collegeName ?? "所辖班级",
|
||||
"处理学生过程管理与学业支持");
|
||||
return new DashboardAudience(
|
||||
"Teaching",
|
||||
"教学工作台",
|
||||
collegeName ?? "个人教学",
|
||||
"查看课程运行并进入日常教学工作");
|
||||
}
|
||||
}
|
||||
|
||||
public sealed record DashboardResponse(
|
||||
DashboardAudience Audience,
|
||||
DashboardTerm? CurrentTerm,
|
||||
DashboardCounts Counts,
|
||||
DashboardPending Pending,
|
||||
DateTime GeneratedAt);
|
||||
|
||||
public sealed record DashboardAudience(
|
||||
string Level,
|
||||
string Title,
|
||||
string ScopeName,
|
||||
string Description);
|
||||
|
||||
public sealed record DashboardTerm(
|
||||
Guid Id,
|
||||
string Name,
|
||||
DateOnly StartDate,
|
||||
DateOnly EndDate);
|
||||
|
||||
public sealed record DashboardCounts(
|
||||
int Students,
|
||||
int Teachers,
|
||||
int Courses,
|
||||
int TeachingTasks,
|
||||
int PublishedTeachingTasks,
|
||||
int ScheduledTeachingTasks,
|
||||
int CourseEnrollments,
|
||||
int GradeSheets,
|
||||
int PublishedGradeSheets,
|
||||
int SubmittedGradeSheets,
|
||||
int OpenCourseSelectionRounds);
|
||||
|
||||
public sealed record DashboardPending(
|
||||
int TeacherApplications,
|
||||
int GradeSheets,
|
||||
int CourseAdjustments,
|
||||
int StudentStatusChanges,
|
||||
int GradeModifications,
|
||||
int ClassroomReservations,
|
||||
int GeneralApprovals);
|
||||
|
||||
Reference in New Issue
Block a user