调课
This commit is contained in:
1 parent
22321abe23
commit
2f5c6af37c
12 files changed
+6281
-212
No files matched your search
@@ -3,6 +3,8 @@ using Jiaowu.Api.Domain.Academic;
|
|||||||
using Jiaowu.Api.Domain.Identity;
|
using Jiaowu.Api.Domain.Identity;
|
||||||
using Jiaowu.Api.Infrastructure.Auth;
|
using Jiaowu.Api.Infrastructure.Auth;
|
||||||
using Jiaowu.Api.Infrastructure.Persistence;
|
using Jiaowu.Api.Infrastructure.Persistence;
|
||||||
|
using Jiaowu.Api.Infrastructure.Scheduling;
|
||||||
|
using Jiaowu.Api.Infrastructure.Teaching;
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
@@ -58,6 +60,114 @@ public sealed class CourseAdjustmentsController(
|
|||||||
.ToListAsync(cancellationToken));
|
.ToListAsync(cancellationToken));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[HttpGet("tasks/{teachingTaskId:guid}/session-options")]
|
||||||
|
[Authorize(Roles = Applicants)]
|
||||||
|
public async Task<ActionResult> GetSessionOptions(
|
||||||
|
Guid teachingTaskId,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var task = await AccessibleTeachingTasks().AsNoTracking()
|
||||||
|
.Where(x =>
|
||||||
|
x.Id == teachingTaskId &&
|
||||||
|
x.Status == TeachingTaskStatus.Published)
|
||||||
|
.Select(x => new
|
||||||
|
{
|
||||||
|
x.Id,
|
||||||
|
x.TaskNumber,
|
||||||
|
CourseName = x.Course!.Name,
|
||||||
|
x.AcademicTermId,
|
||||||
|
TermStartDate = x.AcademicTerm!.StartDate,
|
||||||
|
TermEndDate = x.AcademicTerm.EndDate
|
||||||
|
})
|
||||||
|
.FirstOrDefaultAsync(cancellationToken);
|
||||||
|
if (task is null) return NotFound();
|
||||||
|
|
||||||
|
var entries = await db.ScheduleEntries.AsNoTracking()
|
||||||
|
.Where(x =>
|
||||||
|
x.TeachingTaskId == teachingTaskId &&
|
||||||
|
x.SchedulePlan!.Status == SchedulePlanStatus.Published)
|
||||||
|
.OrderBy(x => x.DayOfWeek)
|
||||||
|
.ThenBy(x => x.StartPeriod)
|
||||||
|
.Select(x => new
|
||||||
|
{
|
||||||
|
x.Id,
|
||||||
|
x.DayOfWeek,
|
||||||
|
x.StartPeriod,
|
||||||
|
x.PeriodCount,
|
||||||
|
x.StartWeek,
|
||||||
|
x.EndWeek,
|
||||||
|
x.WeekPattern,
|
||||||
|
x.ClassroomId,
|
||||||
|
ClassroomName = x.Classroom != null ? x.Classroom.Name : null,
|
||||||
|
BuildingName = x.Classroom != null ? x.Classroom.Building!.Name : null
|
||||||
|
})
|
||||||
|
.ToListAsync(cancellationToken);
|
||||||
|
|
||||||
|
var occupied = await db.CourseAdjustments.AsNoTracking()
|
||||||
|
.Where(x =>
|
||||||
|
x.TeachingTaskId == teachingTaskId &&
|
||||||
|
x.SourceScheduleEntryId != null &&
|
||||||
|
x.SourceWeek != null &&
|
||||||
|
(x.Status == CourseAdjustmentStatus.Submitted ||
|
||||||
|
x.Status == CourseAdjustmentStatus.Approved))
|
||||||
|
.Select(x => new { x.SourceScheduleEntryId, x.SourceWeek })
|
||||||
|
.ToListAsync(cancellationToken);
|
||||||
|
var occupiedOccurrences = occupied
|
||||||
|
.Select(x => (x.SourceScheduleEntryId!.Value, x.SourceWeek!.Value))
|
||||||
|
.ToHashSet();
|
||||||
|
|
||||||
|
var sessions = entries
|
||||||
|
.SelectMany(entry => Enumerable.Range(
|
||||||
|
entry.StartWeek,
|
||||||
|
entry.EndWeek - entry.StartWeek + 1)
|
||||||
|
.Where(week => IncludesWeek(entry.WeekPattern, week))
|
||||||
|
.Select(week => new
|
||||||
|
{
|
||||||
|
ScheduleEntryId = entry.Id,
|
||||||
|
Week = week,
|
||||||
|
Date = ResolveDate(task.TermStartDate, week, entry.DayOfWeek),
|
||||||
|
entry.DayOfWeek,
|
||||||
|
entry.StartPeriod,
|
||||||
|
entry.PeriodCount,
|
||||||
|
entry.ClassroomId,
|
||||||
|
entry.ClassroomName,
|
||||||
|
entry.BuildingName,
|
||||||
|
HasExistingAdjustment = occupiedOccurrences.Contains((entry.Id, week))
|
||||||
|
}))
|
||||||
|
.Where(x => x.Date >= task.TermStartDate && x.Date <= task.TermEndDate)
|
||||||
|
.OrderBy(x => x.Date)
|
||||||
|
.ThenBy(x => x.StartPeriod)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
var classrooms = await db.Classrooms.AsNoTracking()
|
||||||
|
.Where(x => x.IsEnabled)
|
||||||
|
.OrderBy(x => x.Building!.Campus!.SortOrder)
|
||||||
|
.ThenBy(x => x.Building!.SortOrder)
|
||||||
|
.ThenBy(x => x.SortOrder)
|
||||||
|
.Select(x => new
|
||||||
|
{
|
||||||
|
x.Id,
|
||||||
|
x.Name,
|
||||||
|
BuildingName = x.Building!.Name,
|
||||||
|
CampusName = x.Building.Campus!.Name,
|
||||||
|
x.Capacity
|
||||||
|
})
|
||||||
|
.ToListAsync(cancellationToken);
|
||||||
|
var periods = await db.ScheduleTimeSlots.AsNoTracking()
|
||||||
|
.Where(x => x.AcademicTermId == task.AcademicTermId && x.IsEnabled)
|
||||||
|
.OrderBy(x => x.PeriodNumber)
|
||||||
|
.Select(x => new
|
||||||
|
{
|
||||||
|
x.PeriodNumber,
|
||||||
|
x.Name,
|
||||||
|
StartsAt = x.StartsAt.ToString("HH:mm"),
|
||||||
|
EndsAt = x.EndsAt.ToString("HH:mm")
|
||||||
|
})
|
||||||
|
.ToListAsync(cancellationToken);
|
||||||
|
|
||||||
|
return Ok(new { Task = task, Sessions = sessions, Classrooms = classrooms, Periods = periods });
|
||||||
|
}
|
||||||
|
|
||||||
// ═══════════════ My adjustments ═══════════════
|
// ═══════════════ My adjustments ═══════════════
|
||||||
|
|
||||||
[HttpGet("mine")]
|
[HttpGet("mine")]
|
||||||
@@ -131,20 +241,41 @@ public sealed class CourseAdjustmentsController(
|
|||||||
var validation = await ValidateRequestAsync(request, cancellationToken);
|
var validation = await ValidateRequestAsync(request, cancellationToken);
|
||||||
if (validation is not null) return validation;
|
if (validation is not null) return validation;
|
||||||
|
|
||||||
|
var source = RequiresSourceOccurrence(request.Type)
|
||||||
|
? await LoadSourceOccurrenceAsync(
|
||||||
|
request.TeachingTaskId,
|
||||||
|
request.SourceScheduleEntryId,
|
||||||
|
request.SourceWeek,
|
||||||
|
cancellationToken)
|
||||||
|
: null;
|
||||||
|
var targetDayOfWeek = request.TargetDate.HasValue
|
||||||
|
? ToIsoDayOfWeek(request.TargetDate.Value.DayOfWeek)
|
||||||
|
: request.DayOfWeek;
|
||||||
|
|
||||||
var adj = new CourseAdjustment
|
var adj = new CourseAdjustment
|
||||||
{
|
{
|
||||||
TeachingTaskId = request.TeachingTaskId,
|
TeachingTaskId = request.TeachingTaskId,
|
||||||
Type = request.Type,
|
Type = request.Type,
|
||||||
ApplicantUserId = userId,
|
ApplicantUserId = userId,
|
||||||
Reason = request.Reason.Trim(),
|
Reason = request.Reason.Trim(),
|
||||||
|
SourceScheduleEntryId = source?.Entry.Id,
|
||||||
|
SourceWeek = source?.Week,
|
||||||
|
SourceDate = source?.Date,
|
||||||
|
SourceStartPeriod = source?.Entry.StartPeriod,
|
||||||
|
SourcePeriodCount = source?.Entry.PeriodCount,
|
||||||
|
SourceClassroomId = source?.Entry.ClassroomId,
|
||||||
TargetDate = request.TargetDate,
|
TargetDate = request.TargetDate,
|
||||||
DayOfWeek = request.DayOfWeek,
|
DayOfWeek = targetDayOfWeek,
|
||||||
StartPeriod = request.StartPeriod,
|
StartPeriod = request.StartPeriod,
|
||||||
PeriodCount = request.PeriodCount,
|
PeriodCount = source?.Entry.PeriodCount ?? request.PeriodCount,
|
||||||
ClassroomId = request.ClassroomId,
|
ClassroomId = request.ClassroomId,
|
||||||
SubstituteTeacherId = request.SubstituteTeacherId,
|
SubstituteTeacherId = request.SubstituteTeacherId,
|
||||||
CancelWeek = request.CancelWeek,
|
CancelWeek = request.Type == CourseAdjustmentType.Cancel
|
||||||
CancelDate = request.CancelDate
|
? source?.Week
|
||||||
|
: request.CancelWeek,
|
||||||
|
CancelDate = request.Type == CourseAdjustmentType.Cancel
|
||||||
|
? source?.Date
|
||||||
|
: request.CancelDate
|
||||||
};
|
};
|
||||||
|
|
||||||
if (request.Submit)
|
if (request.Submit)
|
||||||
@@ -193,6 +324,25 @@ public sealed class CourseAdjustmentsController(
|
|||||||
if (adj.Status != CourseAdjustmentStatus.Draft)
|
if (adj.Status != CourseAdjustmentStatus.Draft)
|
||||||
return ConflictProblem("只有草稿可以提交。");
|
return ConflictProblem("只有草稿可以提交。");
|
||||||
|
|
||||||
|
var validation = await ValidateRequestAsync(
|
||||||
|
new CourseAdjustmentRequest(
|
||||||
|
adj.TeachingTaskId,
|
||||||
|
adj.Type,
|
||||||
|
adj.Reason,
|
||||||
|
true,
|
||||||
|
adj.TargetDate,
|
||||||
|
adj.DayOfWeek,
|
||||||
|
adj.StartPeriod,
|
||||||
|
adj.PeriodCount,
|
||||||
|
adj.ClassroomId,
|
||||||
|
adj.SubstituteTeacherId,
|
||||||
|
adj.CancelWeek,
|
||||||
|
adj.CancelDate,
|
||||||
|
adj.SourceScheduleEntryId,
|
||||||
|
adj.SourceWeek),
|
||||||
|
cancellationToken);
|
||||||
|
if (validation is not null) return validation;
|
||||||
|
|
||||||
adj.Status = CourseAdjustmentStatus.Submitted;
|
adj.Status = CourseAdjustmentStatus.Submitted;
|
||||||
adj.SubmittedAt = DateTime.UtcNow;
|
adj.SubmittedAt = DateTime.UtcNow;
|
||||||
await db.SaveChangesAsync(cancellationToken);
|
await db.SaveChangesAsync(cancellationToken);
|
||||||
@@ -224,6 +374,12 @@ public sealed class CourseAdjustmentsController(
|
|||||||
.Include(x => x.TeachingTask)
|
.Include(x => x.TeachingTask)
|
||||||
.ThenInclude(x => x!.Course)
|
.ThenInclude(x => x!.Course)
|
||||||
.ThenInclude(x => x!.College)
|
.ThenInclude(x => x!.College)
|
||||||
|
.Include(x => x.TeachingTask)
|
||||||
|
.ThenInclude(x => x!.Teachers)
|
||||||
|
.Include(x => x.TeachingTask)
|
||||||
|
.ThenInclude(x => x!.Classes)
|
||||||
|
.Include(x => x.TeachingTask)
|
||||||
|
.ThenInclude(x => x!.AcademicTerm)
|
||||||
.FirstOrDefaultAsync(x => x.Id == id, cancellationToken);
|
.FirstOrDefaultAsync(x => x.Id == id, cancellationToken);
|
||||||
if (adj is null) return NotFound();
|
if (adj is null) return NotFound();
|
||||||
|
|
||||||
@@ -234,6 +390,12 @@ public sealed class CourseAdjustmentsController(
|
|||||||
if (adj.Status != CourseAdjustmentStatus.Submitted)
|
if (adj.Status != CourseAdjustmentStatus.Submitted)
|
||||||
return ConflictProblem("只有待审核的申请可以审批。");
|
return ConflictProblem("只有待审核的申请可以审批。");
|
||||||
|
|
||||||
|
var scheduleProblem = await ValidateApprovalScheduleAsync(
|
||||||
|
adj,
|
||||||
|
cancellationToken);
|
||||||
|
if (scheduleProblem is not null)
|
||||||
|
return ConflictProblem(scheduleProblem);
|
||||||
|
|
||||||
adj.Status = CourseAdjustmentStatus.Approved;
|
adj.Status = CourseAdjustmentStatus.Approved;
|
||||||
adj.ReviewedAt = DateTime.UtcNow;
|
adj.ReviewedAt = DateTime.UtcNow;
|
||||||
adj.ReviewedByUserId = currentUserDataScope.Current.UserId;
|
adj.ReviewedByUserId = currentUserDataScope.Current.UserId;
|
||||||
@@ -251,11 +413,9 @@ public sealed class CourseAdjustmentsController(
|
|||||||
NotificationCategory.Schedule);
|
NotificationCategory.Schedule);
|
||||||
|
|
||||||
// Notify affected students
|
// Notify affected students
|
||||||
var studentUserIds = await db.CourseEnrollments
|
var studentUserIds = await TeachingTaskRosterQuery
|
||||||
.Where(x =>
|
.ForTask(db, adj.TeachingTaskId)
|
||||||
x.CourseSelectionOffering!.TeachingTaskId == adj.TeachingTaskId &&
|
.Select(x => x.UserId)
|
||||||
x.Status == CourseEnrollmentStatus.Enrolled)
|
|
||||||
.Select(x => x.Student!.UserId)
|
|
||||||
.Where(uid => uid != null)
|
.Where(uid => uid != null)
|
||||||
.Select(uid => uid!.Value)
|
.Select(uid => uid!.Value)
|
||||||
.Distinct()
|
.Distinct()
|
||||||
@@ -278,81 +438,19 @@ public sealed class CourseAdjustmentsController(
|
|||||||
switch (adj.Type)
|
switch (adj.Type)
|
||||||
{
|
{
|
||||||
case CourseAdjustmentType.Reschedule:
|
case CourseAdjustmentType.Reschedule:
|
||||||
// Update existing schedule entries for this teaching task
|
var rescheduleSource = await LoadSourceEntryAsync(adj, ct);
|
||||||
if (adj.DayOfWeek.HasValue && adj.StartPeriod.HasValue)
|
ExcludeWeek(rescheduleSource, adj.SourceWeek!.Value);
|
||||||
{
|
db.ScheduleEntries.Add(CreateTargetEntry(adj, rescheduleSource.SchedulePlanId));
|
||||||
var entries = await db.ScheduleEntries
|
|
||||||
.Where(x => x.TeachingTaskId == adj.TeachingTaskId)
|
|
||||||
.ToListAsync(ct);
|
|
||||||
foreach (var entry in entries)
|
|
||||||
{
|
|
||||||
entry.DayOfWeek = adj.DayOfWeek.Value;
|
|
||||||
entry.StartPeriod = adj.StartPeriod.Value;
|
|
||||||
entry.PeriodCount = adj.PeriodCount ?? entry.PeriodCount;
|
|
||||||
}
|
|
||||||
if (adj.ClassroomId.HasValue)
|
|
||||||
{
|
|
||||||
foreach (var entry in entries)
|
|
||||||
entry.ClassroomId = adj.ClassroomId.Value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case CourseAdjustmentType.Cancel:
|
case CourseAdjustmentType.Cancel:
|
||||||
// Cancel: remove schedule entries for the specified week
|
var cancelSource = await LoadSourceEntryAsync(adj, ct);
|
||||||
if (adj.CancelWeek.HasValue)
|
ExcludeWeek(cancelSource, adj.SourceWeek!.Value);
|
||||||
{
|
|
||||||
var entries = await db.ScheduleEntries
|
|
||||||
.Where(x => x.TeachingTaskId == adj.TeachingTaskId &&
|
|
||||||
x.StartWeek <= adj.CancelWeek.Value &&
|
|
||||||
x.EndWeek >= adj.CancelWeek.Value)
|
|
||||||
.ToListAsync(ct);
|
|
||||||
foreach (var entry in entries)
|
|
||||||
{
|
|
||||||
// Split the entry to exclude the cancelled week
|
|
||||||
if (entry.StartWeek == adj.CancelWeek.Value &&
|
|
||||||
entry.EndWeek == adj.CancelWeek.Value)
|
|
||||||
{
|
|
||||||
db.ScheduleEntries.Remove(entry);
|
|
||||||
}
|
|
||||||
else if (entry.StartWeek == adj.CancelWeek.Value)
|
|
||||||
{
|
|
||||||
entry.StartWeek = adj.CancelWeek.Value + 1;
|
|
||||||
}
|
|
||||||
else if (entry.EndWeek == adj.CancelWeek.Value)
|
|
||||||
{
|
|
||||||
entry.EndWeek = adj.CancelWeek.Value - 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case CourseAdjustmentType.Makeup:
|
case CourseAdjustmentType.Makeup:
|
||||||
// Makeup: add a temp schedule entry for the makeup date
|
var makeupSource = await LoadSourceEntryAsync(adj, ct);
|
||||||
if (adj.TargetDate.HasValue && adj.DayOfWeek.HasValue &&
|
db.ScheduleEntries.Add(CreateTargetEntry(adj, makeupSource.SchedulePlanId));
|
||||||
adj.StartPeriod.HasValue)
|
|
||||||
{
|
|
||||||
var publishedPlan = await db.SchedulePlans
|
|
||||||
.Where(x => x.AcademicTermId == adj.TeachingTask!.AcademicTermId &&
|
|
||||||
x.Status == SchedulePlanStatus.Published)
|
|
||||||
.FirstOrDefaultAsync(ct);
|
|
||||||
if (publishedPlan is not null)
|
|
||||||
{
|
|
||||||
db.ScheduleEntries.Add(new ScheduleEntry
|
|
||||||
{
|
|
||||||
SchedulePlanId = publishedPlan.Id,
|
|
||||||
TeachingTaskId = adj.TeachingTaskId,
|
|
||||||
ClassroomId = adj.ClassroomId,
|
|
||||||
DayOfWeek = adj.DayOfWeek.Value,
|
|
||||||
StartPeriod = adj.StartPeriod.Value,
|
|
||||||
PeriodCount = adj.PeriodCount ?? 2,
|
|
||||||
StartWeek = 1,
|
|
||||||
EndWeek = 1,
|
|
||||||
WeekPattern = WeekPattern.All,
|
|
||||||
Notes = $"补课(原申请 {adj.CreatedAt:yyyy-MM-dd})"
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case CourseAdjustmentType.Substitute:
|
case CourseAdjustmentType.Substitute:
|
||||||
@@ -442,15 +540,13 @@ public sealed class CourseAdjustmentsController(
|
|||||||
{
|
{
|
||||||
case CourseAdjustmentType.Reschedule:
|
case CourseAdjustmentType.Reschedule:
|
||||||
case CourseAdjustmentType.Makeup:
|
case CourseAdjustmentType.Makeup:
|
||||||
if (!request.DayOfWeek.HasValue || request.DayOfWeek is < 1 or > 7)
|
if (!request.TargetDate.HasValue)
|
||||||
return ValidationProblem("请选择有效的上课日。");
|
return ValidationProblem(
|
||||||
|
request.Type == CourseAdjustmentType.Reschedule
|
||||||
|
? "请选择调课后的日期。"
|
||||||
|
: "请选择补课日期。");
|
||||||
if (!request.StartPeriod.HasValue || request.StartPeriod < 1)
|
if (!request.StartPeriod.HasValue || request.StartPeriod < 1)
|
||||||
return ValidationProblem("请选择起始节次。");
|
return ValidationProblem("请选择起始节次。");
|
||||||
if (!request.PeriodCount.HasValue || request.PeriodCount < 1)
|
|
||||||
return ValidationProblem("请选择持续节数。");
|
|
||||||
if (request.Type == CourseAdjustmentType.Makeup &&
|
|
||||||
!request.TargetDate.HasValue)
|
|
||||||
return ValidationProblem("补课必须指定日期。");
|
|
||||||
break;
|
break;
|
||||||
case CourseAdjustmentType.Substitute:
|
case CourseAdjustmentType.Substitute:
|
||||||
if (!request.SubstituteTeacherId.HasValue)
|
if (!request.SubstituteTeacherId.HasValue)
|
||||||
@@ -461,14 +557,226 @@ public sealed class CourseAdjustmentsController(
|
|||||||
return ValidationProblem("代课教师不存在或已离职。");
|
return ValidationProblem("代课教师不存在或已离职。");
|
||||||
break;
|
break;
|
||||||
case CourseAdjustmentType.Cancel:
|
case CourseAdjustmentType.Cancel:
|
||||||
if (!request.CancelWeek.HasValue && !request.CancelDate.HasValue)
|
|
||||||
return ValidationProblem("停课需指定周次或日期。");
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (RequiresSourceOccurrence(request.Type))
|
||||||
|
{
|
||||||
|
var source = await LoadSourceOccurrenceAsync(
|
||||||
|
request.TeachingTaskId,
|
||||||
|
request.SourceScheduleEntryId,
|
||||||
|
request.SourceWeek,
|
||||||
|
cancellationToken);
|
||||||
|
if (source is null)
|
||||||
|
return ValidationProblem("请选择当前已发布课表中的具体原课次。");
|
||||||
|
|
||||||
|
var duplicate = await db.CourseAdjustments.AsNoTracking().AnyAsync(x =>
|
||||||
|
x.SourceScheduleEntryId == source.Entry.Id &&
|
||||||
|
x.SourceWeek == source.Week &&
|
||||||
|
(x.Status == CourseAdjustmentStatus.Submitted ||
|
||||||
|
x.Status == CourseAdjustmentStatus.Approved),
|
||||||
|
cancellationToken);
|
||||||
|
if (duplicate)
|
||||||
|
return ConflictProblem("该课次已有待审核或已通过的调停课记录。");
|
||||||
|
|
||||||
|
if (request.TargetDate.HasValue &&
|
||||||
|
(request.TargetDate < source.Term.StartDate ||
|
||||||
|
request.TargetDate > source.Term.EndDate))
|
||||||
|
return ValidationProblem("目标日期必须在本学期起止日期内。");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (request.ClassroomId.HasValue &&
|
||||||
|
!await db.Classrooms.AsNoTracking().AnyAsync(x =>
|
||||||
|
x.Id == request.ClassroomId && x.IsEnabled,
|
||||||
|
cancellationToken))
|
||||||
|
return ValidationProblem("目标教室不存在或已停用。");
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<string?> ValidateApprovalScheduleAsync(
|
||||||
|
CourseAdjustment adjustment,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
if (!RequiresSourceOccurrence(adjustment.Type)) return null;
|
||||||
|
|
||||||
|
var source = await LoadSourceOccurrenceAsync(
|
||||||
|
adjustment.TeachingTaskId,
|
||||||
|
adjustment.SourceScheduleEntryId,
|
||||||
|
adjustment.SourceWeek,
|
||||||
|
cancellationToken);
|
||||||
|
if (source is null)
|
||||||
|
return "原课次已不在当前发布课表中,请退回后重新选择。";
|
||||||
|
if (adjustment.Type == CourseAdjustmentType.Cancel) return null;
|
||||||
|
if (!adjustment.TargetDate.HasValue || !adjustment.StartPeriod.HasValue)
|
||||||
|
return "目标上课日期或节次不完整。";
|
||||||
|
|
||||||
|
var targetWeek = ResolveWeek(source.Term.StartDate, adjustment.TargetDate.Value);
|
||||||
|
var targetDay = ToIsoDayOfWeek(adjustment.TargetDate.Value.DayOfWeek);
|
||||||
|
var target = new ScheduleEntry
|
||||||
|
{
|
||||||
|
TeachingTaskId = adjustment.TeachingTaskId,
|
||||||
|
TeachingTask = adjustment.TeachingTask,
|
||||||
|
ClassroomId = adjustment.ClassroomId ?? adjustment.SourceClassroomId,
|
||||||
|
DayOfWeek = targetDay,
|
||||||
|
StartPeriod = adjustment.StartPeriod.Value,
|
||||||
|
PeriodCount = adjustment.SourcePeriodCount ?? source.Entry.PeriodCount,
|
||||||
|
StartWeek = targetWeek,
|
||||||
|
EndWeek = targetWeek,
|
||||||
|
WeekPattern = WeekPattern.All
|
||||||
|
};
|
||||||
|
var possibleConflicts = await db.ScheduleEntries.AsNoTracking()
|
||||||
|
.Include(x => x.TeachingTask)
|
||||||
|
.ThenInclude(x => x!.Teachers)
|
||||||
|
.Include(x => x.TeachingTask)
|
||||||
|
.ThenInclude(x => x!.Classes)
|
||||||
|
.Where(x =>
|
||||||
|
x.SchedulePlan!.Status == SchedulePlanStatus.Published &&
|
||||||
|
x.Id != adjustment.SourceScheduleEntryId &&
|
||||||
|
x.DayOfWeek == targetDay &&
|
||||||
|
x.StartWeek <= targetWeek &&
|
||||||
|
x.EndWeek >= targetWeek &&
|
||||||
|
x.StartPeriod < target.StartPeriod + target.PeriodCount &&
|
||||||
|
target.StartPeriod < x.StartPeriod + x.PeriodCount)
|
||||||
|
.ToListAsync(cancellationToken);
|
||||||
|
foreach (var existing in possibleConflicts.Where(x =>
|
||||||
|
IncludesWeek(x.WeekPattern, targetWeek)))
|
||||||
|
{
|
||||||
|
var reason = ScheduleConflictDetector.ConflictReason(target, existing);
|
||||||
|
if (reason is not null)
|
||||||
|
return $"目标时间存在{reason}冲突:{existing.TeachingTask!.Name}。";
|
||||||
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task<SourceOccurrence?> LoadSourceOccurrenceAsync(
|
||||||
|
Guid teachingTaskId,
|
||||||
|
Guid? scheduleEntryId,
|
||||||
|
int? week,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
if (!scheduleEntryId.HasValue || !week.HasValue) return null;
|
||||||
|
var entry = await db.ScheduleEntries.AsNoTracking()
|
||||||
|
.Include(x => x.TeachingTask)
|
||||||
|
.ThenInclude(x => x!.AcademicTerm)
|
||||||
|
.FirstOrDefaultAsync(x =>
|
||||||
|
x.Id == scheduleEntryId.Value &&
|
||||||
|
x.TeachingTaskId == teachingTaskId &&
|
||||||
|
x.SchedulePlan!.Status == SchedulePlanStatus.Published,
|
||||||
|
cancellationToken);
|
||||||
|
if (entry?.TeachingTask?.AcademicTerm is null ||
|
||||||
|
week < entry.StartWeek || week > entry.EndWeek ||
|
||||||
|
!IncludesWeek(entry.WeekPattern, week.Value))
|
||||||
|
return null;
|
||||||
|
var date = ResolveDate(
|
||||||
|
entry.TeachingTask.AcademicTerm.StartDate,
|
||||||
|
week.Value,
|
||||||
|
entry.DayOfWeek);
|
||||||
|
if (date < entry.TeachingTask.AcademicTerm.StartDate ||
|
||||||
|
date > entry.TeachingTask.AcademicTerm.EndDate)
|
||||||
|
return null;
|
||||||
|
return new SourceOccurrence(
|
||||||
|
entry,
|
||||||
|
entry.TeachingTask.AcademicTerm,
|
||||||
|
week.Value,
|
||||||
|
date);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Task<ScheduleEntry> LoadSourceEntryAsync(
|
||||||
|
CourseAdjustment adjustment,
|
||||||
|
CancellationToken cancellationToken) =>
|
||||||
|
db.ScheduleEntries.SingleAsync(x =>
|
||||||
|
x.Id == adjustment.SourceScheduleEntryId &&
|
||||||
|
x.TeachingTaskId == adjustment.TeachingTaskId &&
|
||||||
|
x.SchedulePlan!.Status == SchedulePlanStatus.Published,
|
||||||
|
cancellationToken);
|
||||||
|
|
||||||
|
private void ExcludeWeek(ScheduleEntry entry, int week)
|
||||||
|
{
|
||||||
|
db.ScheduleEntries.Remove(entry);
|
||||||
|
if (entry.StartWeek <= week - 1 &&
|
||||||
|
HasIncludedWeek(entry.WeekPattern, entry.StartWeek, week - 1))
|
||||||
|
db.ScheduleEntries.Add(CloneEntry(entry, entry.StartWeek, week - 1));
|
||||||
|
if (week + 1 <= entry.EndWeek &&
|
||||||
|
HasIncludedWeek(entry.WeekPattern, week + 1, entry.EndWeek))
|
||||||
|
db.ScheduleEntries.Add(CloneEntry(entry, week + 1, entry.EndWeek));
|
||||||
|
}
|
||||||
|
|
||||||
|
private ScheduleEntry CreateTargetEntry(
|
||||||
|
CourseAdjustment adjustment,
|
||||||
|
Guid schedulePlanId)
|
||||||
|
{
|
||||||
|
var targetWeek = ResolveWeek(
|
||||||
|
adjustment.TeachingTask!.AcademicTerm!.StartDate,
|
||||||
|
adjustment.TargetDate!.Value);
|
||||||
|
return new ScheduleEntry
|
||||||
|
{
|
||||||
|
SchedulePlanId = schedulePlanId,
|
||||||
|
TeachingTaskId = adjustment.TeachingTaskId,
|
||||||
|
ClassroomId = adjustment.ClassroomId ?? adjustment.SourceClassroomId,
|
||||||
|
DayOfWeek = ToIsoDayOfWeek(adjustment.TargetDate.Value.DayOfWeek),
|
||||||
|
StartPeriod = adjustment.StartPeriod!.Value,
|
||||||
|
PeriodCount = adjustment.SourcePeriodCount!.Value,
|
||||||
|
StartWeek = targetWeek,
|
||||||
|
EndWeek = targetWeek,
|
||||||
|
WeekPattern = WeekPattern.All,
|
||||||
|
Notes = adjustment.Type == CourseAdjustmentType.Reschedule
|
||||||
|
? $"调课(原第 {adjustment.SourceWeek} 周)"
|
||||||
|
: $"补课(对应原第 {adjustment.SourceWeek} 周课次)"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ScheduleEntry CloneEntry(
|
||||||
|
ScheduleEntry source,
|
||||||
|
int startWeek,
|
||||||
|
int endWeek) => new()
|
||||||
|
{
|
||||||
|
SchedulePlanId = source.SchedulePlanId,
|
||||||
|
TeachingTaskId = source.TeachingTaskId,
|
||||||
|
ClassroomId = source.ClassroomId,
|
||||||
|
DayOfWeek = source.DayOfWeek,
|
||||||
|
StartPeriod = source.StartPeriod,
|
||||||
|
PeriodCount = source.PeriodCount,
|
||||||
|
StartWeek = startWeek,
|
||||||
|
EndWeek = endWeek,
|
||||||
|
WeekPattern = source.WeekPattern,
|
||||||
|
Notes = source.Notes
|
||||||
|
};
|
||||||
|
|
||||||
|
private static bool RequiresSourceOccurrence(CourseAdjustmentType type) =>
|
||||||
|
type is CourseAdjustmentType.Reschedule or
|
||||||
|
CourseAdjustmentType.Cancel or
|
||||||
|
CourseAdjustmentType.Makeup;
|
||||||
|
|
||||||
|
private static bool IncludesWeek(WeekPattern pattern, int week) =>
|
||||||
|
pattern == WeekPattern.All ||
|
||||||
|
pattern == WeekPattern.Odd && week % 2 == 1 ||
|
||||||
|
pattern == WeekPattern.Even && week % 2 == 0;
|
||||||
|
|
||||||
|
private static bool HasIncludedWeek(WeekPattern pattern, int startWeek, int endWeek) =>
|
||||||
|
Enumerable.Range(startWeek, endWeek - startWeek + 1)
|
||||||
|
.Any(week => IncludesWeek(pattern, week));
|
||||||
|
|
||||||
|
private static DateOnly ResolveDate(DateOnly termStartDate, int week, int dayOfWeek)
|
||||||
|
{
|
||||||
|
var startDay = (int)termStartDate.DayOfWeek;
|
||||||
|
var daysSinceMonday = (startDay + 6) % 7;
|
||||||
|
var firstWeekMonday = termStartDate.AddDays(-daysSinceMonday);
|
||||||
|
return firstWeekMonday.AddDays((week - 1) * 7 + dayOfWeek - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int ResolveWeek(DateOnly termStartDate, DateOnly date)
|
||||||
|
{
|
||||||
|
var startDay = (int)termStartDate.DayOfWeek;
|
||||||
|
var daysSinceMonday = (startDay + 6) % 7;
|
||||||
|
var firstWeekMonday = termStartDate.AddDays(-daysSinceMonday);
|
||||||
|
return (date.DayNumber - firstWeekMonday.DayNumber) / 7 + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int ToIsoDayOfWeek(DayOfWeek dayOfWeek) =>
|
||||||
|
dayOfWeek == System.DayOfWeek.Sunday ? 7 : (int)dayOfWeek;
|
||||||
|
|
||||||
private IQueryable<TeachingTask> AccessibleTeachingTasks()
|
private IQueryable<TeachingTask> AccessibleTeachingTasks()
|
||||||
{
|
{
|
||||||
var source = db.TeachingTasks.AsQueryable();
|
var source = db.TeachingTasks.AsQueryable();
|
||||||
@@ -483,7 +791,7 @@ public sealed class CourseAdjustmentsController(
|
|||||||
return source.Where(_ => false);
|
return source.Where(_ => false);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static System.Linq.Expressions.Expression<
|
private System.Linq.Expressions.Expression<
|
||||||
Func<CourseAdjustment, object>> AdjustmentProjection() => x => new
|
Func<CourseAdjustment, object>> AdjustmentProjection() => x => new
|
||||||
{
|
{
|
||||||
x.Id,
|
x.Id,
|
||||||
@@ -500,6 +808,17 @@ public sealed class CourseAdjustmentsController(
|
|||||||
x.Status,
|
x.Status,
|
||||||
x.Reason,
|
x.Reason,
|
||||||
x.ReviewComment,
|
x.ReviewComment,
|
||||||
|
x.SourceScheduleEntryId,
|
||||||
|
x.SourceWeek,
|
||||||
|
x.SourceDate,
|
||||||
|
x.SourceStartPeriod,
|
||||||
|
x.SourcePeriodCount,
|
||||||
|
SourceClassroomName = x.SourceClassroomId == null
|
||||||
|
? null
|
||||||
|
: db.Classrooms
|
||||||
|
.Where(room => room.Id == x.SourceClassroomId)
|
||||||
|
.Select(room => room.Building!.Name + " " + room.Name)
|
||||||
|
.FirstOrDefault(),
|
||||||
x.TargetDate,
|
x.TargetDate,
|
||||||
x.DayOfWeek,
|
x.DayOfWeek,
|
||||||
x.StartPeriod,
|
x.StartPeriod,
|
||||||
@@ -515,6 +834,12 @@ public sealed class CourseAdjustmentsController(
|
|||||||
x.CreatedAt
|
x.CreatedAt
|
||||||
};
|
};
|
||||||
|
|
||||||
|
private sealed record SourceOccurrence(
|
||||||
|
ScheduleEntry Entry,
|
||||||
|
AcademicTerm Term,
|
||||||
|
int Week,
|
||||||
|
DateOnly Date);
|
||||||
|
|
||||||
private ActionResult ConflictProblem(string detail) =>
|
private ActionResult ConflictProblem(string detail) =>
|
||||||
Conflict(new ProblemDetails
|
Conflict(new ProblemDetails
|
||||||
{
|
{
|
||||||
@@ -538,7 +863,9 @@ public sealed record CourseAdjustmentRequest(
|
|||||||
Guid? ClassroomId,
|
Guid? ClassroomId,
|
||||||
Guid? SubstituteTeacherId,
|
Guid? SubstituteTeacherId,
|
||||||
int? CancelWeek,
|
int? CancelWeek,
|
||||||
DateOnly? CancelDate);
|
DateOnly? CancelDate,
|
||||||
|
Guid? SourceScheduleEntryId = null,
|
||||||
|
int? SourceWeek = null);
|
||||||
|
|
||||||
public sealed record RejectionRequest(
|
public sealed record RejectionRequest(
|
||||||
[MaxLength(500)] string? Comment);
|
[MaxLength(500)] string? Comment);
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
using System.ComponentModel.DataAnnotations;
|
using System.ComponentModel.DataAnnotations;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
|
using Jiaowu.Api.Contracts;
|
||||||
using Jiaowu.Api.Domain.Academic;
|
using Jiaowu.Api.Domain.Academic;
|
||||||
using Jiaowu.Api.Domain.Identity;
|
using Jiaowu.Api.Domain.Identity;
|
||||||
using Jiaowu.Api.Infrastructure.Auth;
|
using Jiaowu.Api.Infrastructure.Auth;
|
||||||
@@ -41,18 +42,38 @@ public sealed class GradesController(
|
|||||||
public async Task<ActionResult> GetSheets(
|
public async Task<ActionResult> GetSheets(
|
||||||
Guid? academicTermId,
|
Guid? academicTermId,
|
||||||
GradeSheetStatus? status,
|
GradeSheetStatus? status,
|
||||||
CancellationToken cancellationToken)
|
string? keyword = null,
|
||||||
|
int page = 1,
|
||||||
|
int pageSize = 20,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
|
page = Math.Max(1, page);
|
||||||
|
pageSize = Math.Clamp(pageSize, 10, 50);
|
||||||
var source = AccessibleTasks().AsNoTracking()
|
var source = AccessibleTasks().AsNoTracking()
|
||||||
.Where(x =>
|
.Where(x =>
|
||||||
x.Status == TeachingTaskStatus.Published ||
|
x.Status == TeachingTaskStatus.Published ||
|
||||||
x.Status == TeachingTaskStatus.Closed);
|
x.Status == TeachingTaskStatus.Closed);
|
||||||
if (academicTermId.HasValue)
|
if (academicTermId.HasValue)
|
||||||
source = source.Where(x => x.AcademicTermId == academicTermId);
|
source = source.Where(x => x.AcademicTermId == academicTermId);
|
||||||
|
if (status.HasValue)
|
||||||
|
source = source.Where(x => db.GradeSheets.Any(sheet =>
|
||||||
|
sheet.TeachingTaskId == x.Id && sheet.Status == status.Value));
|
||||||
|
if (!string.IsNullOrWhiteSpace(keyword))
|
||||||
|
{
|
||||||
|
keyword = keyword.Trim();
|
||||||
|
source = source.Where(x =>
|
||||||
|
x.TaskNumber.Contains(keyword) ||
|
||||||
|
x.Name.Contains(keyword) ||
|
||||||
|
x.Course!.Code.Contains(keyword) ||
|
||||||
|
x.Course.Name.Contains(keyword));
|
||||||
|
}
|
||||||
|
|
||||||
|
var total = await source.CountAsync(cancellationToken);
|
||||||
var items = await source
|
var items = await source
|
||||||
.OrderByDescending(x => x.AcademicTerm!.StartDate)
|
.OrderByDescending(x => x.AcademicTerm!.StartDate)
|
||||||
.ThenBy(x => x.TaskNumber)
|
.ThenBy(x => x.TaskNumber)
|
||||||
|
.Skip((page - 1) * pageSize)
|
||||||
|
.Take(pageSize)
|
||||||
.Select(x => new
|
.Select(x => new
|
||||||
{
|
{
|
||||||
x.Id,
|
x.Id,
|
||||||
@@ -96,10 +117,7 @@ public sealed class GradesController(
|
|||||||
.FirstOrDefault()
|
.FirstOrDefault()
|
||||||
})
|
})
|
||||||
.ToListAsync(cancellationToken);
|
.ToListAsync(cancellationToken);
|
||||||
|
return Ok(new PagedResult<object>(items, total, page, pageSize));
|
||||||
if (status.HasValue)
|
|
||||||
items = items.Where(x => x.Sheet?.Status == status.Value).ToList();
|
|
||||||
return Ok(items);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost("sheets")]
|
[HttpPost("sheets")]
|
||||||
@@ -164,8 +182,18 @@ public sealed class GradesController(
|
|||||||
|
|
||||||
[HttpGet("sheets/{id:guid}")]
|
[HttpGet("sheets/{id:guid}")]
|
||||||
[Authorize(Roles = SheetUsers)]
|
[Authorize(Roles = SheetUsers)]
|
||||||
public async Task<ActionResult> GetSheet(Guid id, CancellationToken cancellationToken)
|
public async Task<ActionResult> GetSheet(
|
||||||
|
Guid id,
|
||||||
|
int recordPage = 1,
|
||||||
|
int recordPageSize = 50,
|
||||||
|
string? studentKeyword = null,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
|
recordPage = Math.Max(1, recordPage);
|
||||||
|
recordPageSize = Math.Clamp(recordPageSize, 10, 100);
|
||||||
|
studentKeyword = string.IsNullOrWhiteSpace(studentKeyword)
|
||||||
|
? null
|
||||||
|
: studentKeyword.Trim();
|
||||||
var sheet = await AccessibleSheets().AsNoTracking()
|
var sheet = await AccessibleSheets().AsNoTracking()
|
||||||
.Where(x => x.Id == id)
|
.Where(x => x.Id == id)
|
||||||
.Select(x => new
|
.Select(x => new
|
||||||
@@ -199,37 +227,49 @@ public sealed class GradesController(
|
|||||||
x.SubmittedAt,
|
x.SubmittedAt,
|
||||||
x.ReviewedAt,
|
x.ReviewedAt,
|
||||||
x.PublishedAt,
|
x.PublishedAt,
|
||||||
Records = x.Records
|
|
||||||
.OrderBy(record => record.Student!.StudentNumber)
|
|
||||||
.Select(record => new
|
|
||||||
{
|
|
||||||
record.Id,
|
|
||||||
record.StudentId,
|
|
||||||
record.Student!.StudentNumber,
|
|
||||||
record.Student.Name,
|
|
||||||
ClassName = record.Student.AdministrativeClass!.Name,
|
|
||||||
record.RegularScore,
|
|
||||||
record.FinalScore,
|
|
||||||
ItemScores = record.ItemScores
|
|
||||||
.OrderBy(itemScore => itemScore.GradeItem!.SortOrder)
|
|
||||||
.Select(itemScore => new
|
|
||||||
{
|
|
||||||
itemScore.GradeItemId,
|
|
||||||
itemScore.GradeItem!.Name,
|
|
||||||
itemScore.Score
|
|
||||||
}),
|
|
||||||
record.TotalScore,
|
|
||||||
record.GradePoint,
|
|
||||||
record.ExamStatus,
|
|
||||||
record.Notes,
|
|
||||||
record.UpdatedAt
|
|
||||||
}),
|
|
||||||
x.CreatedAt,
|
x.CreatedAt,
|
||||||
x.UpdatedAt
|
x.UpdatedAt
|
||||||
})
|
})
|
||||||
.FirstOrDefaultAsync(cancellationToken);
|
.FirstOrDefaultAsync(cancellationToken);
|
||||||
if (sheet is null) return NotFound();
|
if (sheet is null) return NotFound();
|
||||||
|
|
||||||
|
var recordsSource = db.GradeRecords.AsNoTracking()
|
||||||
|
.Where(record => record.GradeSheetId == id);
|
||||||
|
if (studentKeyword is not null)
|
||||||
|
recordsSource = recordsSource.Where(record =>
|
||||||
|
record.Student!.StudentNumber.Contains(studentKeyword) ||
|
||||||
|
record.Student.Name.Contains(studentKeyword) ||
|
||||||
|
record.Student.AdministrativeClass!.Name.Contains(studentKeyword));
|
||||||
|
var recordTotal = await recordsSource.CountAsync(cancellationToken);
|
||||||
|
var records = await recordsSource
|
||||||
|
.OrderBy(record => record.Student!.StudentNumber)
|
||||||
|
.Skip((recordPage - 1) * recordPageSize)
|
||||||
|
.Take(recordPageSize)
|
||||||
|
.Select(record => new
|
||||||
|
{
|
||||||
|
record.Id,
|
||||||
|
record.StudentId,
|
||||||
|
record.Student!.StudentNumber,
|
||||||
|
record.Student.Name,
|
||||||
|
ClassName = record.Student.AdministrativeClass!.Name,
|
||||||
|
record.RegularScore,
|
||||||
|
record.FinalScore,
|
||||||
|
ItemScores = record.ItemScores
|
||||||
|
.OrderBy(itemScore => itemScore.GradeItem!.SortOrder)
|
||||||
|
.Select(itemScore => new
|
||||||
|
{
|
||||||
|
itemScore.GradeItemId,
|
||||||
|
itemScore.GradeItem!.Name,
|
||||||
|
itemScore.Score
|
||||||
|
}),
|
||||||
|
record.TotalScore,
|
||||||
|
record.GradePoint,
|
||||||
|
record.ExamStatus,
|
||||||
|
record.Notes,
|
||||||
|
record.UpdatedAt
|
||||||
|
})
|
||||||
|
.ToListAsync(cancellationToken);
|
||||||
|
|
||||||
var task = await db.TeachingTasks.AsNoTracking()
|
var task = await db.TeachingTasks.AsNoTracking()
|
||||||
.Include(x => x.Teachers)
|
.Include(x => x.Teachers)
|
||||||
.ThenInclude(x => x.Teacher)
|
.ThenInclude(x => x.Teacher)
|
||||||
@@ -243,7 +283,36 @@ public sealed class GradesController(
|
|||||||
|
|
||||||
return Ok(new
|
return Ok(new
|
||||||
{
|
{
|
||||||
Sheet = sheet,
|
Sheet = new
|
||||||
|
{
|
||||||
|
sheet.Id,
|
||||||
|
sheet.TeachingTaskId,
|
||||||
|
sheet.TaskNumber,
|
||||||
|
sheet.TaskName,
|
||||||
|
sheet.AcademicTermId,
|
||||||
|
sheet.TermName,
|
||||||
|
sheet.CourseCode,
|
||||||
|
sheet.CourseName,
|
||||||
|
sheet.CourseCollegeId,
|
||||||
|
sheet.CourseCollegeName,
|
||||||
|
sheet.Credits,
|
||||||
|
sheet.TeacherNames,
|
||||||
|
sheet.ClassNames,
|
||||||
|
sheet.RegularWeight,
|
||||||
|
sheet.FinalWeight,
|
||||||
|
sheet.Items,
|
||||||
|
sheet.Status,
|
||||||
|
sheet.ReviewComment,
|
||||||
|
sheet.SubmittedAt,
|
||||||
|
sheet.ReviewedAt,
|
||||||
|
sheet.PublishedAt,
|
||||||
|
RecordTotal = recordTotal,
|
||||||
|
RecordPage = recordPage,
|
||||||
|
RecordPageSize = recordPageSize,
|
||||||
|
Records = records,
|
||||||
|
sheet.CreatedAt,
|
||||||
|
sheet.UpdatedAt
|
||||||
|
},
|
||||||
CanEdit = CanEditScores(task) &&
|
CanEdit = CanEditScores(task) &&
|
||||||
sheet.Status is GradeSheetStatus.Draft or GradeSheetStatus.Returned,
|
sheet.Status is GradeSheetStatus.Draft or GradeSheetStatus.Returned,
|
||||||
CanReview = isCourseCollegeReviewer && sheet.Status == GradeSheetStatus.Submitted,
|
CanReview = isCourseCollegeReviewer && sheet.Status == GradeSheetStatus.Submitted,
|
||||||
|
|||||||
@@ -10,7 +10,15 @@ public sealed class CourseAdjustment : EntityBase
|
|||||||
public CourseAdjustmentStatus Status { get; set; } = CourseAdjustmentStatus.Draft;
|
public CourseAdjustmentStatus Status { get; set; } = CourseAdjustmentStatus.Draft;
|
||||||
public Guid ApplicantUserId { get; set; }
|
public Guid ApplicantUserId { get; set; }
|
||||||
|
|
||||||
// For Reschedule / Makeup
|
// The concrete published timetable occurrence being adjusted.
|
||||||
|
public Guid? SourceScheduleEntryId { get; set; }
|
||||||
|
public int? SourceWeek { get; set; }
|
||||||
|
public DateOnly? SourceDate { get; set; }
|
||||||
|
public int? SourceStartPeriod { get; set; }
|
||||||
|
public int? SourcePeriodCount { get; set; }
|
||||||
|
public Guid? SourceClassroomId { get; set; }
|
||||||
|
|
||||||
|
// Target occurrence for Reschedule / Makeup
|
||||||
public DateOnly? TargetDate { get; set; }
|
public DateOnly? TargetDate { get; set; }
|
||||||
public int? DayOfWeek { get; set; }
|
public int? DayOfWeek { get; set; }
|
||||||
public int? StartPeriod { get; set; }
|
public int? StartPeriod { get; set; }
|
||||||
|
|||||||
Loaded 3 of 12 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user