diff --git a/app/api/announcements/[id]/route.ts b/app/api/announcements/[id]/route.ts index 6c32f47..96e89c5 100644 --- a/app/api/announcements/[id]/route.ts +++ b/app/api/announcements/[id]/route.ts @@ -4,7 +4,7 @@ import mongoose from 'mongoose' import { connectDB } from '@/lib/mongodb' import { Announcement } from '@/models/Announcement' -const ALLOWED_FIELDS = ['title', 'content', 'body', 'audience', 'category', 'pinned', 'expiresAt'] +const ALLOWED_FIELDS = ['title', 'content', 'audience', 'category', 'pinned'] export async function PUT(req: NextRequest, ctx: { params: Promise<{ id: string }> }) { const { userId } = await auth() @@ -27,16 +27,22 @@ export async function PUT(req: NextRequest, ctx: { params: Promise<{ id: string return NextResponse.json({ error: 'Invalid JSON request body' }, { status: 400 }) } - // Sanitize: only allow whitelisted fields + // Sanitize: only allow whitelisted fields (map legacy `body` → `content`) const sanitizedBody: Record = {} for (const key of ALLOWED_FIELDS) { if (key in body) { sanitizedBody[key] = body[key] } } + if ( + typeof body.body === 'string' && + sanitizedBody.content === undefined + ) { + sanitizedBody.content = body.body + } const announcement = await Announcement.findOneAndUpdate( - { _id: id }, + { _id: id, teacherId: userId }, { $set: sanitizedBody }, { new: true, runValidators: true, context: 'query' } ) @@ -63,7 +69,7 @@ export async function DELETE(_req: NextRequest, ctx: { params: Promise<{ id: str } await connectDB() - const deleted = await Announcement.findOneAndDelete({ _id: id }) + const deleted = await Announcement.findOneAndDelete({ _id: id, teacherId: userId }) if (!deleted) { return NextResponse.json({ error: 'Not found' }, { status: 404 }) diff --git a/app/api/assignments/[id]/route.ts b/app/api/assignments/[id]/route.ts index 21fca1c..b041302 100644 --- a/app/api/assignments/[id]/route.ts +++ b/app/api/assignments/[id]/route.ts @@ -36,7 +36,7 @@ export async function PUT(req: NextRequest, ctx: { params: Promise<{ id: string } const assignment = await Assignment.findOneAndUpdate( - { _id: id }, + { _id: id, teacherId: userId }, sanitizedBody, { new: true } ) @@ -63,7 +63,7 @@ export async function DELETE(_req: NextRequest, ctx: { params: Promise<{ id: str } await connectDB() - const deleted = await Assignment.findOneAndDelete({ _id: id }) + const deleted = await Assignment.findOneAndDelete({ _id: id, teacherId: userId }) if (!deleted) { return NextResponse.json({ error: 'Not found' }, { status: 404 }) diff --git a/app/api/assignments/route.ts b/app/api/assignments/route.ts index 19021d8..1d88b7e 100644 --- a/app/api/assignments/route.ts +++ b/app/api/assignments/route.ts @@ -52,7 +52,7 @@ export async function GET(req: NextRequest) { if (error instanceof Error) { console.error('GET /api/assignments error:', error.message) } - return NextResponse.json({ error: error instanceof Error ? error.stack : 'Internal server error' }, { status: 500 }) + return NextResponse.json({ error: 'Internal server error' }, { status: 500 }) } } diff --git a/app/api/attendance/route.ts b/app/api/attendance/route.ts index 14b6c4d..1aedf8f 100644 --- a/app/api/attendance/route.ts +++ b/app/api/attendance/route.ts @@ -30,13 +30,27 @@ export async function GET(req: NextRequest) { const query: Record = { teacherId: userId }; - // Helper to validate and normalize date strings to YYYY-MM-DD format + // Normalize calendar dates to YYYY-MM-DD without UTC day-shift (date-only input) const normalizeDate = (dateStr: string): string | null => { + const trimmed = dateStr.trim(); + const ymd = /^(\d{4})-(\d{2})-(\d{2})$/.exec(trimmed); + if (ymd) { + const y = Number(ymd[1]); + const m = Number(ymd[2]); + const day = Number(ymd[3]); + const local = new Date(y, m - 1, day); + if ( + local.getFullYear() === y && + local.getMonth() === m - 1 && + local.getDate() === day + ) { + return `${ymd[1]}-${ymd[2]}-${ymd[3]}`; + } + return null; + } try { - // Try to parse as ISO date (YYYY-MM-DD or full ISO 8601) - const d = new Date(dateStr); + const d = new Date(trimmed); if (isNaN(d.getTime())) return null; - // Return in YYYY-MM-DD format for MongoDB string comparison return d.toISOString().split("T")[0]; } catch { return null; diff --git a/app/api/grades/[id]/route.ts b/app/api/grades/[id]/route.ts index 0141f63..8a9abf0 100644 --- a/app/api/grades/[id]/route.ts +++ b/app/api/grades/[id]/route.ts @@ -4,7 +4,18 @@ import mongoose from 'mongoose' import { connectDB } from '@/lib/mongodb' import { Grade } from '@/models/Grade' -const ALLOWED_UPDATE_FIELDS = ['marks', 'maxMarks', 'grade'] +const ALLOWED_UPDATE_FIELDS = ['marks', 'maxMarks'] + +function calcGrade(marks: number, max: number): string { + const pct = (marks / max) * 100 + if (pct > 90) return 'A+' + if (pct >= 80) return 'A' + if (pct >= 70) return 'B+' + if (pct >= 60) return 'B' + if (pct >= 50) return 'C' + if (pct >= 40) return 'D' + return 'F' +} export async function PUT(req: NextRequest, ctx: { params: Promise<{ id: string }> }) { const { userId } = await auth() @@ -34,10 +45,32 @@ export async function PUT(req: NextRequest, ctx: { params: Promise<{ id: string } await connectDB() + const existing = await Grade.findOne({ _id: id, teacherId: userId }).lean() + if (!existing) { + return NextResponse.json({ error: 'Not found' }, { status: 404 }) + } + + const nextMarks = + typeof sanitizedBody.marks === 'number' ? sanitizedBody.marks : existing.marks + const nextMax = + typeof sanitizedBody.maxMarks === 'number' + ? sanitizedBody.maxMarks + : existing.maxMarks + + if (nextMarks > nextMax) { + return NextResponse.json( + { error: 'marks must be less than or equal to maxMarks' }, + { status: 400 }, + ) + } + + const updatePayload: Record = { ...sanitizedBody } + updatePayload.grade = calcGrade(nextMarks, nextMax) + const grade = await Grade.findOneAndUpdate( - { _id: id }, - sanitizedBody, - { new: true } + { _id: id, teacherId: userId }, + { $set: updatePayload }, + { new: true }, ) if (!grade) return NextResponse.json({ error: 'Not found' }, { status: 404 }) return NextResponse.json(grade) @@ -55,8 +88,13 @@ export async function DELETE(_req: NextRequest, ctx: { params: Promise<{ id: str try { const { id } = await ctx.params + + if (!mongoose.Types.ObjectId.isValid(id)) { + return NextResponse.json({ error: 'Invalid id' }, { status: 400 }) + } + await connectDB() - const deleted = await Grade.findOneAndDelete({ _id: id }) + const deleted = await Grade.findOneAndDelete({ _id: id, teacherId: userId }) if (!deleted) { return NextResponse.json({ error: 'Grade not found' }, { status: 404 }) diff --git a/app/api/grades/route.ts b/app/api/grades/route.ts index b9da63d..2aa2142 100644 --- a/app/api/grades/route.ts +++ b/app/api/grades/route.ts @@ -48,7 +48,7 @@ export async function GET(req: NextRequest) { return NextResponse.json(grades) } catch (error) { console.error('GET /api/grades error:', error instanceof Error ? error.message : error) - return NextResponse.json({ error: error instanceof Error ? error.stack : 'Internal server error' }, { status: 500 }) + return NextResponse.json({ error: 'Internal server error' }, { status: 500 }) } } @@ -70,10 +70,10 @@ export async function POST(req: NextRequest) { if (!parsed.success) return NextResponse.json({ error: parsed.error.flatten() }, { status: 400 }) const data = parsed.data - const max = data.maxMarks! + const max = data.maxMarks ?? 100 const term = data.term ?? 'Term 1' - const grade = Grade.findOneAndUpdate( + const grade = await Grade.findOneAndUpdate( { teacherId: userId, studentId: data.studentId, subject: data.subject, term }, { $set: { ...data, term, teacherId: userId, grade: calcGrade(data.marks, max) } }, { upsert: true, new: true } @@ -83,6 +83,6 @@ export async function POST(req: NextRequest) { if (error instanceof Error) { console.error('POST /api/grades error:', error.message) } - return NextResponse.json({ error: error instanceof Error ? error.stack : 'Internal server error' }, { status: 500 }) + return NextResponse.json({ error: 'Internal server error' }, { status: 500 }) } } diff --git a/app/api/profile/route.ts b/app/api/profile/route.ts index a3d98bf..0908513 100644 --- a/app/api/profile/route.ts +++ b/app/api/profile/route.ts @@ -3,15 +3,8 @@ import { NextRequest, NextResponse } from 'next/server' import { connectDB } from '@/lib/mongodb' import { Teacher } from '@/models/Teacher' -export async function GET(req: NextRequest) { - const { searchParams } = new URL(req.url) - const queryUserId = searchParams.get('userId') - - let userId: string | null = queryUserId - if (!userId) { - const session = await auth() - userId = session.userId - } +export async function GET(_req: NextRequest) { + const { userId } = await auth() if (!userId) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) try { diff --git a/app/api/students/[id]/route.ts b/app/api/students/[id]/route.ts index 2eaaf93..85d4294 100644 --- a/app/api/students/[id]/route.ts +++ b/app/api/students/[id]/route.ts @@ -4,7 +4,7 @@ import mongoose from 'mongoose' import { connectDB } from '@/lib/mongodb' import { Student } from '@/models/Student' -const ALLOWED_UPDATE_FIELDS = ['name', 'email', 'grade', 'rollNo', 'class', 'phone', 'address', 'parentName', 'parentPhone'] +const ALLOWED_UPDATE_FIELDS = ['name', 'email', 'rollNo', 'class', 'phone', 'address', 'parentName', 'parentPhone'] export async function PUT(req: NextRequest, ctx: { params: Promise<{ id: string }> }) { const { userId } = await auth() @@ -35,7 +35,7 @@ export async function PUT(req: NextRequest, ctx: { params: Promise<{ id: string await connectDB() const student = await Student.findOneAndUpdate( - { _id: id }, + { _id: id, teacherId: userId }, sanitizedBody, { new: true } ) @@ -65,7 +65,7 @@ export async function DELETE(_req: NextRequest, ctx: { params: Promise<{ id: str } await connectDB() - const deleted = await Student.findOneAndDelete({ _id: id }) + const deleted = await Student.findOneAndDelete({ _id: id, teacherId: userId }) if (!deleted) { return NextResponse.json({ error: 'Student not found' }, { status: 404 }) diff --git a/app/api/students/route.ts b/app/api/students/route.ts index 8f3dcc2..bed5f30 100644 --- a/app/api/students/route.ts +++ b/app/api/students/route.ts @@ -92,9 +92,12 @@ export async function POST(req: NextRequest) { return NextResponse.json({ error: 'Malformed JSON' }, { status: 400 }) } - StudentSchema.safeParse(body) + const parsed = StudentSchema.safeParse(body) + if (!parsed.success) { + return NextResponse.json({ error: parsed.error.flatten() }, { status: 400 }) + } - const student = await Student.create({ ...(body as Record), teacherId: userId }) + const student = await Student.create({ ...parsed.data, teacherId: userId }) return NextResponse.json(student, { status: 201 }) } catch (error) { if (error instanceof Error) { diff --git a/app/dashboard/OverviewClient.tsx b/app/dashboard/OverviewClient.tsx index 169795e..f1c19bd 100644 --- a/app/dashboard/OverviewClient.tsx +++ b/app/dashboard/OverviewClient.tsx @@ -200,14 +200,52 @@ export function OverviewClient() { fetch("/api/announcements?limit=5"), ]); - const [students, assignmentsData, attendance, grades, announcements] = - await Promise.all([ - studentsRes.json(), - assignmentsRes.json(), - attendanceRes.json(), - gradesRes.json(), - announcementsRes.json(), - ]); + const students = await studentsRes.json(); + if (!studentsRes.ok) { + throw new Error( + typeof students.error === "string" + ? students.error + : `Students failed (${studentsRes.status})`, + ); + } + const assignmentsData = await assignmentsRes.json(); + if (!assignmentsRes.ok) { + throw new Error( + typeof assignmentsData.error === "string" + ? assignmentsData.error + : `Assignments failed (${assignmentsRes.status})`, + ); + } + const attendanceRaw = await attendanceRes.json(); + if (!attendanceRes.ok) { + throw new Error( + typeof attendanceRaw.error === "string" + ? attendanceRaw.error + : `Attendance failed (${attendanceRes.status})`, + ); + } + const gradesRaw = await gradesRes.json(); + if (!gradesRes.ok) { + throw new Error( + typeof gradesRaw.error === "string" + ? gradesRaw.error + : `Grades failed (${gradesRes.status})`, + ); + } + const announcementsRaw = await announcementsRes.json(); + if (!announcementsRes.ok) { + throw new Error( + typeof announcementsRaw.error === "string" + ? announcementsRaw.error + : `Announcements failed (${announcementsRes.status})`, + ); + } + + const attendance = Array.isArray(attendanceRaw) ? attendanceRaw : []; + const grades = Array.isArray(gradesRaw) ? gradesRaw : []; + const announcements = Array.isArray(announcementsRaw) + ? announcementsRaw + : []; const assignments = assignmentsData.assignments ?? assignmentsData; @@ -254,7 +292,7 @@ export function OverviewClient() { "B+": 8, B: 7, C: 6, - D: 4, + D: 5, F: 0, }; const termMap: Record = {}; @@ -316,7 +354,7 @@ export function OverviewClient() { .slice(0, 5); setStats({ - totalStudents: students.students?.length ?? 0, + totalStudents: students.total ?? students.students?.length ?? 0, totalAssignments: Array.isArray(assignments) ? assignments.length : (assignments.length ?? 0), diff --git a/app/dashboard/assignments/AssignmentsClient.tsx b/app/dashboard/assignments/AssignmentsClient.tsx index e0e7f13..dfc6623 100644 --- a/app/dashboard/assignments/AssignmentsClient.tsx +++ b/app/dashboard/assignments/AssignmentsClient.tsx @@ -59,11 +59,14 @@ const COLUMNS: { ]; function daysUntil(deadline: string) { - return Math.ceil((new Date(deadline).getTime() - Date.now()) / (1000 * 60 * 60 * 24)) + const t = new Date(deadline).getTime(); + if (!Number.isFinite(t)) return NaN; + return Math.ceil((t - Date.now()) / (1000 * 60 * 60 * 24)); } function DeadlineBadge({ deadline }: { deadline: string }) { const days = daysUntil(deadline); + if (!Number.isFinite(days)) return Invalid date; if (days < 0) return Overdue; if (days <= 2) return {days}d left; if (days <= 7) return {days}d left; diff --git a/app/dashboard/attendance/AttendanceClient.tsx b/app/dashboard/attendance/AttendanceClient.tsx index f97e7db..0411e0f 100644 --- a/app/dashboard/attendance/AttendanceClient.tsx +++ b/app/dashboard/attendance/AttendanceClient.tsx @@ -75,7 +75,7 @@ export function AttendanceClient() { } try { const res = await fetch( - `/api/students?search=${encodeURIComponent(selectedClass)}&limit=100`, + `/api/students?class=${encodeURIComponent(selectedClass)}&limit=100`, ); if (!res.ok) throw new Error(`HTTP ${res.status}`); const data = await res.json(); diff --git a/app/dashboard/students/StudentsClient.tsx b/app/dashboard/students/StudentsClient.tsx index 82a1d96..d864363 100644 --- a/app/dashboard/students/StudentsClient.tsx +++ b/app/dashboard/students/StudentsClient.tsx @@ -188,25 +188,36 @@ function StudentDrawer({ }, [attendance]) const recentGrades = useMemo(() => { + const TERM_ORDER = [ + "Term 1", + "Term 2", + "Term 3", + "Term 4", + "Semester 1", + "Semester 2", + "Semester 3", + "Semester 4", + "Semester 5", + "Semester 6", + "Semester 7", + "Semester 8", + ]; const SEASON_ORDER: Record = { Spring: 1, Summer: 2, Fall: 3, Winter: 4, }; - const sortedGrades = [...grades].sort((a, b) => { - // Parse term string (e.g., "Spring 2024") to extract season and year - const parseTermKey = (term: string): number => { - const parts = term.split(" "); - const season = parts[0] || ""; - const year = parseInt(parts[1] || "0", 10); - const seasonVal = SEASON_ORDER[season] ?? 0; - return year * 100 + seasonVal; // e.g., 202401 for Spring 2024 - }; - const aKey = parseTermKey(a.term); - const bKey = parseTermKey(b.term); - return bKey - aKey; // Descending chronological order - }); + const termRank = (term: string): number => { + const i = TERM_ORDER.indexOf(term); + if (i !== -1) return 1000 + i; + const parts = term.split(" "); + const season = parts[0] || ""; + const year = parseInt(parts[1] || "0", 10); + const seasonVal = SEASON_ORDER[season] ?? 0; + return year * 100 + seasonVal; + }; + const sortedGrades = [...grades].sort((a, b) => termRank(b.term) - termRank(a.term)); return sortedGrades.slice(0, 6); }, [grades]); @@ -548,14 +559,27 @@ export function StudentsClient() { if (debouncedSearch) params.set("search", debouncedSearch); if (classFilter !== "all") params.set("class", classFilter); const res = await fetch(`/api/students?${params}`); + if (!res.ok) { + const errBody = await res.json().catch(() => ({})); + const msg = + typeof (errBody as { error?: string }).error === "string" + ? (errBody as { error: string }).error + : `Request failed (${res.status})`; + throw new Error(msg); + } const data = await res.json(); setStudents(data.students ?? []); setTotal(data.total ?? 0); setPages(data.pages ?? 1); + } catch (e) { + toast(e instanceof Error ? e.message : "Failed to load students", "error"); + setStudents([]); + setTotal(0); + setPages(1); } finally { setLoading(false); } - }, [page, debouncedSearch, classFilter]); + }, [page, debouncedSearch, classFilter, toast]); useEffect(() => { fetchStudents(); diff --git a/models/Attendance.ts b/models/Attendance.ts index 3f05fb1..aebb1c1 100644 --- a/models/Attendance.ts +++ b/models/Attendance.ts @@ -24,6 +24,9 @@ const AttendanceSchema = new Schema( { timestamps: true } ) -AttendanceSchema.index({ studentId: 1, date: 1 }, { unique: true }) +AttendanceSchema.index( + { teacherId: 1, studentId: 1, date: 1 }, + { unique: true }, +) export const Attendance = models.Attendance ?? model('Attendance', AttendanceSchema)