Skip to content

feat(relief): cascading absence model for relief-of-relief#257

Open
guangshinhaha wants to merge 1 commit into
mainfrom
feat/cascading-absence-model
Open

feat(relief): cascading absence model for relief-of-relief#257
guangshinhaha wants to merge 1 commit into
mainfrom
feat/cascading-absence-model

Conversation

@guangshinhaha
Copy link
Copy Markdown
Collaborator

Summary

  • Adds ReliefAssignmentStatus enum (ACTIVE, CANCELLED_ASSIGNEE_ABSENT, CANCELLED_ADMIN), cancelledAt, and self-referential replacedById chain to ReliefAssignment for tracking cascading absences
  • When a relief teacher submits an absence, their active relief assignments are auto-cancelled and the periods resurface on the dashboard for re-assignment
  • All 9 query sites (dashboard, available-teachers, auto-assign, export, daily-summary, scheduling, etc.) now filter by status = ACTIVE so cancelled assignments don't appear as filled

Why

If Teacher A is assigned to cover Teacher B's Period 1, but Teacher A also falls sick, that period was previously stuck as "assigned" with no way to reassign. Now the system detects this automatically and releases the slot.

Migration

After pulling, run:

npx prisma migrate dev --name cascading_absence_status

Test plan

  • Run npx prisma migrate dev — migration applies cleanly
  • Seed (npx prisma db seed) includes a cascading absence example
  • Submit absence for a teacher who has active relief assignments → their assignments are auto-cancelled
  • Dashboard shows the previously-assigned period as unfilled again
  • Assign a new teacher to the re-surfaced period → works without unique constraint error
  • npm test passes (516/516)
  • npm run build passes

🤖 Generated with Claude Code

When a relief teacher (Teacher A) who is assigned to cover someone else
also falls sick, their active assignments are now auto-cancelled and the
periods surface back on the dashboard for re-assignment to Teacher C.

Schema: adds ReliefAssignmentStatus enum (ACTIVE, CANCELLED_ASSIGNEE_ABSENT,
CANCELLED_ADMIN), cancelledAt, and a self-referential replacedById chain
on ReliefAssignment. Replaces the @@unique(timetableEntryId, date) with
an app-level check so cancelled rows don't block new assignments.

All query sites (dashboard, available-teachers, auto-assign, export,
daily-summary, scheduling) now filter by status=ACTIVE.

Run `npx prisma migrate dev --name cascading_absence_status` after pulling.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@vercel
Copy link
Copy Markdown

vercel Bot commented May 25, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
relief-teacher-planning Ready Ready Preview, Comment May 25, 2026 6:35am

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a cascading-cancellation model for “relief-of-relief” scenarios by introducing an ACTIVE/CANCELLED_* status on ReliefAssignment and updating read paths to ignore cancelled assignments so uncovered slots can re-surface for re-assignment.

Changes:

  • Introduces ReliefAssignmentStatus, cancelledAt, and a self-referential replacement chain (replacedById) on ReliefAssignment.
  • Adds cascading cancellation when an absence is submitted for a teacher who is currently an active relief assignee.
  • Updates multiple query/read sites (dashboard, auto-assign, export, etc.) to filter ReliefAssignment rows by status: "ACTIVE".

Reviewed changes

Copilot reviewed 14 out of 14 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
src/lib/scheduling/absenceCoverage.ts Treats only ACTIVE internal relief assignments as “covered” when resolving unfilled periods.
src/lib/cascadingAbsence.ts Adds helper to cancel ACTIVE relief assignments for an absent relief teacher across a date range.
src/lib/autoAssignRules.ts Excludes cancelled assignments from workload/shift counting.
src/lib/autoAssign.ts Excludes cancelled assignments when determining which slots are already assigned.
src/app/api/relief-assignments/route.ts Updates assignment creation pre-checks to consider only ACTIVE rows.
src/app/api/export/route.ts Exports only ACTIVE assignments.
src/app/api/dashboard/route.ts Dashboard read path now includes only ACTIVE assignments.
src/app/api/dashboard/available-teachers/route.ts Availability computation ignores cancelled assignments.
src/app/api/cron/daily-summary/route.ts Daily summary email counts/listing exclude cancelled assignments.
src/app/api/auto-assign/route.ts Preview path excludes cancelled assignments when computing “already covering”.
src/app/api/absence-reports/route.ts Triggers cascading cancellation after creating an absence report.
src/app/api/[slug]/preferred-relief-assignments/route.ts Internal-assignment collision check now considers only ACTIVE assignments.
prisma/seed.ts Seeds an example cancelled relief assignment and a replacement row to demonstrate the chain.
prisma/schema.prisma Adds ReliefAssignment status/cancellation/replacement fields and replaces slot/date unique constraint with indexes.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +65 to +69
// Pre-check 2: slot already has an ACTIVE assignment?
const slotTaken = await prisma.reliefAssignment.findFirst({
where: { timetableEntryId, date, status: "ACTIVE" },
include: {
reliefTeacher: { select: { name: true } },
Comment on lines 63 to 66
prisma.reliefAssignment.findMany({
where: { schoolId, date: dateUTC, reliefTeacherId: { in: proposedTeacherIds } },
where: { schoolId, date: dateUTC, status: "ACTIVE", reliefTeacherId: { in: proposedTeacherIds } },
select: { reliefTeacherId: true, timetableEntry: { select: { periodId: true } } },
}),
Comment thread prisma/schema.prisma
outboundMessages OutboundMessage[]

@@unique([timetableEntryId, date])
@@index([timetableEntryId, date])
Comment on lines +384 to +391
const teacherRecord = await prisma.teacher.findFirst({
where: { id: resolvedTeacherId, schoolId: resolvedSchoolId! },
select: { id: true },
});
if (teacherRecord) {
const { cancelAssignmentsForAbsentRelief } = await import("@/lib/cascadingAbsence");
await cancelAssignmentsForAbsentRelief(resolvedTeacherId, start, end);
}
Comment on lines +389 to +390
const { cancelAssignmentsForAbsentRelief } = await import("@/lib/cascadingAbsence");
await cancelAssignmentsForAbsentRelief(resolvedTeacherId, start, end);
Comment on lines +1 to +21
import { prisma } from "@/lib/prisma";

/**
* When a teacher submits an absence, check if they have any ACTIVE relief
* assignments on the affected dates. If so, cancel those assignments
* (CANCELLED_ASSIGNEE_ABSENT) so the periods surface back on the dashboard
* for re-assignment.
*
* Returns the IDs of cancelled assignments (empty array if none).
*/
export async function cancelAssignmentsForAbsentRelief(
teacherId: string,
startDate: Date,
endDate: Date,
): Promise<string[]> {
const affected = await prisma.reliefAssignment.findMany({
where: {
reliefTeacherId: teacherId,
status: "ACTIVE",
date: { gte: startDate, lte: endDate },
},
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants