课程修读名单导出
This commit is contained in:
1 parent
0ce758191e
commit
4575758812
5 files changed
+301
-46
No files matched your search
@@ -1,5 +1,6 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Data;
|
||||
using Jiaowu.Api.Contracts;
|
||||
using Jiaowu.Api.Domain.Academic;
|
||||
using Jiaowu.Api.Domain.Identity;
|
||||
using Jiaowu.Api.Infrastructure.Auth;
|
||||
@@ -37,6 +38,51 @@ public sealed class CourseSelectionsController(
|
||||
OfferingManagers + "," +
|
||||
SystemRoles.Teacher;
|
||||
|
||||
[HttpGet("enrollments")]
|
||||
[Authorize(Roles = OfferingManagers)]
|
||||
public async Task<ActionResult> GetEnrollments(
|
||||
[FromQuery] CourseEnrollmentQuery query,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var source = ApplyEnrollmentFilters(ScopedEnrollments(), query);
|
||||
var total = await source.CountAsync(cancellationToken);
|
||||
var page = Math.Max(1, query.Page);
|
||||
var pageSize = Math.Clamp(query.PageSize, 1, 100);
|
||||
var items = await ProjectEnrollments(source)
|
||||
.OrderByDescending(x => x.TermStartDate)
|
||||
.ThenBy(x => x.CourseCode)
|
||||
.ThenBy(x => x.StudentNumber)
|
||||
.Skip((page - 1) * pageSize)
|
||||
.Take(pageSize)
|
||||
.ToListAsync(cancellationToken);
|
||||
return Ok(new PagedResult<object>(items.Cast<object>().ToList(), total, page, pageSize));
|
||||
}
|
||||
|
||||
[HttpGet("enrollments/export.xlsx")]
|
||||
[Authorize(Roles = OfferingManagers)]
|
||||
public async Task<ActionResult> ExportEnrollments(
|
||||
[FromQuery] CourseEnrollmentQuery query,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var items = await ProjectEnrollments(ApplyEnrollmentFilters(ScopedEnrollments(), query))
|
||||
.OrderByDescending(x => x.TermStartDate)
|
||||
.ThenBy(x => x.CourseCode)
|
||||
.ThenBy(x => x.StudentNumber)
|
||||
.ToListAsync(cancellationToken);
|
||||
var rows = items.Select(x => (IReadOnlyList<object?>)[
|
||||
x.TermName, x.CollegeName, x.CourseCode, x.CourseName, x.TaskNumber,
|
||||
x.StudentNumber, x.StudentName, x.MajorName, x.ClassName, x.Grade,
|
||||
x.Status.ToString(), x.EnrollmentType.ToString(), x.EnrolledAt,
|
||||
x.WithdrawnAt, x.ExemptionStatus?.ToString() ?? "—"
|
||||
]).ToList();
|
||||
var bytes = ExcelWorkbookHelper.Create("修读课程名单", [
|
||||
"学期", "开课院系", "课程代码", "课程名称", "教学班", "学号", "姓名",
|
||||
"专业", "行政班", "年级", "修读状态", "修读类型", "选入时间", "退选时间", "免修审批"
|
||||
], rows);
|
||||
return File(bytes, ExcelWorkbookHelper.ContentType,
|
||||
$"修读课程名单-{DateTime.Now:yyyyMMdd-HHmm}.xlsx");
|
||||
}
|
||||
|
||||
[HttpGet("rounds")]
|
||||
[Authorize(Roles = SelectionUsers)]
|
||||
public async Task<ActionResult> GetRounds(
|
||||
@@ -2225,6 +2271,71 @@ public sealed class CourseSelectionsController(
|
||||
: result;
|
||||
}
|
||||
|
||||
private IQueryable<CourseEnrollment> ScopedEnrollments()
|
||||
{
|
||||
var source = db.CourseEnrollments.AsNoTracking();
|
||||
var scope = currentUserDataScope.Current;
|
||||
return scope.Scope == DataScope.All
|
||||
? source
|
||||
: source.Where(x =>
|
||||
x.CourseSelectionOffering!.TeachingTask!.Course!.CollegeId ==
|
||||
scope.RestrictedCollegeId);
|
||||
}
|
||||
|
||||
private static IQueryable<CourseEnrollment> ApplyEnrollmentFilters(
|
||||
IQueryable<CourseEnrollment> source,
|
||||
CourseEnrollmentQuery query)
|
||||
{
|
||||
if (query.AcademicTermId.HasValue)
|
||||
source = source.Where(x => x.CourseSelectionOffering!.CourseSelectionRound!
|
||||
.AcademicTermId == query.AcademicTermId.Value);
|
||||
if (query.CourseId.HasValue)
|
||||
source = source.Where(x => x.CourseSelectionOffering!.TeachingTask!.CourseId ==
|
||||
query.CourseId.Value);
|
||||
if (query.CollegeId.HasValue)
|
||||
source = source.Where(x => x.CourseSelectionOffering!.TeachingTask!.Course!.CollegeId ==
|
||||
query.CollegeId.Value);
|
||||
if (query.Status.HasValue)
|
||||
source = source.Where(x => x.Status == query.Status.Value);
|
||||
if (!string.IsNullOrWhiteSpace(query.Keyword))
|
||||
{
|
||||
var keyword = query.Keyword.Trim();
|
||||
source = source.Where(x =>
|
||||
x.Student!.StudentNumber.Contains(keyword) ||
|
||||
x.Student.Name.Contains(keyword) ||
|
||||
x.CourseSelectionOffering!.TeachingTask!.Course!.Code.Contains(keyword) ||
|
||||
x.CourseSelectionOffering.TeachingTask.Course.Name.Contains(keyword) ||
|
||||
x.CourseSelectionOffering.TeachingTask.TaskNumber.Contains(keyword));
|
||||
}
|
||||
return source;
|
||||
}
|
||||
|
||||
private IQueryable<CourseEnrollmentListItem> ProjectEnrollments(
|
||||
IQueryable<CourseEnrollment> source) => source.Select(x => new CourseEnrollmentListItem(
|
||||
x.Id,
|
||||
x.CourseSelectionOfferingId,
|
||||
x.CourseSelectionOffering!.CourseSelectionRound!.AcademicTerm!.Name,
|
||||
x.CourseSelectionOffering.CourseSelectionRound.AcademicTerm.StartDate,
|
||||
x.CourseSelectionOffering.TeachingTask!.Course!.College!.Name,
|
||||
x.CourseSelectionOffering.TeachingTask.Course.Code,
|
||||
x.CourseSelectionOffering.TeachingTask.Course.Name,
|
||||
x.CourseSelectionOffering.TeachingTask.TaskNumber,
|
||||
x.Student!.StudentNumber,
|
||||
x.Student.Name,
|
||||
x.Student.AdministrativeClass!.Major!.Name,
|
||||
x.Student.AdministrativeClass.Name,
|
||||
x.Student.AdministrativeClass.Grade,
|
||||
x.Status,
|
||||
x.EnrollmentType,
|
||||
x.EnrolledAt,
|
||||
x.WithdrawnAt,
|
||||
db.CourseExemptions
|
||||
.Where(exemption => exemption.StudentId == x.StudentId &&
|
||||
exemption.TeachingTaskId == x.CourseSelectionOffering.TeachingTaskId)
|
||||
.OrderByDescending(exemption => exemption.CreatedAt)
|
||||
.Select(exemption => (ApprovalStatus?)exemption.Status)
|
||||
.FirstOrDefault()));
|
||||
|
||||
private ActionResult ProfileNotFound() =>
|
||||
ConflictProblem("当前账号未关联有效学生档案,请联系教务管理员。");
|
||||
|
||||
@@ -2294,6 +2405,35 @@ public sealed record AdminEnrollmentRequest(
|
||||
public sealed record ForceEnrollmentRequest(
|
||||
[MinLength(1)] IReadOnlyCollection<Guid> StudentIds);
|
||||
|
||||
public sealed record CourseEnrollmentQuery(
|
||||
Guid? AcademicTermId,
|
||||
Guid? CourseId,
|
||||
Guid? CollegeId,
|
||||
CourseEnrollmentStatus? Status,
|
||||
string? Keyword,
|
||||
int Page = 1,
|
||||
int PageSize = 20);
|
||||
|
||||
public sealed record CourseEnrollmentListItem(
|
||||
Guid Id,
|
||||
Guid CourseSelectionOfferingId,
|
||||
string TermName,
|
||||
DateOnly TermStartDate,
|
||||
string CollegeName,
|
||||
string CourseCode,
|
||||
string CourseName,
|
||||
string TaskNumber,
|
||||
string StudentNumber,
|
||||
string StudentName,
|
||||
string MajorName,
|
||||
string ClassName,
|
||||
int Grade,
|
||||
CourseEnrollmentStatus Status,
|
||||
EnrollmentType EnrollmentType,
|
||||
DateTime? EnrolledAt,
|
||||
DateTime? WithdrawnAt,
|
||||
ApprovalStatus? ExemptionStatus);
|
||||
|
||||
public sealed record StudentOfferingDto(
|
||||
Guid Id,
|
||||
Guid TeachingTaskId,
|
||||
|
||||
@@ -167,6 +167,10 @@ const navigationGroups = computed<NavigationGroup[]>(() => [
|
||||
label: isTeachingAdmin.value ? '选课管理' : '学生选课',
|
||||
},
|
||||
),
|
||||
...whenVisible(
|
||||
hasAnyRole(['SuperAdmin', 'AcademicAdmin', 'CollegeAdmin']),
|
||||
{ path: '/course-enrollments', label: '修读课程管理' },
|
||||
),
|
||||
...whenVisible(
|
||||
hasAnyRole(['SuperAdmin', 'AcademicAdmin', 'CollegeAdmin', 'Teacher']),
|
||||
{
|
||||
|
||||
@@ -251,6 +251,12 @@ const router = createRouter({
|
||||
roles: ['SuperAdmin', 'AcademicAdmin', 'CollegeAdmin', 'Student'],
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'course-enrollments',
|
||||
name: 'course-enrollments',
|
||||
component: () => import('../views/CourseEnrollmentManagementView.vue'),
|
||||
meta: { roles: ['SuperAdmin', 'AcademicAdmin', 'CollegeAdmin'] },
|
||||
},
|
||||
{
|
||||
path: 'grades',
|
||||
name: 'grades',
|
||||
|
||||
Loaded 3 of 5 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user