Add 4 files, update 15 files
This commit is contained in:
1 parent
a97794acf8
commit
2db87a1227
19 files changed
+414
-33
No files matched your search
@@ -1,6 +1,8 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { db } from '@/lib/db';
|
||||
import { getSession } from '@/lib/auth';
|
||||
import { getTeacherClassIds } from '@/lib/access-policy';
|
||||
import { isAdministrator } from '@/lib/role-access';
|
||||
import { addDays, addWeeks, addMonths, addYears, format, getDay } from 'date-fns';
|
||||
|
||||
interface RecurrencePattern {
|
||||
@@ -124,6 +126,19 @@ export async function POST(request: Request) {
|
||||
return NextResponse.json({ error: 'title, date, and schoolId are required' }, { status: 400 });
|
||||
}
|
||||
|
||||
if (!session.user || (!isAdministrator(session.user.role) && session.user.schoolId !== schoolId)) {
|
||||
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
||||
}
|
||||
if (session.user.role === 'TEACHER' && classGroupId) {
|
||||
const classIds = await getTeacherClassIds(session.user.id);
|
||||
if (!classIds.includes(classGroupId)) {
|
||||
return NextResponse.json({ error: 'Teachers can only create events for their own classes' }, { status: 403 });
|
||||
}
|
||||
}
|
||||
if (session.user.role === 'STUDENT' || session.user.role === 'PARENT' || session.user.role === 'VICE_PRINCIPAL') {
|
||||
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
||||
}
|
||||
|
||||
// Parse recurrence pattern
|
||||
let parsedPattern: RecurrencePattern | null = null;
|
||||
if (recurrencePattern && recurrencePattern.type && recurrencePattern.type !== 'none') {
|
||||
@@ -220,6 +235,10 @@ export async function GET(request: Request) {
|
||||
return NextResponse.json({ error: 'schoolId is required' }, { status: 400 });
|
||||
}
|
||||
|
||||
if (!session.user || (!isAdministrator(session.user.role) && session.user.schoolId !== schoolId)) {
|
||||
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
||||
}
|
||||
|
||||
let year: number;
|
||||
let monthIndex: number;
|
||||
if (monthParam && /^\d{4}-\d{2}$/.test(monthParam)) {
|
||||
@@ -235,11 +254,25 @@ export async function GET(request: Request) {
|
||||
const startOfMonth = new Date(Date.UTC(year, monthIndex, 1, 0, 0, 0, 0));
|
||||
const endOfMonth = new Date(Date.UTC(year, monthIndex + 1, 0, 23, 59, 59, 999));
|
||||
|
||||
const eventWhere: Record<string, unknown> = {
|
||||
schoolId,
|
||||
date: { gte: startOfMonth, lte: endOfMonth },
|
||||
};
|
||||
if (session.user.role === 'TEACHER') {
|
||||
const classIds = await getTeacherClassIds(session.user.id);
|
||||
eventWhere.OR = [{ teacherId: session.user.id }, { classGroupId: { in: classIds } }];
|
||||
} else if (session.user.role === 'STUDENT') {
|
||||
const student = await db.student.findFirst({ where: { userId: session.user.id }, select: { id: true } });
|
||||
const enrollments = student ? await db.enrollment.findMany({ where: { studentId: student.id, endDate: null }, select: { classGroupId: true } }) : [];
|
||||
eventWhere.classGroupId = { in: enrollments.map((enrollment) => enrollment.classGroupId) };
|
||||
} else if (session.user.role === 'PARENT') {
|
||||
const links = await db.parentStudentLink.findMany({ where: { parentId: session.user.id }, select: { studentId: true } });
|
||||
const enrollments = await db.enrollment.findMany({ where: { studentId: { in: links.map((link) => link.studentId) }, endDate: null }, select: { classGroupId: true } });
|
||||
eventWhere.classGroupId = { in: enrollments.map((enrollment) => enrollment.classGroupId) };
|
||||
}
|
||||
|
||||
const events = await db.calendarEvent.findMany({
|
||||
where: {
|
||||
schoolId,
|
||||
date: { gte: startOfMonth, lte: endOfMonth },
|
||||
},
|
||||
where: eventWhere,
|
||||
include: {
|
||||
subject: { select: { id: true, name: true } },
|
||||
classGroup: { select: { id: true, name: true } },
|
||||
|
||||
@@ -2,6 +2,8 @@ import { NextResponse } from 'next/server';
|
||||
import { z } from 'zod';
|
||||
import { db } from '@/lib/db';
|
||||
import { getSession } from '@/lib/auth';
|
||||
import { canAccessClass } from '@/lib/access-policy';
|
||||
import { isAdministrator } from '@/lib/role-access';
|
||||
|
||||
const updateClassSchema = z.object({
|
||||
name: z.string().min(1).optional(),
|
||||
@@ -64,6 +66,10 @@ export async function GET(
|
||||
return NextResponse.json({ error: 'Class not found' }, { status: 404 });
|
||||
}
|
||||
|
||||
if (!session.user || !(await canAccessClass(session.user, id))) {
|
||||
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
||||
}
|
||||
|
||||
const result = {
|
||||
...classGroup,
|
||||
studentCount: classGroup._count.enrollments,
|
||||
@@ -94,11 +100,7 @@ export async function PUT(
|
||||
return NextResponse.json({ error: 'Not authenticated' }, { status: 401 });
|
||||
}
|
||||
|
||||
if (
|
||||
session.user?.role !== 'SUPER_ADMIN' &&
|
||||
session.user?.role !== 'SCHOOL_ADMIN' &&
|
||||
session.user?.role !== 'TEACHER'
|
||||
) {
|
||||
if (!isAdministrator(session.user?.role)) {
|
||||
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
||||
}
|
||||
|
||||
@@ -117,6 +119,10 @@ export async function PUT(
|
||||
return NextResponse.json({ error: 'Class not found' }, { status: 404 });
|
||||
}
|
||||
|
||||
if (session.user?.role !== 'SUPER_ADMIN' && existing.schoolId !== session.user?.schoolId) {
|
||||
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
||||
}
|
||||
|
||||
// If updating responsibleTeacherId, validate the teacher exists
|
||||
if (parsed.data.responsibleTeacherId !== undefined) {
|
||||
const teacherId = parsed.data.responsibleTeacherId;
|
||||
@@ -226,6 +232,10 @@ export async function DELETE(
|
||||
return NextResponse.json({ error: 'Class not found' }, { status: 404 });
|
||||
}
|
||||
|
||||
if (session.user?.role !== 'SUPER_ADMIN' && existing.schoolId !== session.user?.schoolId) {
|
||||
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
||||
}
|
||||
|
||||
await db.classGroup.delete({ where: { id } });
|
||||
|
||||
await db.auditLog.create({
|
||||
|
||||
@@ -124,6 +124,9 @@ export async function PUT(
|
||||
if (role !== 'SCHOOL_ADMIN' && role !== 'VICE_PRINCIPAL' && role !== 'SUPER_ADMIN') {
|
||||
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
||||
}
|
||||
if (role !== 'SUPER_ADMIN' && room.schoolId !== session.user?.schoolId) {
|
||||
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
||||
}
|
||||
}
|
||||
|
||||
const updated = await db.communicationRoom.update({
|
||||
@@ -192,10 +195,14 @@ export async function DELETE(
|
||||
|
||||
const role = session.user?.role;
|
||||
|
||||
// Only admin can delete rooms
|
||||
if (role !== 'SUPER_ADMIN' && role !== 'SCHOOL_ADMIN' && role !== 'VICE_PRINCIPAL') {
|
||||
// Only school administrators can delete rooms. Vice principals may manage
|
||||
// operational states but must not remove communication records.
|
||||
if (role !== 'SUPER_ADMIN' && role !== 'SCHOOL_ADMIN') {
|
||||
return NextResponse.json({ error: 'Only admins can delete rooms' }, { status: 403 });
|
||||
}
|
||||
if (role !== 'SUPER_ADMIN' && room.schoolId !== session.user?.schoolId) {
|
||||
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
||||
}
|
||||
|
||||
await db.communicationMessage.deleteMany({ where: { roomId: id } });
|
||||
await db.communicationRoomMember.deleteMany({ where: { roomId: id } });
|
||||
|
||||
@@ -138,19 +138,27 @@ export async function POST(request: Request) {
|
||||
return NextResponse.json({ error: 'Teachers should use staff communication channels' }, { status: 400 });
|
||||
}
|
||||
|
||||
// Find the student's class and the responsible teacher
|
||||
// Student direct messages target their own profile. Parent direct messages
|
||||
// must explicitly name one linked child.
|
||||
const requestedStudentId = body.studentId as string | undefined;
|
||||
if (role === 'PARENT' && !requestedStudentId) {
|
||||
return NextResponse.json({ error: 'studentId is required for parent conversations' }, { status: 400 });
|
||||
}
|
||||
const studentWhere = role === 'PARENT'
|
||||
? { id: requestedStudentId, parentStudentLinks: { some: { parentId: userId } } }
|
||||
: {
|
||||
OR: [
|
||||
{ userId },
|
||||
{
|
||||
userId: null,
|
||||
firstName: session.user?.firstName,
|
||||
lastName: session.user?.lastName,
|
||||
schoolId: session.user?.schoolId,
|
||||
},
|
||||
],
|
||||
};
|
||||
const student = await db.student.findFirst({
|
||||
where: {
|
||||
OR: [
|
||||
{ userId },
|
||||
{
|
||||
userId: null,
|
||||
firstName: session.user?.firstName,
|
||||
lastName: session.user?.lastName,
|
||||
schoolId: session.user?.schoolId,
|
||||
},
|
||||
],
|
||||
},
|
||||
where: studentWhere,
|
||||
include: {
|
||||
enrollments: {
|
||||
where: { endDate: null },
|
||||
@@ -183,7 +191,7 @@ export async function POST(request: Request) {
|
||||
// Check if there's already an active/requested room
|
||||
const existingRoom = await db.communicationRoom.findFirst({
|
||||
where: {
|
||||
studentId: userId,
|
||||
studentId: role === 'PARENT' ? student.userId ?? userId : userId,
|
||||
teacherId: teacher.id,
|
||||
status: { in: ['requested', 'active'] },
|
||||
},
|
||||
@@ -196,7 +204,7 @@ export async function POST(request: Request) {
|
||||
const room = await db.communicationRoom.create({
|
||||
data: {
|
||||
schoolId: student.schoolId,
|
||||
studentId: userId,
|
||||
studentId: role === 'PARENT' ? student.userId ?? userId : userId,
|
||||
teacherId: teacher.id,
|
||||
roomType: 'chat',
|
||||
status: 'requested',
|
||||
|
||||
@@ -63,7 +63,16 @@ export async function GET(request: Request) {
|
||||
// Super admin can see all
|
||||
}
|
||||
|
||||
if (studentId) where.studentId = studentId;
|
||||
if (studentId) {
|
||||
if (role === 'STUDENT' && where.studentId !== studentId) {
|
||||
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
||||
}
|
||||
if (role === 'PARENT') {
|
||||
const linked = await db.parentStudentLink.findFirst({ where: { parentId: userId, studentId }, select: { id: true } });
|
||||
if (!linked) return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
||||
}
|
||||
where.studentId = studentId;
|
||||
}
|
||||
if (status) where.status = status;
|
||||
if (parentApprovalStatus) where.parentApprovalStatus = parentApprovalStatus;
|
||||
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { db } from '@/lib/db';
|
||||
import { getSession } from '@/lib/auth';
|
||||
import { canAccessStudent } from '@/lib/access-policy';
|
||||
import { isAdministrator } from '@/lib/role-access';
|
||||
|
||||
export async function GET(
|
||||
request: Request,
|
||||
@@ -52,6 +54,16 @@ export async function GET(
|
||||
return NextResponse.json({ error: 'Link not found' }, { status: 404 });
|
||||
}
|
||||
|
||||
if (session.user?.role === 'TEACHER' && (!session.user || !(await canAccessStudent(session.user, link.studentId)))) {
|
||||
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
||||
}
|
||||
if (!isAdministrator(session.user?.role) && session.user?.role !== 'TEACHER' && session.user?.role !== 'PARENT') {
|
||||
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
||||
}
|
||||
if (session.user?.role !== 'SUPER_ADMIN' && session.user?.role !== 'PARENT' && link.schoolId !== session.user?.schoolId) {
|
||||
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
||||
}
|
||||
|
||||
// Parents can only view their own links
|
||||
if (session.user?.role === 'PARENT' && link.parentId !== session.user.id) {
|
||||
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
||||
@@ -83,6 +95,13 @@ export async function PUT(
|
||||
return NextResponse.json({ error: 'Link not found' }, { status: 404 });
|
||||
}
|
||||
|
||||
if (session.user?.role === 'TEACHER' && (!session.user || !(await canAccessStudent(session.user, link.studentId)))) {
|
||||
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
||||
}
|
||||
if (session.user?.role !== 'SUPER_ADMIN' && session.user?.role !== 'PARENT' && link.schoolId !== session.user?.schoolId) {
|
||||
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
||||
}
|
||||
|
||||
// Only admins, teachers, or the parent can update
|
||||
if (
|
||||
session.user?.role !== 'TEACHER' &&
|
||||
@@ -143,6 +162,13 @@ export async function DELETE(
|
||||
return NextResponse.json({ error: 'Link not found' }, { status: 404 });
|
||||
}
|
||||
|
||||
if (session.user?.role === 'TEACHER' && (!session.user || !(await canAccessStudent(session.user, link.studentId)))) {
|
||||
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
||||
}
|
||||
if (session.user?.role !== 'SUPER_ADMIN' && session.user?.role !== 'PARENT' && link.schoolId !== session.user?.schoolId) {
|
||||
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
||||
}
|
||||
|
||||
// Only admins, teachers, or the parent can delete
|
||||
if (
|
||||
session.user?.role !== 'TEACHER' &&
|
||||
|
||||
@@ -2,6 +2,7 @@ import { NextResponse } from 'next/server';
|
||||
import { z } from 'zod';
|
||||
import { db } from '@/lib/db';
|
||||
import { getSession } from '@/lib/auth';
|
||||
import { getTeacherClassIds } from '@/lib/access-policy';
|
||||
|
||||
const studentItemSchema = z.object({
|
||||
firstName: z.string().min(1),
|
||||
@@ -43,6 +44,19 @@ export async function POST(request: Request) {
|
||||
|
||||
const { schoolId, classGroupId, schoolYearId, students } = parsed.data;
|
||||
|
||||
if (session.user?.role !== 'SUPER_ADMIN' && session.user?.schoolId !== schoolId) {
|
||||
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
||||
}
|
||||
if (session.user?.role === 'TEACHER') {
|
||||
if (!classGroupId) {
|
||||
return NextResponse.json({ error: 'Teachers must select one of their classes' }, { status: 400 });
|
||||
}
|
||||
const classIds = await getTeacherClassIds(session.userId);
|
||||
if (!classIds.includes(classGroupId)) {
|
||||
return NextResponse.json({ error: 'Teachers can only import students into their own classes' }, { status: 403 });
|
||||
}
|
||||
}
|
||||
|
||||
// Validate class & school year
|
||||
if (classGroupId) {
|
||||
const cls = await db.classGroup.findUnique({ where: { id: classGroupId } });
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { db } from '@/lib/db';
|
||||
import { getSession } from '@/lib/auth';
|
||||
import { canApproveTeacherLeave, canCreateTeacherLeaveCalendarEvent } from '@/lib/teacher-leave-policy';
|
||||
|
||||
export async function POST(request: Request, { params }: { params: Promise<{ id: string }> }) {
|
||||
const session = await getSession();
|
||||
if (!session) return NextResponse.json({ error: 'Not authenticated' }, { status: 401 });
|
||||
if (!canApproveTeacherLeave(session.user?.role)) {
|
||||
return NextResponse.json({ error: 'Only administrators can approve teacher leave' }, { status: 403 });
|
||||
}
|
||||
|
||||
const { id } = await params;
|
||||
const { action, privateAdminNotes } = await request.json();
|
||||
if (action !== 'approve' && action !== 'reject') {
|
||||
return NextResponse.json({ error: 'action must be approve or reject' }, { status: 400 });
|
||||
}
|
||||
|
||||
const absence = await db.teacherAbsence.findUnique({ where: { id } });
|
||||
if (!absence) return NextResponse.json({ error: 'Absence not found' }, { status: 404 });
|
||||
if (session.user?.role !== 'SUPER_ADMIN' && absence.schoolId !== session.user?.schoolId) {
|
||||
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
||||
}
|
||||
if (absence.approvalStatus !== 'pending') {
|
||||
return NextResponse.json({ error: 'Leave request already processed' }, { status: 400 });
|
||||
}
|
||||
|
||||
const approvalStatus = action === 'approve' ? 'approved' : 'rejected';
|
||||
const updated = await db.teacherAbsence.update({
|
||||
where: { id },
|
||||
data: {
|
||||
approvalStatus,
|
||||
approvedBy: session.userId,
|
||||
approvedAt: new Date(),
|
||||
privateAdminNotes: privateAdminNotes || null,
|
||||
status: approvalStatus === 'approved' ? 'reported' : 'cancelled',
|
||||
},
|
||||
});
|
||||
|
||||
if (canCreateTeacherLeaveCalendarEvent(updated)) {
|
||||
const event = await db.calendarEvent.create({
|
||||
data: {
|
||||
schoolId: updated.schoolId,
|
||||
teacherId: updated.teacherId,
|
||||
title: 'Approved teacher leave',
|
||||
date: updated.startDate,
|
||||
eventType: 'teacher_leave',
|
||||
notes: null,
|
||||
allDay: true,
|
||||
},
|
||||
});
|
||||
await db.teacherAbsence.update({ where: { id }, data: { calendarEventId: event.id } });
|
||||
}
|
||||
|
||||
return NextResponse.json(updated);
|
||||
}
|
||||
@@ -1,9 +1,12 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { db } from '@/lib/db';
|
||||
import { getSession } from '@/lib/auth';
|
||||
|
||||
// GET /api/substitutes/absences - List teacher absences
|
||||
export async function GET(req: NextRequest) {
|
||||
try {
|
||||
const session = await getSession();
|
||||
if (!session) return NextResponse.json({ error: 'Not authenticated' }, { status: 401 });
|
||||
const { searchParams } = new URL(req.url);
|
||||
const schoolId = searchParams.get('schoolId');
|
||||
const teacherId = searchParams.get('teacherId');
|
||||
@@ -16,9 +19,13 @@ export async function GET(req: NextRequest) {
|
||||
return NextResponse.json({ error: 'schoolId is required' }, { status: 400 });
|
||||
}
|
||||
|
||||
if (session.user?.role !== 'SUPER_ADMIN' && session.user?.schoolId !== schoolId) {
|
||||
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
||||
}
|
||||
const where: Record<string, unknown> = { schoolId };
|
||||
if (session.user?.role === 'TEACHER') where.teacherId = session.userId;
|
||||
|
||||
if (teacherId) where.teacherId = teacherId;
|
||||
if (teacherId && session.user?.role !== 'TEACHER') where.teacherId = teacherId;
|
||||
if (type) where.type = type;
|
||||
if (status) where.status = status;
|
||||
|
||||
@@ -57,6 +64,8 @@ export async function GET(req: NextRequest) {
|
||||
// POST /api/substitutes/absences - Create a teacher absence
|
||||
export async function POST(req: NextRequest) {
|
||||
try {
|
||||
const session = await getSession();
|
||||
if (!session) return NextResponse.json({ error: 'Not authenticated' }, { status: 401 });
|
||||
const body = await req.json();
|
||||
const { schoolId, teacherId, type, startDate, endDate, reason, status, notes } = body;
|
||||
|
||||
@@ -64,6 +73,17 @@ export async function POST(req: NextRequest) {
|
||||
return NextResponse.json({ error: 'schoolId, teacherId, type, startDate, and endDate are required' }, { status: 400 });
|
||||
}
|
||||
|
||||
const isTeacherRequest = session.user?.role === 'TEACHER';
|
||||
if (isTeacherRequest && teacherId !== session.userId) {
|
||||
return NextResponse.json({ error: 'Teachers can only submit their own leave request' }, { status: 403 });
|
||||
}
|
||||
if (session.user?.role !== 'SUPER_ADMIN' && session.user?.schoolId !== schoolId) {
|
||||
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
||||
}
|
||||
if (session.user?.role !== 'TEACHER' && session.user?.role !== 'SCHOOL_ADMIN' && session.user?.role !== 'SUPER_ADMIN') {
|
||||
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
||||
}
|
||||
|
||||
const absence = await db.teacherAbsence.create({
|
||||
data: {
|
||||
schoolId,
|
||||
@@ -74,6 +94,8 @@ export async function POST(req: NextRequest) {
|
||||
reason: reason || null,
|
||||
status: status || 'reported',
|
||||
notes: notes || null,
|
||||
approvalStatus: 'pending',
|
||||
documentUrl: body.documentUrl || null,
|
||||
},
|
||||
include: {
|
||||
teacher: { select: { id: true, firstName: true, lastName: true, email: true } },
|
||||
|
||||
@@ -3,6 +3,7 @@ import { NextResponse } from 'next/server';
|
||||
import { z } from 'zod';
|
||||
import { db } from '@/lib/db';
|
||||
import { getSession } from '@/lib/auth';
|
||||
import { canManageUser } from '@/lib/role-access';
|
||||
|
||||
const changeRoleSchema = z.object({
|
||||
userId: z.string().min(1),
|
||||
@@ -48,6 +49,10 @@ export async function PUT(request: Request) {
|
||||
return NextResponse.json({ error: 'User not found' }, { status: 404 });
|
||||
}
|
||||
|
||||
if (!canManageUser(session.user?.role, existing.role, userId === session.userId)) {
|
||||
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
||||
}
|
||||
|
||||
// School admins can only manage users from their own school
|
||||
if (
|
||||
session.user?.role === 'SCHOOL_ADMIN' &&
|
||||
|
||||
Reference in new issue
Block a user