Round 20: Student Portal, District Management, userId field, styling enhancements, bug fixes
- Added userId field to Student model with User relation - Created Student Portal view with 6 tabs (competencies, goals, achievements, schedule, homework, feedback) - Created StudentGoal model + CRUD API with role-based access - Created District Management view with CRUD, analytics, school assignment - Enhanced auth page: animated gradient, floating particles, glassmorphism, staggered entrance - Enhanced dashboard: time-based greeting, animated stat cards, class overview, school branding - Enhanced student portal: hero section, circular progress, streak counter, celebration particles - Fixed student-goals API 403 for STUDENT role (name-matching fallback for legacy data) - Added userId filter to students API with OR fallback - Added nav.student-portal i18n key - 100+ new i18n keys in DE and EN - All lint checks pass
This commit is contained in:
1 parent
9680cc9244
commit
04ae7aa80c
23 files changed
+4624
-2868
No files matched your search
@@ -0,0 +1,175 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { db } from '@/lib/db';
|
||||
import { getSession } from '@/lib/auth';
|
||||
|
||||
/**
|
||||
* Check if a student belongs to the current user (STUDENT role).
|
||||
* Uses userId (direct link) with name-matching fallback for legacy data.
|
||||
*/
|
||||
function isOwnStudent(
|
||||
student: { userId: string | null; firstName: string; lastName: string; schoolId: string | null },
|
||||
user: { id: string; firstName: string; lastName: string; schoolId: string | null }
|
||||
): boolean {
|
||||
if (student.userId === user.id) return true;
|
||||
// Fallback: match by name + school for legacy records without userId
|
||||
if (
|
||||
student.userId === null &&
|
||||
student.firstName === user.firstName &&
|
||||
student.lastName === user.lastName &&
|
||||
student.schoolId === user.schoolId
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
export async function GET(
|
||||
_request: Request,
|
||||
{ params }: { params: Promise<{ id: string }> }
|
||||
) {
|
||||
try {
|
||||
const session = await getSession();
|
||||
if (!session) {
|
||||
return NextResponse.json({ error: 'Not authenticated' }, { status: 401 });
|
||||
}
|
||||
|
||||
const { id } = await params;
|
||||
const goal = await db.studentGoal.findUnique({
|
||||
where: { id },
|
||||
include: {
|
||||
student: {
|
||||
select: { id: true, firstName: true, lastName: true, schoolId: true, userId: true },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!goal) {
|
||||
return NextResponse.json({ error: 'Goal not found' }, { status: 404 });
|
||||
}
|
||||
|
||||
// Verify access
|
||||
if (session.user?.role === 'STUDENT') {
|
||||
if (!isOwnStudent(goal.student, session.user)) {
|
||||
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
||||
}
|
||||
} else if (session.user?.role !== 'SUPER_ADMIN') {
|
||||
if (goal.student.schoolId !== session.user?.schoolId) {
|
||||
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
||||
}
|
||||
}
|
||||
|
||||
return NextResponse.json(goal);
|
||||
} catch (error) {
|
||||
console.error('StudentGoal GET by ID error:', error);
|
||||
return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
export async function PUT(
|
||||
request: Request,
|
||||
{ params }: { params: Promise<{ id: string }> }
|
||||
) {
|
||||
try {
|
||||
const session = await getSession();
|
||||
if (!session) {
|
||||
return NextResponse.json({ error: 'Not authenticated' }, { status: 401 });
|
||||
}
|
||||
|
||||
const { id } = await params;
|
||||
const body = await request.json();
|
||||
const { title, description, targetDate, progress, status } = body;
|
||||
|
||||
const existingGoal = await db.studentGoal.findUnique({
|
||||
where: { id },
|
||||
include: {
|
||||
student: {
|
||||
select: { id: true, firstName: true, lastName: true, schoolId: true, userId: true },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!existingGoal) {
|
||||
return NextResponse.json({ error: 'Goal not found' }, { status: 404 });
|
||||
}
|
||||
|
||||
// Verify access
|
||||
if (session.user?.role === 'STUDENT') {
|
||||
if (!isOwnStudent(existingGoal.student, session.user)) {
|
||||
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
||||
}
|
||||
} else if (session.user?.role !== 'SUPER_ADMIN') {
|
||||
if (existingGoal.student.schoolId !== session.user?.schoolId) {
|
||||
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
||||
}
|
||||
}
|
||||
|
||||
const updateData: Record<string, unknown> = {};
|
||||
if (title !== undefined) updateData.title = title;
|
||||
if (description !== undefined) updateData.description = description;
|
||||
if (targetDate !== undefined) updateData.targetDate = targetDate ? new Date(targetDate) : null;
|
||||
if (progress !== undefined) updateData.progress = Math.min(100, Math.max(0, progress));
|
||||
if (status !== undefined) updateData.status = status;
|
||||
|
||||
// Auto-complete if progress reaches 100
|
||||
if (progress !== undefined && progress >= 100) {
|
||||
updateData.status = 'completed';
|
||||
updateData.progress = 100;
|
||||
}
|
||||
|
||||
const goal = await db.studentGoal.update({
|
||||
where: { id },
|
||||
data: updateData,
|
||||
});
|
||||
|
||||
return NextResponse.json(goal);
|
||||
} catch (error) {
|
||||
console.error('StudentGoal PUT error:', error);
|
||||
return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
export async function DELETE(
|
||||
_request: Request,
|
||||
{ params }: { params: Promise<{ id: string }> }
|
||||
) {
|
||||
try {
|
||||
const session = await getSession();
|
||||
if (!session) {
|
||||
return NextResponse.json({ error: 'Not authenticated' }, { status: 401 });
|
||||
}
|
||||
|
||||
const { id } = await params;
|
||||
const existingGoal = await db.studentGoal.findUnique({
|
||||
where: { id },
|
||||
include: {
|
||||
student: {
|
||||
select: { id: true, firstName: true, lastName: true, schoolId: true, userId: true },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!existingGoal) {
|
||||
return NextResponse.json({ error: 'Goal not found' }, { status: 404 });
|
||||
}
|
||||
|
||||
// Verify access
|
||||
if (session.user?.role === 'STUDENT') {
|
||||
if (!isOwnStudent(existingGoal.student, session.user)) {
|
||||
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
||||
}
|
||||
} else if (session.user?.role !== 'SUPER_ADMIN') {
|
||||
if (existingGoal.student.schoolId !== session.user?.schoolId) {
|
||||
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
||||
}
|
||||
}
|
||||
|
||||
await db.studentGoal.delete({
|
||||
where: { id },
|
||||
});
|
||||
|
||||
return NextResponse.json({ success: true });
|
||||
} catch (error) {
|
||||
console.error('StudentGoal DELETE error:', error);
|
||||
return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user