Skip to content

feat(teacher): add teacher acknowledgement dashboard with accept/decline flow#255

Open
guangshinhaha wants to merge 2 commits into
mainfrom
feat/teacher-acknowledgement-dashboard
Open

feat(teacher): add teacher acknowledgement dashboard with accept/decline flow#255
guangshinhaha wants to merge 2 commits into
mainfrom
feat/teacher-acknowledgement-dashboard

Conversation

@guangshinhaha
Copy link
Copy Markdown
Collaborator

Summary

  • New teacher-facing dashboard at /{slug}/teacher where teachers view their relief assignments and accept or decline each one
  • Decline auto-unassigns the period (shows as uncovered on KP dashboard) and creates in-app notifications for school admins via a new notification bell
  • Copy summary link: both the all-staff and per-teacher WhatsApp summaries now include a link to the teacher dashboard
  • OAuth next param: clicking the link from WhatsApp redirects through Google login and back to the teacher dashboard
  • Schema: declinedAt/declineReason on ReliefAssignment, new Notification model

Changes

  • Schema: add declinedAt, declineReason to ReliefAssignment; add Notification model with user/school relations
  • New pages: /{slug}/teacher with sidebar tabs (My Relief, My Absences, Report Absence) and mobile bottom tab bar
  • New APIs: GET/POST /api/teacher-dashboard, GET /api/teacher-dashboard/absences, GET/POST /api/in-app-notifications
  • Auth flow: thread next query param through Google OAuth initiation → callback redirect; TEACHER default redirect → /{slug}/teacher
  • Middleware: protect /{slug}/teacher routes with demo cookie seeding
  • KP dashboard: notification bell in DesktopTopBar with 30s polling; declined assignments render as uncovered with "Declined by {name}" annotation
  • Copy summary: append teacher dashboard URL to generateReliefSummary() and generatePerTeacherSummaries()
  • Seed data: guaranteed demo relief assignments for Demo Admin + David Chen teacher dashboard demo
  • Tests: fix Google OAuth test to pass Request arg to updated handler

Test plan

  • Run npx prisma migrate dev --name add_decline_and_notifications after checkout
  • Visit /demo/teacher — auto-login, see assignment cards for today
  • Accept an assignment → card shows green "Accepted" badge
  • Decline an assignment with reason → card shows "Declined", period shows uncovered on KP dashboard
  • Check notification bell on KP dashboard shows decline notification
  • Copy summary from KP dashboard → verify teacher dashboard link is appended
  • Open teacher dashboard link while logged out → login → redirects back to teacher dashboard
  • npm run build passes
  • npm test passes (516 tests)

🤖 Generated with Claude Code

guangshinhaha and others added 2 commits May 25, 2026 11:56
…ine flow

Teachers get a new dashboard at /{slug}/teacher where they can view their
relief assignments for the day and accept or decline each one. Declining
auto-unassigns the period (shows as uncovered on KP dashboard) and notifies
school admins via an in-app notification bell.

- Schema: add declinedAt/declineReason to ReliefAssignment, new Notification model
- New routes: /{slug}/teacher with sidebar tabs (My Relief, My Absences, Report)
- APIs: GET/POST /api/teacher-dashboard, /api/in-app-notifications
- Auth: thread OAuth `next` param so WhatsApp link → login → teacher dashboard
- Copy summary: append teacher dashboard link to both full + per-teacher summaries
- KP dashboard: notification bell with polling, declined periods show "Declined by"
- Seed: guaranteed demo relief assignments for teacher dashboard demo
- Middleware: protect /{slug}/teacher routes with demo cookie support

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
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 3:58am

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 new teacher-facing dashboard and acknowledgement (accept/decline) workflow for relief assignments, including admin-facing in-app notifications and WhatsApp summary links that deep-link through OAuth back to the teacher dashboard.

Changes:

  • Introduces /{slug}/teacher UI (desktop sidebar + mobile header/tab bar) backed by new teacher-dashboard APIs.
  • Adds decline tracking (declinedAt, declineReason) on ReliefAssignment and surfaces declines as “uncovered” on the KP dashboard.
  • Adds a Notification model + KP notification bell with polling, and threads a safe next param through Google OAuth.

Reviewed changes

Copilot reviewed 27 out of 27 changed files in this pull request and generated 9 comments.

Show a summary per file
File Description
tests/api/helpers.ts Truncation order updated to include Notification for DB-reset in API tests.
tests/api/google-oauth.test.ts Updates OAuth route tests to pass a Request into the handler.
src/middleware.ts Protects /{slug}/teacher routes and adds next redirect to login.
src/lib/types/dashboard.ts Extends dashboard slot type with decline metadata.
src/lib/generateReliefSummary.ts Appends optional teacher-dashboard link to generated summaries.
src/app/login/page.tsx Threads next into the “Sign in with Google” link (sanitized).
src/app/api/teacher-dashboard/route.ts New API to fetch a teacher’s relief assignments for a date.
src/app/api/teacher-dashboard/acknowledge/route.ts New accept/decline endpoint; decline creates admin notifications.
src/app/api/teacher-dashboard/absences/route.ts New API to list upcoming/past absence reports for the teacher.
src/app/api/in-app-notifications/route.ts New API to fetch latest notifications + unread count.
src/app/api/in-app-notifications/read/route.ts New API to mark notification IDs as read.
src/app/api/dashboard/route.ts Adds decline fields and treats declined assignments as uncovered.
src/app/api/auth/google/route.ts Stores a safe next path in a short-lived cookie during OAuth init.
src/app/api/auth/google/callback/route.ts Redirects to the stored next path after OAuth; TEACHER default → /{slug}/teacher.
src/app/[slug]/teacher/TeacherSidebar.tsx Desktop nav + logout for teacher dashboard.
src/app/[slug]/teacher/TeacherMobileHeader.tsx Mobile header + logout for teacher dashboard.
src/app/[slug]/teacher/TeacherDashboardContent.tsx Tab router between “My Relief” and “My Absences”.
src/app/[slug]/teacher/TeacherBottomTabBar.tsx Mobile bottom tab bar navigation for teacher dashboard.
src/app/[slug]/teacher/page.tsx Teacher dashboard page wrapper with suspense loading UI.
src/app/[slug]/teacher/MyReliefView.tsx Teacher assignment cards with accept/decline UI and date navigation.
src/app/[slug]/teacher/MyAbsencesView.tsx Read-only listing of the teacher’s absence reports.
src/app/[slug]/teacher/layout.tsx Shared teacher dashboard layout (sidebar/mobile header/tab bar).
src/app/[slug]/dashboard/ReliefOverview.tsx Copies summary including teacher-dashboard link.
src/app/[slug]/dashboard/DesktopTopBar.tsx Adds notification bell + panel with polling and “mark all read”.
src/app/[slug]/dashboard/AbsentTeachersTab.tsx Shows “Declined by {name}” annotation in the absent-teacher slot list.
prisma/seed.ts Ensures demo users see teacher-dashboard assignments and creates demo data.
prisma/schema.prisma Adds decline fields to ReliefAssignment and introduces Notification model.
Comments suppressed due to low confidence (1)

prisma/schema.prisma:435

  • This PR updates schema.prisma (new Notification model + new ReliefAssignment fields) but there’s no corresponding new Prisma migration directory committed. Per prisma/migrations/README.md, schema changes should be captured as a new migration so npx prisma migrate deploy on a fresh DB matches schema.prisma. Please add and commit a migration (e.g. npx prisma migrate dev --name add_decline_and_notifications).
model ReliefAssignment {
  id               String    @id @default(cuid())
  absenceReportId  String    @map("sickReportId")
  timetableEntryId String
  reliefTeacherId  String
  date             DateTime
  ackToken         String?   @unique // signed token for one-tap acknowledge
  acknowledgedAt   DateTime?
  declinedAt       DateTime?
  declineReason    String?
  // Phase 15 / D2 — who created this assignment? Null when system-created
  // (seed, future cron paths). For multi-KP schools (Zhenghua), the
  // dashboard reads this to show "Assigned by ___" so concurrent KPs can
  // tell who took which row.
  assignedByUserId String?
  schoolId         String
  createdAt        DateTime  @default(now())

  school           School            @relation(fields: [schoolId], references: [id], onDelete: Cascade)
  absenceReport    AbsenceReport     @relation(fields: [absenceReportId], references: [id], onDelete: Cascade)
  timetableEntry   TimetableEntry    @relation(fields: [timetableEntryId], references: [id], onDelete: Cascade)
  reliefTeacher    Teacher           @relation(fields: [reliefTeacherId], references: [id], onDelete: Cascade)
  assignedBy       User?             @relation("ReliefAssignedBy", fields: [assignedByUserId], references: [id], onDelete: SetNull)
  outboundMessages OutboundMessage[]

  @@unique([timetableEntryId, date])
  @@index([date])
  @@index([reliefTeacherId, date])
  @@index([absenceReportId])
  @@index([schoolId])
  @@index([schoolId, date, reliefTeacherId])
  @@index([assignedByUserId])
}

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

Comment thread src/middleware.ts
Comment on lines +77 to +78
return NextResponse.redirect(
new URL(`/login?next=${encodeURIComponent(pathname)}`, request.url)
Comment on lines +24 to +32
const dateParam = request.nextUrl.searchParams.get("date");
const dateStr =
dateParam && /^\d{4}-\d{2}-\d{2}$/.test(dateParam)
? dateParam
: todayInSGT();

const dateUTC = new Date(dateStr + "T00:00:00.000Z");

const assignments = await prisma.reliefAssignment.findMany({
Comment on lines +23 to +30
const todayUtc = new Date(
Date.UTC(
new Date().getUTCFullYear(),
new Date().getUTCMonth(),
new Date().getUTCDate()
)
);

Comment on lines +11 to +18
const notifications = await prisma.notification.findMany({
where: { userId: session.user.id },
orderBy: { createdAt: "desc" },
take: 50,
});

const unreadCount = notifications.filter((n) => !n.read).length;

Comment on lines +66 to +77
// Decline: mark the assignment and notify school admins
const trimmedReason = reason?.trim().slice(0, 200) || null;
const period = assignment.timetableEntry.period;

const updated = await prisma.reliefAssignment.update({
where: { id: assignmentId },
data: {
declinedAt: new Date(),
declineReason: trimmedReason,
acknowledgedAt: null,
},
});
</div>
<div className="rounded-lg border border-border bg-card p-3 text-center">
<p className="text-2xl font-bold text-foreground">
{pending + declined}
Comment thread prisma/seed.ts
Comment on lines +733 to +743
for (const target of demoReliefTargets) {
let count = 0;
for (const absence of todayAbsences) {
if (count >= 3) break;
const assignedEntryIds = new Set(absence.reliefAssignments.map((ra) => ra.timetableEntryId));
const uncoveredEntries = createdEntries.filter(
(e) =>
e.teacherId === absence.teacherId &&
e.dayOfWeek === today.getUTCDay() &&
!assignedEntryIds.has(e.id),
);
Comment on lines +5 to +33
export async function POST(request: NextRequest) {
const session = await getSession();
if (!session) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}

const teacher = await prisma.teacher.findUnique({
where: { userId: session.user.id },
select: { id: true, name: true, schoolId: true },
});

if (!teacher) {
return NextResponse.json(
{ error: "No teacher record linked to this account" },
{ status: 403 }
);
}

const body = await request.json();
const { assignmentId, action, reason } = body as {
assignmentId: string;
action: "acknowledge" | "decline";
reason?: string;
};

if (!assignmentId || !["acknowledge", "decline"].includes(action)) {
return NextResponse.json({ error: "Invalid request" }, { status: 400 });
}

Comment on lines +11 to +18
const notifications = await prisma.notification.findMany({
where: { userId: session.user.id },
orderBy: { createdAt: "desc" },
take: 50,
});

const unreadCount = notifications.filter((n) => !n.read).length;

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