调课
This commit is contained in:
@@ -3,6 +3,8 @@ using Jiaowu.Api.Domain.Academic;
|
||||
using Jiaowu.Api.Domain.Identity;
|
||||
using Jiaowu.Api.Infrastructure.Auth;
|
||||
using Jiaowu.Api.Infrastructure.Persistence;
|
||||
using Jiaowu.Api.Infrastructure.Scheduling;
|
||||
using Jiaowu.Api.Infrastructure.Teaching;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
@@ -58,6 +60,114 @@ public sealed class CourseAdjustmentsController(
|
||||
.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 ═══════════════
|
||||
|
||||
[HttpGet("mine")]
|
||||
@@ -131,20 +241,41 @@ public sealed class CourseAdjustmentsController(
|
||||
var validation = await ValidateRequestAsync(request, cancellationToken);
|
||||
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
|
||||
{
|
||||
TeachingTaskId = request.TeachingTaskId,
|
||||
Type = request.Type,
|
||||
ApplicantUserId = userId,
|
||||
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,
|
||||
DayOfWeek = request.DayOfWeek,
|
||||
DayOfWeek = targetDayOfWeek,
|
||||
StartPeriod = request.StartPeriod,
|
||||
PeriodCount = request.PeriodCount,
|
||||
PeriodCount = source?.Entry.PeriodCount ?? request.PeriodCount,
|
||||
ClassroomId = request.ClassroomId,
|
||||
SubstituteTeacherId = request.SubstituteTeacherId,
|
||||
CancelWeek = request.CancelWeek,
|
||||
CancelDate = request.CancelDate
|
||||
CancelWeek = request.Type == CourseAdjustmentType.Cancel
|
||||
? source?.Week
|
||||
: request.CancelWeek,
|
||||
CancelDate = request.Type == CourseAdjustmentType.Cancel
|
||||
? source?.Date
|
||||
: request.CancelDate
|
||||
};
|
||||
|
||||
if (request.Submit)
|
||||
@@ -193,6 +324,25 @@ public sealed class CourseAdjustmentsController(
|
||||
if (adj.Status != CourseAdjustmentStatus.Draft)
|
||||
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.SubmittedAt = DateTime.UtcNow;
|
||||
await db.SaveChangesAsync(cancellationToken);
|
||||
@@ -224,6 +374,12 @@ public sealed class CourseAdjustmentsController(
|
||||
.Include(x => x.TeachingTask)
|
||||
.ThenInclude(x => x!.Course)
|
||||
.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);
|
||||
if (adj is null) return NotFound();
|
||||
|
||||
@@ -234,6 +390,12 @@ public sealed class CourseAdjustmentsController(
|
||||
if (adj.Status != CourseAdjustmentStatus.Submitted)
|
||||
return ConflictProblem("只有待审核的申请可以审批。");
|
||||
|
||||
var scheduleProblem = await ValidateApprovalScheduleAsync(
|
||||
adj,
|
||||
cancellationToken);
|
||||
if (scheduleProblem is not null)
|
||||
return ConflictProblem(scheduleProblem);
|
||||
|
||||
adj.Status = CourseAdjustmentStatus.Approved;
|
||||
adj.ReviewedAt = DateTime.UtcNow;
|
||||
adj.ReviewedByUserId = currentUserDataScope.Current.UserId;
|
||||
@@ -251,11 +413,9 @@ public sealed class CourseAdjustmentsController(
|
||||
NotificationCategory.Schedule);
|
||||
|
||||
// Notify affected students
|
||||
var studentUserIds = await db.CourseEnrollments
|
||||
.Where(x =>
|
||||
x.CourseSelectionOffering!.TeachingTaskId == adj.TeachingTaskId &&
|
||||
x.Status == CourseEnrollmentStatus.Enrolled)
|
||||
.Select(x => x.Student!.UserId)
|
||||
var studentUserIds = await TeachingTaskRosterQuery
|
||||
.ForTask(db, adj.TeachingTaskId)
|
||||
.Select(x => x.UserId)
|
||||
.Where(uid => uid != null)
|
||||
.Select(uid => uid!.Value)
|
||||
.Distinct()
|
||||
@@ -278,81 +438,19 @@ public sealed class CourseAdjustmentsController(
|
||||
switch (adj.Type)
|
||||
{
|
||||
case CourseAdjustmentType.Reschedule:
|
||||
// Update existing schedule entries for this teaching task
|
||||
if (adj.DayOfWeek.HasValue && adj.StartPeriod.HasValue)
|
||||
{
|
||||
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;
|
||||
}
|
||||
}
|
||||
var rescheduleSource = await LoadSourceEntryAsync(adj, ct);
|
||||
ExcludeWeek(rescheduleSource, adj.SourceWeek!.Value);
|
||||
db.ScheduleEntries.Add(CreateTargetEntry(adj, rescheduleSource.SchedulePlanId));
|
||||
break;
|
||||
|
||||
case CourseAdjustmentType.Cancel:
|
||||
// Cancel: remove schedule entries for the specified week
|
||||
if (adj.CancelWeek.HasValue)
|
||||
{
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
var cancelSource = await LoadSourceEntryAsync(adj, ct);
|
||||
ExcludeWeek(cancelSource, adj.SourceWeek!.Value);
|
||||
break;
|
||||
|
||||
case CourseAdjustmentType.Makeup:
|
||||
// Makeup: add a temp schedule entry for the makeup date
|
||||
if (adj.TargetDate.HasValue && adj.DayOfWeek.HasValue &&
|
||||
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})"
|
||||
});
|
||||
}
|
||||
}
|
||||
var makeupSource = await LoadSourceEntryAsync(adj, ct);
|
||||
db.ScheduleEntries.Add(CreateTargetEntry(adj, makeupSource.SchedulePlanId));
|
||||
break;
|
||||
|
||||
case CourseAdjustmentType.Substitute:
|
||||
@@ -442,15 +540,13 @@ public sealed class CourseAdjustmentsController(
|
||||
{
|
||||
case CourseAdjustmentType.Reschedule:
|
||||
case CourseAdjustmentType.Makeup:
|
||||
if (!request.DayOfWeek.HasValue || request.DayOfWeek is < 1 or > 7)
|
||||
return ValidationProblem("请选择有效的上课日。");
|
||||
if (!request.TargetDate.HasValue)
|
||||
return ValidationProblem(
|
||||
request.Type == CourseAdjustmentType.Reschedule
|
||||
? "请选择调课后的日期。"
|
||||
: "请选择补课日期。");
|
||||
if (!request.StartPeriod.HasValue || request.StartPeriod < 1)
|
||||
return ValidationProblem("请选择起始节次。");
|
||||
if (!request.PeriodCount.HasValue || request.PeriodCount < 1)
|
||||
return ValidationProblem("请选择持续节数。");
|
||||
if (request.Type == CourseAdjustmentType.Makeup &&
|
||||
!request.TargetDate.HasValue)
|
||||
return ValidationProblem("补课必须指定日期。");
|
||||
break;
|
||||
case CourseAdjustmentType.Substitute:
|
||||
if (!request.SubstituteTeacherId.HasValue)
|
||||
@@ -461,14 +557,226 @@ public sealed class CourseAdjustmentsController(
|
||||
return ValidationProblem("代课教师不存在或已离职。");
|
||||
break;
|
||||
case CourseAdjustmentType.Cancel:
|
||||
if (!request.CancelWeek.HasValue && !request.CancelDate.HasValue)
|
||||
return ValidationProblem("停课需指定周次或日期。");
|
||||
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;
|
||||
}
|
||||
|
||||
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()
|
||||
{
|
||||
var source = db.TeachingTasks.AsQueryable();
|
||||
@@ -483,7 +791,7 @@ public sealed class CourseAdjustmentsController(
|
||||
return source.Where(_ => false);
|
||||
}
|
||||
|
||||
private static System.Linq.Expressions.Expression<
|
||||
private System.Linq.Expressions.Expression<
|
||||
Func<CourseAdjustment, object>> AdjustmentProjection() => x => new
|
||||
{
|
||||
x.Id,
|
||||
@@ -500,6 +808,17 @@ public sealed class CourseAdjustmentsController(
|
||||
x.Status,
|
||||
x.Reason,
|
||||
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.DayOfWeek,
|
||||
x.StartPeriod,
|
||||
@@ -515,6 +834,12 @@ public sealed class CourseAdjustmentsController(
|
||||
x.CreatedAt
|
||||
};
|
||||
|
||||
private sealed record SourceOccurrence(
|
||||
ScheduleEntry Entry,
|
||||
AcademicTerm Term,
|
||||
int Week,
|
||||
DateOnly Date);
|
||||
|
||||
private ActionResult ConflictProblem(string detail) =>
|
||||
Conflict(new ProblemDetails
|
||||
{
|
||||
@@ -538,7 +863,9 @@ public sealed record CourseAdjustmentRequest(
|
||||
Guid? ClassroomId,
|
||||
Guid? SubstituteTeacherId,
|
||||
int? CancelWeek,
|
||||
DateOnly? CancelDate);
|
||||
DateOnly? CancelDate,
|
||||
Guid? SourceScheduleEntryId = null,
|
||||
int? SourceWeek = null);
|
||||
|
||||
public sealed record RejectionRequest(
|
||||
[MaxLength(500)] string? Comment);
|
||||
|
||||
Reference in New Issue
Block a user