feat(teacher): add teacher acknowledgement dashboard with accept/decline flow#255
Open
guangshinhaha wants to merge 2 commits into
Open
feat(teacher): add teacher acknowledgement dashboard with accept/decline flow#255guangshinhaha wants to merge 2 commits into
guangshinhaha wants to merge 2 commits into
Conversation
…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>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
There was a problem hiding this comment.
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}/teacherUI (desktop sidebar + mobile header/tab bar) backed by new teacher-dashboard APIs. - Adds decline tracking (
declinedAt,declineReason) onReliefAssignmentand surfaces declines as “uncovered” on the KP dashboard. - Adds a
Notificationmodel + KP notification bell with polling, and threads a safenextparam 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(newNotificationmodel + newReliefAssignmentfields) but there’s no corresponding new Prisma migration directory committed. Perprisma/migrations/README.md, schema changes should be captured as a new migration sonpx prisma migrate deployon a fresh DB matchesschema.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 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 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; | ||
|
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
/{slug}/teacherwhere teachers view their relief assignments and accept or decline each onenextparam: clicking the link from WhatsApp redirects through Google login and back to the teacher dashboarddeclinedAt/declineReasononReliefAssignment, newNotificationmodelChanges
declinedAt,declineReasontoReliefAssignment; addNotificationmodel with user/school relations/{slug}/teacherwith sidebar tabs (My Relief, My Absences, Report Absence) and mobile bottom tab barGET/POST /api/teacher-dashboard,GET /api/teacher-dashboard/absences,GET/POST /api/in-app-notificationsnextquery param through Google OAuth initiation → callback redirect; TEACHER default redirect →/{slug}/teacher/{slug}/teacherroutes with demo cookie seedinggenerateReliefSummary()andgeneratePerTeacherSummaries()Requestarg to updated handlerTest plan
npx prisma migrate dev --name add_decline_and_notificationsafter checkout/demo/teacher— auto-login, see assignment cards for todaynpm run buildpassesnpm testpasses (516 tests)🤖 Generated with Claude Code