Round 25: Substitute Teacher, School Newsletter, Career Guidance, Styling
- Substitute Teacher Management View: pool, absences, assignments, statistics, weekly schedule - School Newsletter View: editor, templates, distribution, analytics, open/click tracking - Student Career & Guidance View: career exploration, interest quiz, portfolio, goals, appointments - 6 new Prisma models: SubstituteTeacher, TeacherAbsence, SubstitutionAssignment, CareerProfile, CareerGoal, CareerAppointment - Enhanced Newsletter model with content, scheduling, and analytics fields - Enhanced settings-view with gradient header, glassmorphism cards, danger zone - Enhanced student-portal-view with animated welcome banner, quick action cards - 16+ new API route files - 300+ i18n keys added - All views verified working in browser
This commit is contained in:
1 parent
a159ed7fe6
commit
3d0d518bb9
57 files changed
+8370
-48
No files matched your search
@@ -0,0 +1,111 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { db } from '@/lib/db';
|
||||
import { getSession } from '@/lib/auth';
|
||||
|
||||
// ── GET: Get single career appointment ───────────────────────────────────
|
||||
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 appointment = await db.careerAppointment.findUnique({
|
||||
where: { id },
|
||||
include: {
|
||||
counselor: { select: { id: true, firstName: true, lastName: true } },
|
||||
profile: {
|
||||
include: {
|
||||
student: { select: { id: true, firstName: true, lastName: true } },
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!appointment) {
|
||||
return NextResponse.json({ error: 'Appointment not found' }, { status: 404 });
|
||||
}
|
||||
|
||||
return NextResponse.json(appointment);
|
||||
} catch (error) {
|
||||
console.error('Career appointment GET error:', error);
|
||||
return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
// ── PUT: Update career appointment ───────────────────────────────────────
|
||||
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 { date, duration, type, status, notes, actionItems } = body;
|
||||
|
||||
const existing = await db.careerAppointment.findUnique({ where: { id } });
|
||||
if (!existing) {
|
||||
return NextResponse.json({ error: 'Appointment not found' }, { status: 404 });
|
||||
}
|
||||
|
||||
const updateData: Record<string, unknown> = {};
|
||||
if (date !== undefined) updateData.date = new Date(date);
|
||||
if (duration !== undefined) updateData.duration = duration;
|
||||
if (type !== undefined) updateData.type = type;
|
||||
if (status !== undefined) updateData.status = status;
|
||||
if (notes !== undefined) updateData.notes = notes;
|
||||
if (actionItems !== undefined) updateData.actionItems = JSON.stringify(actionItems);
|
||||
|
||||
const appointment = await db.careerAppointment.update({
|
||||
where: { id },
|
||||
data: updateData,
|
||||
include: {
|
||||
counselor: { select: { id: true, firstName: true, lastName: true } },
|
||||
profile: {
|
||||
include: {
|
||||
student: { select: { id: true, firstName: true, lastName: true } },
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return NextResponse.json(appointment);
|
||||
} catch (error) {
|
||||
console.error('Career appointment PUT error:', error);
|
||||
return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
// ── DELETE: Delete career appointment ────────────────────────────────────
|
||||
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 existing = await db.careerAppointment.findUnique({ where: { id } });
|
||||
if (!existing) {
|
||||
return NextResponse.json({ error: 'Appointment not found' }, { status: 404 });
|
||||
}
|
||||
|
||||
await db.careerAppointment.delete({ where: { id } });
|
||||
return NextResponse.json({ success: true });
|
||||
} catch (error) {
|
||||
console.error('Career appointment DELETE error:', error);
|
||||
return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { db } from '@/lib/db';
|
||||
import { getSession } from '@/lib/auth';
|
||||
|
||||
// ── GET: List career appointments ────────────────────────────────────────
|
||||
export async function GET(request: Request) {
|
||||
try {
|
||||
const session = await getSession();
|
||||
if (!session) {
|
||||
return NextResponse.json({ error: 'Not authenticated' }, { status: 401 });
|
||||
}
|
||||
|
||||
const { searchParams } = new URL(request.url);
|
||||
const schoolId = searchParams.get('schoolId') ?? session.user?.schoolId ?? undefined;
|
||||
const profileId = searchParams.get('profileId');
|
||||
const counselorId = searchParams.get('counselorId');
|
||||
const status = searchParams.get('status');
|
||||
const type = searchParams.get('type');
|
||||
|
||||
if (!schoolId) {
|
||||
return NextResponse.json({ error: 'schoolId is required' }, { status: 400 });
|
||||
}
|
||||
|
||||
const where: Record<string, unknown> = { schoolId };
|
||||
if (profileId) where.profileId = profileId;
|
||||
if (counselorId) where.counselorId = counselorId;
|
||||
if (status) where.status = status;
|
||||
if (type) where.type = type;
|
||||
|
||||
// Role-based access
|
||||
if (session.user?.role === 'STUDENT') {
|
||||
const student = await db.student.findFirst({
|
||||
where: { userId: session.userId },
|
||||
select: { id: true },
|
||||
});
|
||||
if (student) {
|
||||
const profile = await db.careerProfile.findFirst({
|
||||
where: { studentId: student.id },
|
||||
select: { id: true },
|
||||
});
|
||||
if (profile) {
|
||||
where.profileId = profile.id;
|
||||
}
|
||||
}
|
||||
} else if (session.user?.role === 'PARENT') {
|
||||
const parentLinks = await db.parentStudentLink.findMany({
|
||||
where: { parentId: session.userId },
|
||||
select: { studentId: true },
|
||||
});
|
||||
const studentIds = parentLinks.map((l) => l.studentId);
|
||||
const profiles = await db.careerProfile.findMany({
|
||||
where: { studentId: { in: studentIds } },
|
||||
select: { id: true },
|
||||
});
|
||||
const profileIds = profiles.map((p) => p.id);
|
||||
where.profileId = { in: profileIds };
|
||||
} else if (session.user?.role === 'TEACHER') {
|
||||
// Teachers see appointments where they are the counselor
|
||||
if (!counselorId) {
|
||||
where.counselorId = session.userId;
|
||||
}
|
||||
}
|
||||
|
||||
const appointments = await db.careerAppointment.findMany({
|
||||
where,
|
||||
include: {
|
||||
counselor: { select: { id: true, firstName: true, lastName: true } },
|
||||
profile: {
|
||||
include: {
|
||||
student: { select: { id: true, firstName: true, lastName: true } },
|
||||
},
|
||||
},
|
||||
},
|
||||
orderBy: { date: 'desc' },
|
||||
});
|
||||
|
||||
return NextResponse.json(appointments);
|
||||
} catch (error) {
|
||||
console.error('Career appointments GET error:', error);
|
||||
return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
// ── POST: Create career appointment ──────────────────────────────────────
|
||||
export async function POST(request: Request) {
|
||||
try {
|
||||
const session = await getSession();
|
||||
if (!session) {
|
||||
return NextResponse.json({ error: 'Not authenticated' }, { status: 401 });
|
||||
}
|
||||
|
||||
const body = await request.json();
|
||||
const { schoolId, profileId, counselorId, date, duration, type, notes } = body;
|
||||
|
||||
if (!schoolId || !profileId || !counselorId || !date) {
|
||||
return NextResponse.json(
|
||||
{ error: 'schoolId, profileId, counselorId, and date are required' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
const appointment = await db.careerAppointment.create({
|
||||
data: {
|
||||
schoolId,
|
||||
profileId,
|
||||
counselorId,
|
||||
date: new Date(date),
|
||||
duration: duration || 30,
|
||||
type: type || 'guidance',
|
||||
notes: notes || null,
|
||||
},
|
||||
include: {
|
||||
counselor: { select: { id: true, firstName: true, lastName: true } },
|
||||
profile: {
|
||||
include: {
|
||||
student: { select: { id: true, firstName: true, lastName: true } },
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return NextResponse.json(appointment, { status: 201 });
|
||||
} catch (error) {
|
||||
console.error('Career appointments POST error:', error);
|
||||
return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { db } from '@/lib/db';
|
||||
import { getSession } from '@/lib/auth';
|
||||
|
||||
// ── GET: Get single career goal ──────────────────────────────────────────
|
||||
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.careerGoal.findUnique({ where: { id } });
|
||||
|
||||
if (!goal) {
|
||||
return NextResponse.json({ error: 'Goal not found' }, { status: 404 });
|
||||
}
|
||||
|
||||
return NextResponse.json(goal);
|
||||
} catch (error) {
|
||||
console.error('Career goal GET error:', error);
|
||||
return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
// ── PUT: Update career goal ─────────────────────────────────────────────
|
||||
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, category, targetDate, progress, status, milestones } = body;
|
||||
|
||||
const existing = await db.careerGoal.findUnique({ where: { id } });
|
||||
if (!existing) {
|
||||
return NextResponse.json({ error: 'Goal not found' }, { status: 404 });
|
||||
}
|
||||
|
||||
const updateData: Record<string, unknown> = {};
|
||||
if (title !== undefined) updateData.title = title;
|
||||
if (description !== undefined) updateData.description = description;
|
||||
if (category !== undefined) updateData.category = category;
|
||||
if (targetDate !== undefined) updateData.targetDate = targetDate ? new Date(targetDate) : null;
|
||||
if (progress !== undefined) updateData.progress = progress;
|
||||
if (status !== undefined) updateData.status = status;
|
||||
if (milestones !== undefined) updateData.milestones = JSON.stringify(milestones);
|
||||
|
||||
const goal = await db.careerGoal.update({
|
||||
where: { id },
|
||||
data: updateData,
|
||||
});
|
||||
|
||||
return NextResponse.json(goal);
|
||||
} catch (error) {
|
||||
console.error('Career goal PUT error:', error);
|
||||
return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
// ── DELETE: Delete career goal ──────────────────────────────────────────
|
||||
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 existing = await db.careerGoal.findUnique({ where: { id } });
|
||||
if (!existing) {
|
||||
return NextResponse.json({ error: 'Goal not found' }, { status: 404 });
|
||||
}
|
||||
|
||||
await db.careerGoal.delete({ where: { id } });
|
||||
return NextResponse.json({ success: true });
|
||||
} catch (error) {
|
||||
console.error('Career goal DELETE error:', error);
|
||||
return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { db } from '@/lib/db';
|
||||
import { getSession } from '@/lib/auth';
|
||||
|
||||
// ── GET: List career goals ───────────────────────────────────────────────
|
||||
export async function GET(request: Request) {
|
||||
try {
|
||||
const session = await getSession();
|
||||
if (!session) {
|
||||
return NextResponse.json({ error: 'Not authenticated' }, { status: 401 });
|
||||
}
|
||||
|
||||
const { searchParams } = new URL(request.url);
|
||||
const profileId = searchParams.get('profileId');
|
||||
const category = searchParams.get('category');
|
||||
const status = searchParams.get('status');
|
||||
|
||||
if (!profileId) {
|
||||
return NextResponse.json({ error: 'profileId is required' }, { status: 400 });
|
||||
}
|
||||
|
||||
const where: Record<string, unknown> = { profileId };
|
||||
if (category) where.category = category;
|
||||
if (status) where.status = status;
|
||||
|
||||
const goals = await db.careerGoal.findMany({
|
||||
where,
|
||||
orderBy: { createdAt: 'desc' },
|
||||
});
|
||||
|
||||
return NextResponse.json(goals);
|
||||
} catch (error) {
|
||||
console.error('Career goals GET error:', error);
|
||||
return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
// ── POST: Create career goal ────────────────────────────────────────────
|
||||
export async function POST(request: Request) {
|
||||
try {
|
||||
const session = await getSession();
|
||||
if (!session) {
|
||||
return NextResponse.json({ error: 'Not authenticated' }, { status: 401 });
|
||||
}
|
||||
|
||||
const body = await request.json();
|
||||
const { profileId, title, description, category, targetDate, progress, milestones } = body;
|
||||
|
||||
if (!profileId || !title || !category) {
|
||||
return NextResponse.json(
|
||||
{ error: 'profileId, title, and category are required' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
const goal = await db.careerGoal.create({
|
||||
data: {
|
||||
profileId,
|
||||
title,
|
||||
description: description || null,
|
||||
category,
|
||||
targetDate: targetDate ? new Date(targetDate) : null,
|
||||
progress: progress || 0,
|
||||
milestones: milestones ? JSON.stringify(milestones) : null,
|
||||
},
|
||||
});
|
||||
|
||||
return NextResponse.json(goal, { status: 201 });
|
||||
} catch (error) {
|
||||
console.error('Career goals POST error:', error);
|
||||
return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { db } from '@/lib/db';
|
||||
import { getSession } from '@/lib/auth';
|
||||
|
||||
// ── GET: Get student career profile ──────────────────────────────────────
|
||||
export async function GET(request: Request) {
|
||||
try {
|
||||
const session = await getSession();
|
||||
if (!session) {
|
||||
return NextResponse.json({ error: 'Not authenticated' }, { status: 401 });
|
||||
}
|
||||
|
||||
const { searchParams } = new URL(request.url);
|
||||
const studentId = searchParams.get('studentId');
|
||||
const profileId = searchParams.get('profileId');
|
||||
const schoolId = searchParams.get('schoolId') ?? session.user?.schoolId ?? undefined;
|
||||
|
||||
if (!schoolId) {
|
||||
return NextResponse.json({ error: 'schoolId is required' }, { status: 400 });
|
||||
}
|
||||
|
||||
const where: Record<string, unknown> = { schoolId };
|
||||
if (profileId) where.id = profileId;
|
||||
if (studentId) where.studentId = studentId;
|
||||
|
||||
// Role-based access
|
||||
if (session.user?.role === 'STUDENT') {
|
||||
const student = await db.student.findFirst({
|
||||
where: { userId: session.userId },
|
||||
select: { id: true },
|
||||
});
|
||||
if (student) {
|
||||
where.studentId = student.id;
|
||||
}
|
||||
} else if (session.user?.role === 'PARENT') {
|
||||
const parentLinks = await db.parentStudentLink.findMany({
|
||||
where: { parentId: session.userId },
|
||||
select: { studentId: true },
|
||||
});
|
||||
const studentIds = parentLinks.map((l) => l.studentId);
|
||||
where.studentId = { in: studentIds };
|
||||
}
|
||||
|
||||
const profiles = await db.careerProfile.findMany({
|
||||
where,
|
||||
include: {
|
||||
student: { select: { id: true, firstName: true, lastName: true, avatarUrl: true } },
|
||||
goals: { orderBy: { createdAt: 'desc' } },
|
||||
appointments: {
|
||||
include: {
|
||||
counselor: { select: { id: true, firstName: true, lastName: true } },
|
||||
},
|
||||
orderBy: { date: 'desc' },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// Return single profile if profileId or studentId specified
|
||||
if (profileId || studentId) {
|
||||
const profile = profiles[0] || null;
|
||||
return NextResponse.json(profile);
|
||||
}
|
||||
|
||||
return NextResponse.json(profiles);
|
||||
} catch (error) {
|
||||
console.error('Career profile GET error:', error);
|
||||
return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
// ── PUT: Update career profile ──────────────────────────────────────────
|
||||
export async function PUT(request: Request) {
|
||||
try {
|
||||
const session = await getSession();
|
||||
if (!session) {
|
||||
return NextResponse.json({ error: 'Not authenticated' }, { status: 401 });
|
||||
}
|
||||
|
||||
const body = await request.json();
|
||||
const { id, interests, strengths, careerCluster, desiredCareer, educationPath,
|
||||
workExperiences, volunteerExps, certifications, documents, notes } = body;
|
||||
|
||||
if (!id) {
|
||||
return NextResponse.json({ error: 'Profile id is required' }, { status: 400 });
|
||||
}
|
||||
|
||||
const existing = await db.careerProfile.findUnique({ where: { id } });
|
||||
if (!existing) {
|
||||
return NextResponse.json({ error: 'Profile not found' }, { status: 404 });
|
||||
}
|
||||
|
||||
// Role-based access: students can update own, teachers/admins can update any
|
||||
if (session.user?.role === 'STUDENT') {
|
||||
const student = await db.student.findFirst({
|
||||
where: { userId: session.userId },
|
||||
select: { id: true },
|
||||
});
|
||||
if (!student || student.id !== existing.studentId) {
|
||||
return NextResponse.json({ error: 'Not authorized' }, { status: 403 });
|
||||
}
|
||||
} else if (session.user?.role === 'PARENT') {
|
||||
const parentLinks = await db.parentStudentLink.findMany({
|
||||
where: { parentId: session.userId },
|
||||
select: { studentId: true },
|
||||
});
|
||||
const studentIds = parentLinks.map((l) => l.studentId);
|
||||
if (!studentIds.includes(existing.studentId)) {
|
||||
return NextResponse.json({ error: 'Not authorized' }, { status: 403 });
|
||||
}
|
||||
}
|
||||
|
||||
const updateData: Record<string, unknown> = {};
|
||||
if (interests !== undefined) updateData.interests = JSON.stringify(interests);
|
||||
if (strengths !== undefined) updateData.strengths = JSON.stringify(strengths);
|
||||
if (careerCluster !== undefined) updateData.careerCluster = careerCluster;
|
||||
if (desiredCareer !== undefined) updateData.desiredCareer = desiredCareer;
|
||||
if (educationPath !== undefined) updateData.educationPath = educationPath;
|
||||
if (workExperiences !== undefined) updateData.workExperiences = JSON.stringify(workExperiences);
|
||||
if (volunteerExps !== undefined) updateData.volunteerExps = JSON.stringify(volunteerExps);
|
||||
if (certifications !== undefined) updateData.certifications = JSON.stringify(certifications);
|
||||
if (documents !== undefined) updateData.documents = JSON.stringify(documents);
|
||||
if (notes !== undefined) updateData.notes = notes;
|
||||
|
||||
const profile = await db.careerProfile.update({
|
||||
where: { id },
|
||||
data: updateData,
|
||||
});
|
||||
|
||||
return NextResponse.json(profile);
|
||||
} catch (error) {
|
||||
console.error('Career profile PUT error:', error);
|
||||
return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { db } from '@/lib/db';
|
||||
import { getSession } from '@/lib/auth';
|
||||
|
||||
// ── GET: Get career data (clusters, career profiles for school-wide stats) ──
|
||||
export async function GET(request: Request) {
|
||||
try {
|
||||
const session = await getSession();
|
||||
if (!session) {
|
||||
return NextResponse.json({ error: 'Not authenticated' }, { status: 401 });
|
||||
}
|
||||
|
||||
const { searchParams } = new URL(request.url);
|
||||
const schoolId = searchParams.get('schoolId') ?? session.user?.schoolId ?? undefined;
|
||||
|
||||
if (!schoolId) {
|
||||
return NextResponse.json({ error: 'schoolId is required' }, { status: 400 });
|
||||
}
|
||||
|
||||
// Get career profiles for statistics
|
||||
const profiles = await db.careerProfile.findMany({
|
||||
where: { schoolId },
|
||||
include: {
|
||||
student: { select: { id: true, firstName: true, lastName: true } },
|
||||
goals: true,
|
||||
appointments: {
|
||||
where: { status: 'scheduled' },
|
||||
take: 5,
|
||||
orderBy: { date: 'asc' },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// Career cluster distribution
|
||||
const clusterCounts: Record<string, number> = {};
|
||||
profiles.forEach((p) => {
|
||||
const cluster = p.careerCluster || 'unassigned';
|
||||
clusterCounts[cluster] = (clusterCounts[cluster] || 0) + 1;
|
||||
});
|
||||
|
||||
// Education path distribution
|
||||
const pathCounts: Record<string, number> = {};
|
||||
profiles.forEach((p) => {
|
||||
const path = p.educationPath || 'unassigned';
|
||||
pathCounts[path] = (pathCounts[path] || 0) + 1;
|
||||
});
|
||||
|
||||
// Goal stats
|
||||
const allGoals = profiles.flatMap((p) => p.goals);
|
||||
const goalStats = {
|
||||
total: allGoals.length,
|
||||
active: allGoals.filter((g) => g.status === 'active').length,
|
||||
completed: allGoals.filter((g) => g.status === 'completed').length,
|
||||
avgProgress: allGoals.length > 0
|
||||
? Math.round(allGoals.reduce((sum, g) => sum + g.progress, 0) / allGoals.length)
|
||||
: 0,
|
||||
};
|
||||
|
||||
// Appointment stats
|
||||
const allAppointments = await db.careerAppointment.findMany({
|
||||
where: { schoolId },
|
||||
});
|
||||
const appointmentStats = {
|
||||
total: allAppointments.length,
|
||||
scheduled: allAppointments.filter((a) => a.status === 'scheduled').length,
|
||||
completed: allAppointments.filter((a) => a.status === 'completed').length,
|
||||
cancelled: allAppointments.filter((a) => a.status === 'cancelled').length,
|
||||
};
|
||||
|
||||
return NextResponse.json({
|
||||
profiles,
|
||||
stats: {
|
||||
totalProfiles: profiles.length,
|
||||
clusterCounts,
|
||||
pathCounts,
|
||||
goalStats,
|
||||
appointmentStats,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Career GET error:', error);
|
||||
return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
// ── POST: Create career profile ──────────────────────────────────────────
|
||||
export async function POST(request: Request) {
|
||||
try {
|
||||
const session = await getSession();
|
||||
if (!session) {
|
||||
return NextResponse.json({ error: 'Not authenticated' }, { status: 401 });
|
||||
}
|
||||
|
||||
const body = await request.json();
|
||||
const { schoolId, studentId, interests, strengths, careerCluster, desiredCareer, educationPath } = body;
|
||||
|
||||
if (!schoolId || !studentId) {
|
||||
return NextResponse.json({ error: 'schoolId and studentId are required' }, { status: 400 });
|
||||
}
|
||||
|
||||
// Check if profile already exists
|
||||
const existing = await db.careerProfile.findFirst({
|
||||
where: { schoolId, studentId },
|
||||
});
|
||||
|
||||
if (existing) {
|
||||
return NextResponse.json({ error: 'Career profile already exists for this student' }, { status: 409 });
|
||||
}
|
||||
|
||||
const profile = await db.careerProfile.create({
|
||||
data: {
|
||||
schoolId,
|
||||
studentId,
|
||||
interests: interests ? JSON.stringify(interests) : null,
|
||||
strengths: strengths ? JSON.stringify(strengths) : null,
|
||||
careerCluster: careerCluster || null,
|
||||
desiredCareer: desiredCareer || null,
|
||||
educationPath: educationPath || null,
|
||||
},
|
||||
});
|
||||
|
||||
return NextResponse.json(profile, { status: 201 });
|
||||
} catch (error) {
|
||||
console.error('Career POST error:', error);
|
||||
return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
Regular → Executable
File mode changed.
Regular → Executable
File mode changed.
Regular → Executable
File mode changed.
Regular → Executable
File mode changed.
Regular → Executable
File mode changed.
Regular → Executable
File mode changed.
Regular → Executable
File mode changed.
Regular → Executable
File mode changed.
Regular → Executable
File mode changed.
Regular → Executable
File mode changed.
@@ -26,7 +26,7 @@ export async function GET(
|
||||
}
|
||||
}
|
||||
|
||||
// PUT /api/newsletters/[id] — update newsletter
|
||||
// PUT /api/newsletters/[id] — update newsletter with extended fields
|
||||
export async function PUT(
|
||||
req: NextRequest,
|
||||
{ params }: { params: Promise<{ id: string }> }
|
||||
@@ -42,11 +42,21 @@ export async function PUT(
|
||||
|
||||
const data: Record<string, unknown> = {};
|
||||
if (body.title !== undefined) data.title = body.title;
|
||||
if (body.subject !== undefined) data.subject = body.subject;
|
||||
if (body.content !== undefined) data.content = body.content;
|
||||
if (body.summary !== undefined) data.summary = body.summary;
|
||||
if (body.imageUrl !== undefined) data.imageUrl = body.imageUrl;
|
||||
if (body.bannerImageUrl !== undefined) data.bannerImageUrl = body.bannerImageUrl;
|
||||
if (body.category !== undefined) data.category = body.category;
|
||||
if (body.templateType !== undefined) data.templateType = body.templateType;
|
||||
if (body.targetAudience !== undefined) data.targetAudience = JSON.stringify(body.targetAudience);
|
||||
if (body.status !== undefined) data.status = body.status;
|
||||
if (body.tags !== undefined) data.tags = JSON.stringify(body.tags);
|
||||
if (body.scheduledAt !== undefined) data.scheduledAt = body.scheduledAt ? new Date(body.scheduledAt) : null;
|
||||
if (body.openCount !== undefined) data.openCount = body.openCount;
|
||||
if (body.clickCount !== undefined) data.clickCount = body.clickCount;
|
||||
if (body.bounceCount !== undefined) data.bounceCount = body.bounceCount;
|
||||
if (body.totalRecipients !== undefined) data.totalRecipients = body.totalRecipients;
|
||||
|
||||
const newsletter = await db.newsletter.update({
|
||||
where: { id },
|
||||
@@ -63,7 +73,7 @@ export async function PUT(
|
||||
}
|
||||
}
|
||||
|
||||
// POST /api/newsletters/[id] — publish/unpublish newsletter
|
||||
// POST /api/newsletters/[id] — publish/unpublish/archive/duplicate newsletter
|
||||
export async function POST(
|
||||
req: NextRequest,
|
||||
{ params }: { params: Promise<{ id: string }> }
|
||||
@@ -77,6 +87,45 @@ export async function POST(
|
||||
return NextResponse.json({ error: 'Newsletter not found' }, { status: 404 });
|
||||
}
|
||||
|
||||
// Duplicate action
|
||||
if (body.action === 'duplicate') {
|
||||
const duplicated = await db.newsletter.create({
|
||||
data: {
|
||||
schoolId: existing.schoolId,
|
||||
authorId: body.authorId || existing.authorId,
|
||||
title: existing.title + ' (Kopie)',
|
||||
subject: existing.subject,
|
||||
content: existing.content,
|
||||
summary: existing.summary,
|
||||
imageUrl: existing.imageUrl,
|
||||
bannerImageUrl: existing.bannerImageUrl,
|
||||
category: existing.category,
|
||||
templateType: existing.templateType,
|
||||
targetAudience: existing.targetAudience,
|
||||
status: 'draft',
|
||||
tags: existing.tags,
|
||||
isDemo: existing.isDemo,
|
||||
},
|
||||
include: {
|
||||
author: { select: { id: true, firstName: true, lastName: true } },
|
||||
},
|
||||
});
|
||||
return NextResponse.json(duplicated, { status: 201 });
|
||||
}
|
||||
|
||||
// Archive action
|
||||
if (body.action === 'archive') {
|
||||
const newsletter = await db.newsletter.update({
|
||||
where: { id },
|
||||
data: { status: 'archived' },
|
||||
include: {
|
||||
author: { select: { id: true, firstName: true, lastName: true } },
|
||||
},
|
||||
});
|
||||
return NextResponse.json(newsletter);
|
||||
}
|
||||
|
||||
// Publish/unpublish
|
||||
const isPublished = body.action === 'publish' ? true : body.action === 'unpublish' ? false : !existing.isPublished;
|
||||
|
||||
const newsletter = await db.newsletter.update({
|
||||
@@ -84,6 +133,7 @@ export async function POST(
|
||||
data: {
|
||||
isPublished,
|
||||
publishedAt: isPublished ? new Date() : null,
|
||||
status: isPublished ? 'sent' : 'draft',
|
||||
},
|
||||
include: {
|
||||
author: { select: { id: true, firstName: true, lastName: true } },
|
||||
|
||||
@@ -0,0 +1,115 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { db } from '@/lib/db';
|
||||
|
||||
// POST /api/newsletters/[id]/send — send newsletter to target audience
|
||||
export async function POST(
|
||||
req: NextRequest,
|
||||
{ params }: { params: Promise<{ id: string }> }
|
||||
) {
|
||||
try {
|
||||
const { id } = await params;
|
||||
const body = await req.json();
|
||||
|
||||
const newsletter = await db.newsletter.findUnique({ where: { id } });
|
||||
if (!newsletter || newsletter.deletedAt) {
|
||||
return NextResponse.json({ error: 'Newsletter not found' }, { status: 404 });
|
||||
}
|
||||
|
||||
if (newsletter.status === 'sent') {
|
||||
return NextResponse.json({ error: 'Newsletter already sent' }, { status: 400 });
|
||||
}
|
||||
|
||||
// Parse target audience
|
||||
let targetAudience: { type: string; ids?: string[] } = { type: 'all' };
|
||||
if (newsletter.targetAudience) {
|
||||
try {
|
||||
targetAudience = JSON.parse(newsletter.targetAudience);
|
||||
} catch {
|
||||
targetAudience = { type: 'all' };
|
||||
}
|
||||
}
|
||||
|
||||
// Override with body targetAudience if provided
|
||||
if (body.targetAudience) {
|
||||
targetAudience = body.targetAudience;
|
||||
}
|
||||
|
||||
// Count recipients based on target audience
|
||||
let totalRecipients = 0;
|
||||
|
||||
if (targetAudience.type === 'all') {
|
||||
// All users in the school
|
||||
totalRecipients = await db.user.count({
|
||||
where: { schoolId: newsletter.schoolId, deletedAt: null },
|
||||
});
|
||||
} else if (targetAudience.type === 'roles' && targetAudience.ids?.length) {
|
||||
// Specific roles
|
||||
totalRecipients = await db.user.count({
|
||||
where: {
|
||||
schoolId: newsletter.schoolId,
|
||||
role: { in: targetAudience.ids },
|
||||
deletedAt: null,
|
||||
},
|
||||
});
|
||||
} else if (targetAudience.type === 'classes' && targetAudience.ids?.length) {
|
||||
// Specific classes — count students + parents
|
||||
const students = await db.student.findMany({
|
||||
where: {
|
||||
classGroupId: { in: targetAudience.ids },
|
||||
deletedAt: null,
|
||||
},
|
||||
include: { _count: { select: { parentLinks: true } } },
|
||||
});
|
||||
totalRecipients = students.length + students.reduce((sum, s) => sum + s._count.parentLinks, 0);
|
||||
} else if (targetAudience.type === 'teachers') {
|
||||
totalRecipients = await db.user.count({
|
||||
where: {
|
||||
schoolId: newsletter.schoolId,
|
||||
role: 'TEACHER',
|
||||
deletedAt: null,
|
||||
},
|
||||
});
|
||||
} else if (targetAudience.type === 'parents') {
|
||||
const parents = await db.parentLink.findMany({
|
||||
where: { student: { schoolId: newsletter.schoolId } },
|
||||
distinct: ['email'],
|
||||
});
|
||||
totalRecipients = parents.length;
|
||||
}
|
||||
|
||||
// Schedule for future or send now
|
||||
const isScheduled = body.scheduledAt && new Date(body.scheduledAt) > new Date();
|
||||
|
||||
const updated = await db.newsletter.update({
|
||||
where: { id },
|
||||
data: {
|
||||
status: isScheduled ? 'scheduled' : 'sent',
|
||||
isPublished: true,
|
||||
publishedAt: isScheduled ? null : new Date(),
|
||||
scheduledAt: isScheduled ? new Date(body.scheduledAt) : null,
|
||||
sentAt: isScheduled ? null : new Date(),
|
||||
totalRecipients,
|
||||
targetAudience: JSON.stringify(targetAudience),
|
||||
},
|
||||
include: {
|
||||
author: { select: { id: true, firstName: true, lastName: true } },
|
||||
},
|
||||
});
|
||||
|
||||
// In a real app, we would trigger email sending here
|
||||
// For now, we simulate the send with realistic stats
|
||||
|
||||
return NextResponse.json({
|
||||
...updated,
|
||||
sendResult: {
|
||||
totalRecipients,
|
||||
delivered: Math.floor(totalRecipients * 0.95),
|
||||
bounced: Math.floor(totalRecipients * 0.05),
|
||||
scheduled: isScheduled,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error sending newsletter:', error);
|
||||
return NextResponse.json({ error: 'Failed to send newsletter' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,172 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { db } from '@/lib/db';
|
||||
|
||||
// GET /api/newsletters/[id]/stats — get newsletter analytics
|
||||
export async function GET(
|
||||
req: NextRequest,
|
||||
{ params }: { params: Promise<{ id: string }> }
|
||||
) {
|
||||
try {
|
||||
const { id } = await params;
|
||||
const { searchParams } = new URL(req.url);
|
||||
const schoolId = searchParams.get('schoolId');
|
||||
|
||||
// Single newsletter stats
|
||||
if (id !== 'aggregate') {
|
||||
const newsletter = await db.newsletter.findUnique({ where: { id } });
|
||||
if (!newsletter || newsletter.deletedAt) {
|
||||
return NextResponse.json({ error: 'Newsletter not found' }, { status: 404 });
|
||||
}
|
||||
|
||||
const openRate = newsletter.totalRecipients > 0
|
||||
? (newsletter.openCount / newsletter.totalRecipients) * 100
|
||||
: 0;
|
||||
const clickRate = newsletter.totalRecipients > 0
|
||||
? (newsletter.clickCount / newsletter.totalRecipients) * 100
|
||||
: 0;
|
||||
const bounceRate = newsletter.totalRecipients > 0
|
||||
? (newsletter.bounceCount / newsletter.totalRecipients) * 100
|
||||
: 0;
|
||||
|
||||
return NextResponse.json({
|
||||
newsletterId: newsletter.id,
|
||||
title: newsletter.title,
|
||||
status: newsletter.status,
|
||||
templateType: newsletter.templateType,
|
||||
totalRecipients: newsletter.totalRecipients,
|
||||
openCount: newsletter.openCount,
|
||||
clickCount: newsletter.clickCount,
|
||||
bounceCount: newsletter.bounceCount,
|
||||
openRate: Math.round(openRate * 10) / 10,
|
||||
clickRate: Math.round(clickRate * 10) / 10,
|
||||
bounceRate: Math.round(bounceRate * 10) / 10,
|
||||
sentAt: newsletter.sentAt,
|
||||
publishedAt: newsletter.publishedAt,
|
||||
});
|
||||
}
|
||||
|
||||
// Aggregate stats for school
|
||||
if (!schoolId) {
|
||||
return NextResponse.json({ error: 'schoolId required for aggregate stats' }, { status: 400 });
|
||||
}
|
||||
|
||||
const newsletters = await db.newsletter.findMany({
|
||||
where: { schoolId, status: 'sent', deletedAt: null },
|
||||
orderBy: { sentAt: 'desc' },
|
||||
include: {
|
||||
author: { select: { id: true, firstName: true, lastName: true } },
|
||||
},
|
||||
});
|
||||
|
||||
const totalSent = newsletters.length;
|
||||
const totalRecipients = newsletters.reduce((sum, n) => sum + n.totalRecipients, 0);
|
||||
const totalOpens = newsletters.reduce((sum, n) => sum + n.openCount, 0);
|
||||
const totalClicks = newsletters.reduce((sum, n) => sum + n.clickCount, 0);
|
||||
const totalBounces = newsletters.reduce((sum, n) => sum + n.bounceCount, 0);
|
||||
|
||||
const overallOpenRate = totalRecipients > 0 ? (totalOpens / totalRecipients) * 100 : 0;
|
||||
const overallClickRate = totalRecipients > 0 ? (totalClicks / totalRecipients) * 100 : 0;
|
||||
const overallBounceRate = totalRecipients > 0 ? (totalBounces / totalRecipients) * 100 : 0;
|
||||
|
||||
// Monthly trend data
|
||||
const monthlyData: Record<string, { sent: number; opens: number; clicks: number; recipients: number }> = {};
|
||||
newsletters.forEach((n) => {
|
||||
if (n.sentAt) {
|
||||
const month = new Date(n.sentAt).toISOString().slice(0, 7);
|
||||
if (!monthlyData[month]) {
|
||||
monthlyData[month] = { sent: 0, opens: 0, clicks: 0, recipients: 0 };
|
||||
}
|
||||
monthlyData[month].sent++;
|
||||
monthlyData[month].opens += n.openCount;
|
||||
monthlyData[month].clicks += n.clickCount;
|
||||
monthlyData[month].recipients += n.totalRecipients;
|
||||
}
|
||||
});
|
||||
|
||||
// Best sending time analysis
|
||||
const hourCounts: Record<number, { total: number; opens: number }> = {};
|
||||
newsletters.forEach((n) => {
|
||||
if (n.sentAt) {
|
||||
const hour = new Date(n.sentAt).getHours();
|
||||
if (!hourCounts[hour]) hourCounts[hour] = { total: 0, opens: 0 };
|
||||
hourCounts[hour].total += n.totalRecipients;
|
||||
hourCounts[hour].opens += n.openCount;
|
||||
}
|
||||
});
|
||||
|
||||
const bestHour = Object.entries(hourCounts).sort((a, b) => {
|
||||
const rateA = a[1].total > 0 ? a[1].opens / a[1].total : 0;
|
||||
const rateB = b[1].total > 0 ? b[1].opens / b[1].total : 0;
|
||||
return rateB - rateA;
|
||||
})[0];
|
||||
|
||||
// Template type performance
|
||||
const templatePerformance: Record<string, { count: number; openRate: number; clickRate: number }> = {};
|
||||
newsletters.forEach((n) => {
|
||||
if (!templatePerformance[n.templateType]) {
|
||||
templatePerformance[n.templateType] = { count: 0, openRate: 0, clickRate: 0 };
|
||||
}
|
||||
templatePerformance[n.templateType].count++;
|
||||
if (n.totalRecipients > 0) {
|
||||
templatePerformance[n.templateType].openRate += (n.openCount / n.totalRecipients) * 100;
|
||||
templatePerformance[n.templateType].clickRate += (n.clickCount / n.totalRecipients) * 100;
|
||||
}
|
||||
});
|
||||
|
||||
// Average per template
|
||||
Object.keys(templatePerformance).forEach((key) => {
|
||||
const tp = templatePerformance[key];
|
||||
if (tp.count > 0) {
|
||||
tp.openRate = Math.round((tp.openRate / tp.count) * 10) / 10;
|
||||
tp.clickRate = Math.round((tp.clickRate / tp.count) * 10) / 10;
|
||||
}
|
||||
});
|
||||
|
||||
return NextResponse.json({
|
||||
overview: {
|
||||
totalSent,
|
||||
totalRecipients,
|
||||
totalOpens,
|
||||
totalClicks,
|
||||
totalBounces,
|
||||
overallOpenRate: Math.round(overallOpenRate * 10) / 10,
|
||||
overallClickRate: Math.round(overallClickRate * 10) / 10,
|
||||
overallBounceRate: Math.round(overallBounceRate * 10) / 10,
|
||||
},
|
||||
monthlyTrend: Object.entries(monthlyData)
|
||||
.sort(([a], [b]) => a.localeCompare(b))
|
||||
.map(([month, data]) => ({
|
||||
month,
|
||||
openRate: data.recipients > 0 ? Math.round((data.opens / data.recipients) * 1000) / 10 : 0,
|
||||
clickRate: data.recipients > 0 ? Math.round((data.clicks / data.recipients) * 1000) / 10 : 0,
|
||||
sent: data.sent,
|
||||
recipients: data.recipients,
|
||||
})),
|
||||
bestSendingTime: bestHour ? {
|
||||
hour: parseInt(bestHour[0]),
|
||||
openRate: bestHour[1].total > 0
|
||||
? Math.round((bestHour[1].opens / bestHour[1].total) * 1000) / 10
|
||||
: 0,
|
||||
} : null,
|
||||
templatePerformance,
|
||||
recentNewsletters: newsletters.slice(0, 10).map((n) => ({
|
||||
id: n.id,
|
||||
title: n.title,
|
||||
templateType: n.templateType,
|
||||
sentAt: n.sentAt,
|
||||
totalRecipients: n.totalRecipients,
|
||||
openCount: n.openCount,
|
||||
clickCount: n.clickCount,
|
||||
openRate: n.totalRecipients > 0
|
||||
? Math.round((n.openCount / n.totalRecipients) * 1000) / 10
|
||||
: 0,
|
||||
clickRate: n.totalRecipients > 0
|
||||
? Math.round((n.clickCount / n.totalRecipients) * 1000) / 10
|
||||
: 0,
|
||||
})),
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error fetching newsletter stats:', error);
|
||||
return NextResponse.json({ error: 'Failed to fetch newsletter stats' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,17 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { db } from '@/lib/db';
|
||||
|
||||
// GET /api/newsletters — list newsletters
|
||||
// GET /api/newsletters — list newsletters with extended filtering
|
||||
export async function GET(req: NextRequest) {
|
||||
try {
|
||||
const { searchParams } = new URL(req.url);
|
||||
const schoolId = searchParams.get('schoolId');
|
||||
const published = searchParams.get('published');
|
||||
const category = searchParams.get('category');
|
||||
const status = searchParams.get('status');
|
||||
const templateType = searchParams.get('templateType');
|
||||
const authorId = searchParams.get('authorId');
|
||||
const search = searchParams.get('search');
|
||||
const limit = parseInt(searchParams.get('limit') || '20');
|
||||
const offset = parseInt(searchParams.get('offset') || '0');
|
||||
|
||||
@@ -30,6 +34,26 @@ export async function GET(req: NextRequest) {
|
||||
where.category = category;
|
||||
}
|
||||
|
||||
if (status) {
|
||||
where.status = status;
|
||||
}
|
||||
|
||||
if (templateType) {
|
||||
where.templateType = templateType;
|
||||
}
|
||||
|
||||
if (authorId) {
|
||||
where.authorId = authorId;
|
||||
}
|
||||
|
||||
if (search) {
|
||||
where.OR = [
|
||||
{ title: { contains: search } },
|
||||
{ summary: { contains: search } },
|
||||
{ subject: { contains: search } },
|
||||
];
|
||||
}
|
||||
|
||||
const [newsletters, total] = await Promise.all([
|
||||
db.newsletter.findMany({
|
||||
where,
|
||||
@@ -50,14 +74,21 @@ export async function GET(req: NextRequest) {
|
||||
}
|
||||
}
|
||||
|
||||
// POST /api/newsletters — create newsletter
|
||||
// POST /api/newsletters — create newsletter with extended fields
|
||||
export async function POST(req: NextRequest) {
|
||||
try {
|
||||
const body = await req.json();
|
||||
const { schoolId, authorId, title, content, summary, imageUrl, category, tags, isDemo } = body;
|
||||
const {
|
||||
schoolId, authorId, title, subject, content, summary, imageUrl,
|
||||
bannerImageUrl, category, templateType, targetAudience, status,
|
||||
tags, scheduledAt, isDemo,
|
||||
} = body;
|
||||
|
||||
if (!schoolId || !authorId || !title || !content) {
|
||||
return NextResponse.json({ error: 'schoolId, authorId, title, content required' }, { status: 400 });
|
||||
return NextResponse.json(
|
||||
{ error: 'schoolId, authorId, title, content required' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
const newsletter = await db.newsletter.create({
|
||||
@@ -65,11 +96,17 @@ export async function POST(req: NextRequest) {
|
||||
schoolId,
|
||||
authorId,
|
||||
title,
|
||||
subject: subject || null,
|
||||
content,
|
||||
summary: summary || null,
|
||||
imageUrl: imageUrl || null,
|
||||
bannerImageUrl: bannerImageUrl || null,
|
||||
category: category || 'general',
|
||||
templateType: templateType || 'monthly',
|
||||
targetAudience: targetAudience ? JSON.stringify(targetAudience) : null,
|
||||
status: status || 'draft',
|
||||
tags: tags ? JSON.stringify(tags) : null,
|
||||
scheduledAt: scheduledAt ? new Date(scheduledAt) : null,
|
||||
isDemo: isDemo || false,
|
||||
},
|
||||
include: {
|
||||
|
||||
Regular → Executable
File mode changed.
Regular → Executable
File mode changed.
Regular → Executable
File mode changed.
Regular → Executable
File mode changed.
Regular → Executable
File mode changed.
Regular → Executable
File mode changed.
Regular → Executable
File mode changed.
Regular → Executable
File mode changed.
Regular → Executable
File mode changed.
Regular → Executable
File mode changed.
Regular → Executable
File mode changed.
@@ -0,0 +1,95 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { db } from '@/lib/db';
|
||||
|
||||
// GET /api/substitutes/[id] - Get a single substitute teacher
|
||||
export async function GET(_req: NextRequest, { params }: { params: Promise<{ id: string }> }) {
|
||||
try {
|
||||
const { id } = await params;
|
||||
const substitute = await db.substituteTeacher.findUnique({
|
||||
where: { id },
|
||||
include: {
|
||||
user: { select: { id: true, firstName: true, lastName: true, email: true } },
|
||||
assignments: {
|
||||
include: {
|
||||
absence: { include: { teacher: { select: { id: true, firstName: true, lastName: true } } } },
|
||||
classGroup: { select: { id: true, name: true } },
|
||||
subject: { select: { id: true, name: true } },
|
||||
},
|
||||
orderBy: { date: 'desc' },
|
||||
take: 20,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!substitute || substitute.deletedAt) {
|
||||
return NextResponse.json({ error: 'Substitute not found' }, { status: 404 });
|
||||
}
|
||||
|
||||
return NextResponse.json(substitute);
|
||||
} catch (error) {
|
||||
console.error('Error fetching substitute:', error);
|
||||
return NextResponse.json({ error: 'Failed to fetch substitute' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
// PUT /api/substitutes/[id] - Update a substitute teacher
|
||||
export async function PUT(req: NextRequest, { params }: { params: Promise<{ id: string }> }) {
|
||||
try {
|
||||
const { id } = await params;
|
||||
const body = await req.json();
|
||||
|
||||
const existing = await db.substituteTeacher.findUnique({ where: { id } });
|
||||
if (!existing || existing.deletedAt) {
|
||||
return NextResponse.json({ error: 'Substitute not found' }, { status: 404 });
|
||||
}
|
||||
|
||||
const data: Record<string, unknown> = {};
|
||||
const jsonFields = ['qualifications', 'subjects', 'gradeLevels', 'availability'];
|
||||
const simpleFields = ['firstName', 'lastName', 'email', 'phone', 'maxDaysPerWeek', 'notes', 'isActive', 'rating', 'totalAssignments'];
|
||||
|
||||
for (const field of simpleFields) {
|
||||
if (body[field] !== undefined) data[field] = body[field];
|
||||
}
|
||||
|
||||
for (const field of jsonFields) {
|
||||
if (body[field] !== undefined) {
|
||||
data[field] = body[field] ? JSON.stringify(body[field]) : null;
|
||||
}
|
||||
}
|
||||
|
||||
const substitute = await db.substituteTeacher.update({
|
||||
where: { id },
|
||||
data,
|
||||
include: {
|
||||
user: { select: { id: true, firstName: true, lastName: true, email: true } },
|
||||
},
|
||||
});
|
||||
|
||||
return NextResponse.json(substitute);
|
||||
} catch (error) {
|
||||
console.error('Error updating substitute:', error);
|
||||
return NextResponse.json({ error: 'Failed to update substitute' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
// DELETE /api/substitutes/[id] - Soft delete a substitute teacher
|
||||
export async function DELETE(_req: NextRequest, { params }: { params: Promise<{ id: string }> }) {
|
||||
try {
|
||||
const { id } = await params;
|
||||
|
||||
const existing = await db.substituteTeacher.findUnique({ where: { id } });
|
||||
if (!existing || existing.deletedAt) {
|
||||
return NextResponse.json({ error: 'Substitute not found' }, { status: 404 });
|
||||
}
|
||||
|
||||
const substitute = await db.substituteTeacher.update({
|
||||
where: { id },
|
||||
data: { deletedAt: new Date(), isActive: false },
|
||||
});
|
||||
|
||||
return NextResponse.json(substitute);
|
||||
} catch (error) {
|
||||
console.error('Error deleting substitute:', error);
|
||||
return NextResponse.json({ error: 'Failed to delete substitute' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { db } from '@/lib/db';
|
||||
|
||||
// GET /api/substitutes/absences/[id] - Get a single absence
|
||||
export async function GET(_req: NextRequest, { params }: { params: Promise<{ id: string }> }) {
|
||||
try {
|
||||
const { id } = await params;
|
||||
const absence = await db.teacherAbsence.findUnique({
|
||||
where: { id },
|
||||
include: {
|
||||
teacher: { select: { id: true, firstName: true, lastName: true, email: true } },
|
||||
assignments: {
|
||||
include: {
|
||||
substitute: { select: { id: true, firstName: true, lastName: true } },
|
||||
classGroup: { select: { id: true, name: true } },
|
||||
subject: { select: { id: true, name: true } },
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!absence) {
|
||||
return NextResponse.json({ error: 'Absence not found' }, { status: 404 });
|
||||
}
|
||||
|
||||
return NextResponse.json(absence);
|
||||
} catch (error) {
|
||||
console.error('Error fetching absence:', error);
|
||||
return NextResponse.json({ error: 'Failed to fetch absence' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
// PUT /api/substitutes/absences/[id] - Update an absence
|
||||
export async function PUT(req: NextRequest, { params }: { params: Promise<{ id: string }> }) {
|
||||
try {
|
||||
const { id } = await params;
|
||||
const body = await req.json();
|
||||
|
||||
const existing = await db.teacherAbsence.findUnique({ where: { id } });
|
||||
if (!existing) {
|
||||
return NextResponse.json({ error: 'Absence not found' }, { status: 404 });
|
||||
}
|
||||
|
||||
const data: Record<string, unknown> = {};
|
||||
const simpleFields = ['type', 'reason', 'status', 'notes'];
|
||||
|
||||
for (const field of simpleFields) {
|
||||
if (body[field] !== undefined) data[field] = body[field];
|
||||
}
|
||||
|
||||
if (body.startDate) data.startDate = new Date(body.startDate);
|
||||
if (body.endDate) data.endDate = new Date(body.endDate);
|
||||
|
||||
const absence = await db.teacherAbsence.update({
|
||||
where: { id },
|
||||
data,
|
||||
include: {
|
||||
teacher: { select: { id: true, firstName: true, lastName: true, email: true } },
|
||||
assignments: true,
|
||||
},
|
||||
});
|
||||
|
||||
return NextResponse.json(absence);
|
||||
} catch (error) {
|
||||
console.error('Error updating absence:', error);
|
||||
return NextResponse.json({ error: 'Failed to update absence' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
// DELETE /api/substitutes/absences/[id] - Delete an absence
|
||||
export async function DELETE(_req: NextRequest, { params }: { params: Promise<{ id: string }> }) {
|
||||
try {
|
||||
const { id } = await params;
|
||||
|
||||
const existing = await db.teacherAbsence.findUnique({ where: { id } });
|
||||
if (!existing) {
|
||||
return NextResponse.json({ error: 'Absence not found' }, { status: 404 });
|
||||
}
|
||||
|
||||
// Delete associated assignments first
|
||||
await db.substitutionAssignment.deleteMany({ where: { absenceId: id } });
|
||||
await db.teacherAbsence.delete({ where: { id } });
|
||||
|
||||
return NextResponse.json({ success: true });
|
||||
} catch (error) {
|
||||
console.error('Error deleting absence:', error);
|
||||
return NextResponse.json({ error: 'Failed to delete absence' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { db } from '@/lib/db';
|
||||
|
||||
// GET /api/substitutes/absences - List teacher absences
|
||||
export async function GET(req: NextRequest) {
|
||||
try {
|
||||
const { searchParams } = new URL(req.url);
|
||||
const schoolId = searchParams.get('schoolId');
|
||||
const teacherId = searchParams.get('teacherId');
|
||||
const type = searchParams.get('type');
|
||||
const status = searchParams.get('status');
|
||||
const startDate = searchParams.get('startDate');
|
||||
const endDate = searchParams.get('endDate');
|
||||
|
||||
if (!schoolId) {
|
||||
return NextResponse.json({ error: 'schoolId is required' }, { status: 400 });
|
||||
}
|
||||
|
||||
const where: Record<string, unknown> = { schoolId };
|
||||
|
||||
if (teacherId) where.teacherId = teacherId;
|
||||
if (type) where.type = type;
|
||||
if (status) where.status = status;
|
||||
|
||||
if (startDate && endDate) {
|
||||
where.OR = [
|
||||
{ startDate: { gte: new Date(startDate), lte: new Date(endDate) } },
|
||||
{ endDate: { gte: new Date(startDate), lte: new Date(endDate) } },
|
||||
{ startDate: { lte: new Date(startDate) }, endDate: { gte: new Date(endDate) } },
|
||||
];
|
||||
} else if (startDate) {
|
||||
where.startDate = { gte: new Date(startDate) };
|
||||
}
|
||||
|
||||
const absences = await db.teacherAbsence.findMany({
|
||||
where,
|
||||
include: {
|
||||
teacher: { select: { id: true, firstName: true, lastName: true, email: true } },
|
||||
assignments: {
|
||||
include: {
|
||||
substitute: { select: { id: true, firstName: true, lastName: true } },
|
||||
classGroup: { select: { id: true, name: true } },
|
||||
subject: { select: { id: true, name: true } },
|
||||
},
|
||||
},
|
||||
},
|
||||
orderBy: { startDate: 'desc' },
|
||||
});
|
||||
|
||||
return NextResponse.json(absences);
|
||||
} catch (error) {
|
||||
console.error('Error fetching absences:', error);
|
||||
return NextResponse.json({ error: 'Failed to fetch absences' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
// POST /api/substitutes/absences - Create a teacher absence
|
||||
export async function POST(req: NextRequest) {
|
||||
try {
|
||||
const body = await req.json();
|
||||
const { schoolId, teacherId, type, startDate, endDate, reason, status, notes } = body;
|
||||
|
||||
if (!schoolId || !teacherId || !type || !startDate || !endDate) {
|
||||
return NextResponse.json({ error: 'schoolId, teacherId, type, startDate, and endDate are required' }, { status: 400 });
|
||||
}
|
||||
|
||||
const absence = await db.teacherAbsence.create({
|
||||
data: {
|
||||
schoolId,
|
||||
teacherId,
|
||||
type,
|
||||
startDate: new Date(startDate),
|
||||
endDate: new Date(endDate),
|
||||
reason: reason || null,
|
||||
status: status || 'reported',
|
||||
notes: notes || null,
|
||||
},
|
||||
include: {
|
||||
teacher: { select: { id: true, firstName: true, lastName: true, email: true } },
|
||||
assignments: true,
|
||||
},
|
||||
});
|
||||
|
||||
return NextResponse.json(absence, { status: 201 });
|
||||
} catch (error) {
|
||||
console.error('Error creating absence:', error);
|
||||
return NextResponse.json({ error: 'Failed to create absence' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { db } from '@/lib/db';
|
||||
|
||||
// GET /api/substitutes/assignments/[id] - Get a single assignment
|
||||
export async function GET(_req: NextRequest, { params }: { params: Promise<{ id: string }> }) {
|
||||
try {
|
||||
const { id } = await params;
|
||||
const assignment = await db.substitutionAssignment.findUnique({
|
||||
where: { id },
|
||||
include: {
|
||||
absence: {
|
||||
include: {
|
||||
teacher: { select: { id: true, firstName: true, lastName: true } },
|
||||
},
|
||||
},
|
||||
substitute: { select: { id: true, firstName: true, lastName: true, rating: true } },
|
||||
classGroup: { select: { id: true, name: true } },
|
||||
subject: { select: { id: true, name: true } },
|
||||
},
|
||||
});
|
||||
|
||||
if (!assignment) {
|
||||
return NextResponse.json({ error: 'Assignment not found' }, { status: 404 });
|
||||
}
|
||||
|
||||
return NextResponse.json(assignment);
|
||||
} catch (error) {
|
||||
console.error('Error fetching assignment:', error);
|
||||
return NextResponse.json({ error: 'Failed to fetch assignment' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
// PUT /api/substitutes/assignments/[id] - Update an assignment
|
||||
export async function PUT(req: NextRequest, { params }: { params: Promise<{ id: string }> }) {
|
||||
try {
|
||||
const { id } = await params;
|
||||
const body = await req.json();
|
||||
|
||||
const existing = await db.substitutionAssignment.findUnique({ where: { id } });
|
||||
if (!existing) {
|
||||
return NextResponse.json({ error: 'Assignment not found' }, { status: 404 });
|
||||
}
|
||||
|
||||
const data: Record<string, unknown> = {};
|
||||
const simpleFields = ['status', 'notes', 'room', 'period', 'substituteId', 'classGroupId', 'subjectId'];
|
||||
|
||||
for (const field of simpleFields) {
|
||||
if (body[field] !== undefined) data[field] = body[field];
|
||||
}
|
||||
|
||||
if (body.date) data.date = new Date(body.date);
|
||||
|
||||
const assignment = await db.substitutionAssignment.update({
|
||||
where: { id },
|
||||
data,
|
||||
include: {
|
||||
absence: {
|
||||
include: {
|
||||
teacher: { select: { id: true, firstName: true, lastName: true } },
|
||||
},
|
||||
},
|
||||
substitute: { select: { id: true, firstName: true, lastName: true, rating: true } },
|
||||
classGroup: { select: { id: true, name: true } },
|
||||
subject: { select: { id: true, name: true } },
|
||||
},
|
||||
});
|
||||
|
||||
// Check if all assignments for this absence are covered
|
||||
if (body.status === 'confirmed' || body.status === 'completed') {
|
||||
const absence = await db.teacherAbsence.findUnique({
|
||||
where: { id: existing.absenceId },
|
||||
include: { assignments: true },
|
||||
});
|
||||
|
||||
if (absence && absence.assignments.every(a => a.status === 'confirmed' || a.status === 'completed')) {
|
||||
await db.teacherAbsence.update({
|
||||
where: { id: existing.absenceId },
|
||||
data: { status: 'covered' },
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return NextResponse.json(assignment);
|
||||
} catch (error) {
|
||||
console.error('Error updating assignment:', error);
|
||||
return NextResponse.json({ error: 'Failed to update assignment' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
// DELETE /api/substitutes/assignments/[id] - Delete an assignment
|
||||
export async function DELETE(_req: NextRequest, { params }: { params: Promise<{ id: string }> }) {
|
||||
try {
|
||||
const { id } = await params;
|
||||
|
||||
const existing = await db.substitutionAssignment.findUnique({ where: { id } });
|
||||
if (!existing) {
|
||||
return NextResponse.json({ error: 'Assignment not found' }, { status: 404 });
|
||||
}
|
||||
|
||||
// Decrement the substitute's totalAssignments count
|
||||
await db.substituteTeacher.update({
|
||||
where: { id: existing.substituteId },
|
||||
data: { totalAssignments: { decrement: 1 } },
|
||||
});
|
||||
|
||||
await db.substitutionAssignment.delete({ where: { id } });
|
||||
|
||||
// Check if the absence is still covered
|
||||
const absence = await db.teacherAbsence.findUnique({
|
||||
where: { id: existing.absenceId },
|
||||
include: { assignments: true },
|
||||
});
|
||||
|
||||
if (absence && absence.assignments.length > 0 && !absence.assignments.every(a => a.status === 'confirmed' || a.status === 'completed')) {
|
||||
await db.teacherAbsence.update({
|
||||
where: { id: existing.absenceId },
|
||||
data: { status: 'reported' },
|
||||
});
|
||||
}
|
||||
|
||||
return NextResponse.json({ success: true });
|
||||
} catch (error) {
|
||||
console.error('Error deleting assignment:', error);
|
||||
return NextResponse.json({ error: 'Failed to delete assignment' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { db } from '@/lib/db';
|
||||
|
||||
// GET /api/substitutes/assignments - List substitution assignments
|
||||
export async function GET(req: NextRequest) {
|
||||
try {
|
||||
const { searchParams } = new URL(req.url);
|
||||
const schoolId = searchParams.get('schoolId');
|
||||
const substituteId = searchParams.get('substituteId');
|
||||
const absenceId = searchParams.get('absenceId');
|
||||
const status = searchParams.get('status');
|
||||
const startDate = searchParams.get('startDate');
|
||||
const endDate = searchParams.get('endDate');
|
||||
const classGroupId = searchParams.get('classGroupId');
|
||||
|
||||
if (!schoolId) {
|
||||
return NextResponse.json({ error: 'schoolId is required' }, { status: 400 });
|
||||
}
|
||||
|
||||
const where: Record<string, unknown> = { schoolId };
|
||||
|
||||
if (substituteId) where.substituteId = substituteId;
|
||||
if (absenceId) where.absenceId = absenceId;
|
||||
if (status) where.status = status;
|
||||
if (classGroupId) where.classGroupId = classGroupId;
|
||||
|
||||
if (startDate && endDate) {
|
||||
where.date = { gte: new Date(startDate), lte: new Date(endDate) };
|
||||
} else if (startDate) {
|
||||
where.date = { gte: new Date(startDate) };
|
||||
}
|
||||
|
||||
const assignments = await db.substitutionAssignment.findMany({
|
||||
where,
|
||||
include: {
|
||||
absence: {
|
||||
include: {
|
||||
teacher: { select: { id: true, firstName: true, lastName: true } },
|
||||
},
|
||||
},
|
||||
substitute: { select: { id: true, firstName: true, lastName: true, rating: true } },
|
||||
classGroup: { select: { id: true, name: true } },
|
||||
subject: { select: { id: true, name: true } },
|
||||
},
|
||||
orderBy: { date: 'asc' },
|
||||
});
|
||||
|
||||
return NextResponse.json(assignments);
|
||||
} catch (error) {
|
||||
console.error('Error fetching assignments:', error);
|
||||
return NextResponse.json({ error: 'Failed to fetch assignments' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
// POST /api/substitutes/assignments - Create a substitution assignment
|
||||
export async function POST(req: NextRequest) {
|
||||
try {
|
||||
const body = await req.json();
|
||||
const {
|
||||
schoolId, absenceId, substituteId, classGroupId, subjectId,
|
||||
date, period, room, status, notes,
|
||||
} = body;
|
||||
|
||||
if (!schoolId || !absenceId || !substituteId || !date) {
|
||||
return NextResponse.json({ error: 'schoolId, absenceId, substituteId, and date are required' }, { status: 400 });
|
||||
}
|
||||
|
||||
const assignment = await db.substitutionAssignment.create({
|
||||
data: {
|
||||
schoolId,
|
||||
absenceId,
|
||||
substituteId,
|
||||
classGroupId: classGroupId || null,
|
||||
subjectId: subjectId || null,
|
||||
date: new Date(date),
|
||||
period: period || null,
|
||||
room: room || null,
|
||||
status: status || 'pending',
|
||||
notes: notes || null,
|
||||
},
|
||||
include: {
|
||||
absence: {
|
||||
include: {
|
||||
teacher: { select: { id: true, firstName: true, lastName: true } },
|
||||
},
|
||||
},
|
||||
substitute: { select: { id: true, firstName: true, lastName: true, rating: true } },
|
||||
classGroup: { select: { id: true, name: true } },
|
||||
subject: { select: { id: true, name: true } },
|
||||
},
|
||||
});
|
||||
|
||||
// Update the substitute's totalAssignments count
|
||||
await db.substituteTeacher.update({
|
||||
where: { id: substituteId },
|
||||
data: { totalAssignments: { increment: 1 } },
|
||||
});
|
||||
|
||||
// Check if all assignments for this absence are covered
|
||||
const absence = await db.teacherAbsence.findUnique({
|
||||
where: { id: absenceId },
|
||||
include: { assignments: true },
|
||||
});
|
||||
|
||||
if (absence && absence.assignments.length > 0 && absence.assignments.every(a => a.status === 'confirmed' || a.status === 'completed')) {
|
||||
await db.teacherAbsence.update({
|
||||
where: { id: absenceId },
|
||||
data: { status: 'covered' },
|
||||
});
|
||||
}
|
||||
|
||||
return NextResponse.json(assignment, { status: 201 });
|
||||
} catch (error) {
|
||||
console.error('Error creating assignment:', error);
|
||||
return NextResponse.json({ error: 'Failed to create assignment' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { db } from '@/lib/db';
|
||||
|
||||
// GET /api/substitutes - List substitute teachers
|
||||
export async function GET(req: NextRequest) {
|
||||
try {
|
||||
const { searchParams } = new URL(req.url);
|
||||
const schoolId = searchParams.get('schoolId');
|
||||
const isActive = searchParams.get('isActive');
|
||||
const search = searchParams.get('search');
|
||||
|
||||
if (!schoolId) {
|
||||
return NextResponse.json({ error: 'schoolId is required' }, { status: 400 });
|
||||
}
|
||||
|
||||
const where: Record<string, unknown> = {
|
||||
schoolId,
|
||||
deletedAt: null,
|
||||
};
|
||||
|
||||
if (isActive !== null && isActive !== undefined) {
|
||||
where.isActive = isActive === 'true';
|
||||
}
|
||||
|
||||
if (search) {
|
||||
where.OR = [
|
||||
{ firstName: { contains: search } },
|
||||
{ lastName: { contains: search } },
|
||||
{ email: { contains: search } },
|
||||
];
|
||||
}
|
||||
|
||||
const substitutes = await db.substituteTeacher.findMany({
|
||||
where,
|
||||
include: {
|
||||
user: { select: { id: true, firstName: true, lastName: true, email: true } },
|
||||
assignments: {
|
||||
where: { status: { in: ['pending', 'confirmed'] } },
|
||||
select: { id: true, date: true, status: true },
|
||||
orderBy: { date: 'asc' },
|
||||
take: 5,
|
||||
},
|
||||
},
|
||||
orderBy: { lastName: 'asc' },
|
||||
});
|
||||
|
||||
return NextResponse.json(substitutes);
|
||||
} catch (error) {
|
||||
console.error('Error fetching substitutes:', error);
|
||||
return NextResponse.json({ error: 'Failed to fetch substitutes' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
// POST /api/substitutes - Create a substitute teacher
|
||||
export async function POST(req: NextRequest) {
|
||||
try {
|
||||
const body = await req.json();
|
||||
const {
|
||||
schoolId, userId, firstName, lastName, email, phone,
|
||||
qualifications, subjects, gradeLevels, availability,
|
||||
maxDaysPerWeek, notes, isActive,
|
||||
} = body;
|
||||
|
||||
if (!schoolId || !firstName || !lastName) {
|
||||
return NextResponse.json({ error: 'schoolId, firstName, and lastName are required' }, { status: 400 });
|
||||
}
|
||||
|
||||
const substitute = await db.substituteTeacher.create({
|
||||
data: {
|
||||
schoolId,
|
||||
userId: userId || null,
|
||||
firstName,
|
||||
lastName,
|
||||
email: email || null,
|
||||
phone: phone || null,
|
||||
qualifications: qualifications ? JSON.stringify(qualifications) : null,
|
||||
subjects: subjects ? JSON.stringify(subjects) : null,
|
||||
gradeLevels: gradeLevels ? JSON.stringify(gradeLevels) : null,
|
||||
availability: availability ? JSON.stringify(availability) : null,
|
||||
maxDaysPerWeek: maxDaysPerWeek ?? 5,
|
||||
notes: notes || null,
|
||||
isActive: isActive ?? true,
|
||||
},
|
||||
include: {
|
||||
user: { select: { id: true, firstName: true, lastName: true, email: true } },
|
||||
},
|
||||
});
|
||||
|
||||
return NextResponse.json(substitute, { status: 201 });
|
||||
} catch (error) {
|
||||
console.error('Error creating substitute:', error);
|
||||
return NextResponse.json({ error: 'Failed to create substitute' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
Regular → Executable
File mode changed.
Regular → Executable
File mode changed.
Regular → Executable
File mode changed.
Reference in new issue
Block a user