Round 17: QR Codes, Gamification Badges, Auto-Award, Badge Management
This commit is contained in:
1 parent
6174f127f9
commit
cba8a8e724
23 files changed
+4028
-1
No files matched your search
@@ -0,0 +1,35 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { db } from '@/lib/db';
|
||||
import { getSession } from '@/lib/auth';
|
||||
import { seedDefaultBadges } from '@/lib/badge-check';
|
||||
|
||||
function isTeacherOrAdmin(role: string | undefined): boolean {
|
||||
return role === 'TEACHER' || role === 'SCHOOL_ADMIN' || role === 'SUPER_ADMIN';
|
||||
}
|
||||
|
||||
export async function POST(request: Request) {
|
||||
try {
|
||||
const session = await getSession();
|
||||
if (!session) return NextResponse.json({ error: 'Not authenticated' }, { status: 401 });
|
||||
if (!isTeacherOrAdmin(session.user?.role)) return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
||||
|
||||
const body = await request.json();
|
||||
const { schoolId, studentId } = body;
|
||||
|
||||
if (!schoolId || !studentId) {
|
||||
return NextResponse.json({ error: 'Missing schoolId or studentId' }, { status: 400 });
|
||||
}
|
||||
|
||||
// Seed default badges if needed
|
||||
await seedDefaultBadges(schoolId);
|
||||
|
||||
// Check and award
|
||||
const { checkAndAwardBadges } = await import('@/lib/badge-check');
|
||||
const awarded = await checkAndAwardBadges(schoolId, studentId);
|
||||
|
||||
return NextResponse.json({ awarded });
|
||||
} catch (error) {
|
||||
console.error('Badge-check POST error:', error);
|
||||
return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { db } from '@/lib/db';
|
||||
import { getSession } from '@/lib/auth';
|
||||
import { getBadgeProgress } from '@/lib/badge-check';
|
||||
|
||||
function isTeacherOrAdmin(role: string | undefined): boolean {
|
||||
return role === 'TEACHER' || role === 'SCHOOL_ADMIN' || role === 'SUPER_ADMIN';
|
||||
}
|
||||
|
||||
export async function GET(request: Request) {
|
||||
try {
|
||||
const session = await getSession();
|
||||
if (!session) return NextResponse.json({ error: 'Not authenticated' }, { status: 401 });
|
||||
if (!isTeacherOrAdmin(session.user?.role)) return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
||||
|
||||
const { searchParams } = new URL(request.url);
|
||||
const schoolId = searchParams.get('schoolId');
|
||||
const studentId = searchParams.get('studentId');
|
||||
|
||||
if (!schoolId || !studentId) {
|
||||
return NextResponse.json({ error: 'Missing schoolId or studentId' }, { status: 400 });
|
||||
}
|
||||
|
||||
// Seed default badges if needed
|
||||
const { seedDefaultBadges } = await import('@/lib/badge-check');
|
||||
await seedDefaultBadges(schoolId);
|
||||
|
||||
const progress = await getBadgeProgress(schoolId, studentId);
|
||||
|
||||
return NextResponse.json(progress);
|
||||
} catch (error) {
|
||||
console.error('Badge-progress GET error:', error);
|
||||
return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { getSession } from '@/lib/auth';
|
||||
import { seedDefaultBadges } from '@/lib/badge-check';
|
||||
|
||||
function isTeacherOrAdmin(role: string | undefined): boolean {
|
||||
return role === 'TEACHER' || role === 'SCHOOL_ADMIN' || role === 'SUPER_ADMIN';
|
||||
}
|
||||
|
||||
export async function POST(request: Request) {
|
||||
try {
|
||||
const session = await getSession();
|
||||
if (!session) return NextResponse.json({ error: 'Not authenticated' }, { status: 401 });
|
||||
if (!isTeacherOrAdmin(session.user?.role)) return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
||||
|
||||
const body = await request.json();
|
||||
const { schoolId } = body;
|
||||
|
||||
if (!schoolId) {
|
||||
return NextResponse.json({ error: 'Missing schoolId' }, { status: 400 });
|
||||
}
|
||||
|
||||
await seedDefaultBadges(schoolId);
|
||||
|
||||
return NextResponse.json({ success: true });
|
||||
} catch (error) {
|
||||
console.error('Badge-seed POST error:', error);
|
||||
return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { db } from '@/lib/db';
|
||||
import { getSession } from '@/lib/auth';
|
||||
|
||||
function isTeacherOrAdmin(role: string | undefined): boolean {
|
||||
return role === 'TEACHER' || role === 'SCHOOL_ADMIN' || role === 'SUPER_ADMIN';
|
||||
}
|
||||
|
||||
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 });
|
||||
if (!isTeacherOrAdmin(session.user?.role)) return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
||||
|
||||
const { id } = await params;
|
||||
const badge = await db.badge.findUnique({
|
||||
where: { id, deletedAt: null },
|
||||
include: {
|
||||
_count: { select: { studentBadges: true } },
|
||||
},
|
||||
});
|
||||
|
||||
if (!badge) return NextResponse.json({ error: 'Badge not found' }, { status: 404 });
|
||||
return NextResponse.json(badge);
|
||||
} catch (error) {
|
||||
console.error('Badge GET 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 });
|
||||
if (!isTeacherOrAdmin(session.user?.role)) return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
||||
|
||||
const { id } = await params;
|
||||
const body = await request.json();
|
||||
|
||||
const existing = await db.badge.findUnique({ where: { id, deletedAt: null } });
|
||||
if (!existing) return NextResponse.json({ error: 'Badge not found' }, { status: 404 });
|
||||
|
||||
const updated = await db.badge.update({
|
||||
where: { id },
|
||||
data: {
|
||||
name: body.name ?? existing.name,
|
||||
description: body.description ?? existing.description,
|
||||
icon: body.icon ?? existing.icon,
|
||||
color: body.color ?? existing.color,
|
||||
category: body.category ?? existing.category,
|
||||
requirementType: body.requirementType ?? existing.requirementType,
|
||||
requirementValue: body.requirementValue ?? existing.requirementValue,
|
||||
isAuto: body.isAuto ?? existing.isAuto,
|
||||
},
|
||||
});
|
||||
|
||||
return NextResponse.json(updated);
|
||||
} catch (error) {
|
||||
console.error('Badge 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 });
|
||||
if (!isTeacherOrAdmin(session.user?.role)) return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
||||
|
||||
const { id } = await params;
|
||||
|
||||
const existing = await db.badge.findUnique({ where: { id, deletedAt: null } });
|
||||
if (!existing) return NextResponse.json({ error: 'Badge not found' }, { status: 404 });
|
||||
|
||||
// Soft delete
|
||||
await db.badge.update({ where: { id }, data: { deletedAt: new Date() } });
|
||||
|
||||
return NextResponse.json({ success: true });
|
||||
} catch (error) {
|
||||
console.error('Badge DELETE error:', error);
|
||||
return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { z } from 'zod';
|
||||
import { db } from '@/lib/db';
|
||||
import { getSession } from '@/lib/auth';
|
||||
|
||||
const categoryEnum = z.enum(['competency', 'attendance', 'behavior', 'achievement', 'milestone']);
|
||||
const reqTypeEnum = z.enum(['mastery_level', 'attendance_rate', 'behavior_count', 'progress_entries', 'custom']);
|
||||
|
||||
const createBadgeSchema = z.object({
|
||||
schoolId: z.string().min(1),
|
||||
name: z.string().min(1).max(100),
|
||||
description: z.string().max(500).optional().nullable(),
|
||||
icon: z.string().min(1).max(50),
|
||||
color: z.string().regex(/^#[0-9a-fA-F]{6}$/).default('#10b981'),
|
||||
category: categoryEnum,
|
||||
requirementType: reqTypeEnum,
|
||||
requirementValue: z.number().int().min(0).optional().nullable(),
|
||||
isAuto: z.boolean().default(true),
|
||||
});
|
||||
|
||||
function isTeacherOrAdmin(role: string | undefined): boolean {
|
||||
return role === 'TEACHER' || role === 'SCHOOL_ADMIN' || role === 'SUPER_ADMIN';
|
||||
}
|
||||
|
||||
export async function GET(request: Request) {
|
||||
try {
|
||||
const session = await getSession();
|
||||
if (!session) return NextResponse.json({ error: 'Not authenticated' }, { status: 401 });
|
||||
if (!isTeacherOrAdmin(session.user?.role)) return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
||||
|
||||
const { searchParams } = new URL(request.url);
|
||||
const schoolIdParam = searchParams.get('schoolId');
|
||||
const categoryParam = searchParams.get('category');
|
||||
|
||||
let schoolId: string | undefined;
|
||||
if (session.user?.role === 'SCHOOL_ADMIN') {
|
||||
schoolId = session.user.schoolId ?? undefined;
|
||||
} else {
|
||||
schoolId = schoolIdParam ?? session.user?.schoolId ?? undefined;
|
||||
}
|
||||
|
||||
if (!schoolId) return NextResponse.json([]);
|
||||
|
||||
const where: Record<string, unknown> = { schoolId, deletedAt: null };
|
||||
if (categoryParam) where.category = categoryParam;
|
||||
|
||||
const badges = await db.badge.findMany({
|
||||
where,
|
||||
orderBy: [{ category: 'asc' }, { name: 'asc' }],
|
||||
include: {
|
||||
_count: { select: { studentBadges: true } },
|
||||
},
|
||||
});
|
||||
|
||||
return NextResponse.json(badges);
|
||||
} catch (error) {
|
||||
console.error('Badges GET error:', error);
|
||||
return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
export async function POST(request: Request) {
|
||||
try {
|
||||
const session = await getSession();
|
||||
if (!session) return NextResponse.json({ error: 'Not authenticated' }, { status: 401 });
|
||||
if (!isTeacherOrAdmin(session.user?.role)) return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
||||
|
||||
const body = await request.json();
|
||||
const parsed = createBadgeSchema.safeParse(body);
|
||||
if (!parsed.success) {
|
||||
return NextResponse.json({ error: 'Validation failed', details: parsed.error.issues }, { status: 400 });
|
||||
}
|
||||
|
||||
const { schoolId } = parsed.data;
|
||||
|
||||
const school = await db.school.findUnique({ where: { id: schoolId } });
|
||||
if (!school) return NextResponse.json({ error: 'School not found' }, { status: 404 });
|
||||
if (session.user?.role === 'SCHOOL_ADMIN' && session.user.schoolId && school.id !== session.user.schoolId) {
|
||||
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
||||
}
|
||||
|
||||
const badge = await db.badge.create({
|
||||
data: {
|
||||
schoolId,
|
||||
name: parsed.data.name,
|
||||
description: parsed.data.description ?? null,
|
||||
icon: parsed.data.icon,
|
||||
color: parsed.data.color,
|
||||
category: parsed.data.category,
|
||||
requirementType: parsed.data.requirementType,
|
||||
requirementValue: parsed.data.requirementValue ?? null,
|
||||
isAuto: parsed.data.isAuto,
|
||||
},
|
||||
});
|
||||
|
||||
return NextResponse.json(badge, { status: 201 });
|
||||
} catch (error) {
|
||||
console.error('Badges POST error:', error);
|
||||
return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { db } from '@/lib/db';
|
||||
import { getSession } from '@/lib/auth';
|
||||
|
||||
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 classGroup = await db.classGroup.findFirst({
|
||||
where: { id, deletedAt: null },
|
||||
});
|
||||
|
||||
if (!classGroup) {
|
||||
return NextResponse.json({ error: 'Class not found' }, { status: 404 });
|
||||
}
|
||||
|
||||
if (session.user?.role !== 'SUPER_ADMIN' && session.user?.schoolId !== classGroup.schoolId) {
|
||||
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
||||
}
|
||||
|
||||
// Get enrolled students
|
||||
const enrollments = await db.enrollment.findMany({
|
||||
where: { classGroupId: id, endDate: null },
|
||||
include: {
|
||||
student: {
|
||||
select: { id: true, firstName: true, lastName: true },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const students = enrollments.map((e) => e.student);
|
||||
|
||||
// Parse existing seating order
|
||||
let seatingOrder: Array<{ studentId: string; row: number; col: number }> = [];
|
||||
if (classGroup.seatingOrder) {
|
||||
try {
|
||||
const parsed = JSON.parse(classGroup.seatingOrder);
|
||||
if (Array.isArray(parsed)) {
|
||||
// Check if it's the new format [{ studentId, row, col }]
|
||||
if (parsed.length > 0 && parsed[0].studentId !== undefined && parsed[0].row !== undefined) {
|
||||
seatingOrder = parsed;
|
||||
} else {
|
||||
// Legacy format: just array of student IDs, convert to grid
|
||||
seatingOrder = parsed.map((studentId: string, i: number) => ({
|
||||
studentId,
|
||||
row: Math.floor(i / 5),
|
||||
col: i % 5,
|
||||
}));
|
||||
}
|
||||
}
|
||||
} catch { /* ignore parse errors */ }
|
||||
}
|
||||
|
||||
return NextResponse.json({ students, seatingOrder, classId: id });
|
||||
} catch (error) {
|
||||
console.error('Seating GET 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 classGroup = await db.classGroup.findFirst({
|
||||
where: { id, deletedAt: null },
|
||||
});
|
||||
|
||||
if (!classGroup) {
|
||||
return NextResponse.json({ error: 'Class not found' }, { status: 404 });
|
||||
}
|
||||
|
||||
if (session.user?.role !== 'SUPER_ADMIN' && session.user?.schoolId !== classGroup.schoolId) {
|
||||
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
||||
}
|
||||
|
||||
const body = await request.json();
|
||||
const { seatingOrder } = body;
|
||||
|
||||
if (!Array.isArray(seatingOrder)) {
|
||||
return NextResponse.json({ error: 'seatingOrder must be an array' }, { status: 400 });
|
||||
}
|
||||
|
||||
await db.classGroup.update({
|
||||
where: { id },
|
||||
data: { seatingOrder: JSON.stringify(seatingOrder) },
|
||||
});
|
||||
|
||||
return NextResponse.json({ success: true });
|
||||
} catch (error) {
|
||||
console.error('Seating PUT error:', error);
|
||||
return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { db } from '@/lib/db';
|
||||
import { getSession } from '@/lib/auth';
|
||||
|
||||
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 record = await db.healthRecord.findFirst({
|
||||
where: { id, deletedAt: null },
|
||||
include: {
|
||||
student: {
|
||||
select: { id: true, firstName: true, lastName: true },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!record) {
|
||||
return NextResponse.json({ error: 'Not found' }, { status: 404 });
|
||||
}
|
||||
|
||||
if (session.user?.role !== 'SUPER_ADMIN' && session.user?.schoolId !== record.schoolId) {
|
||||
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
||||
}
|
||||
|
||||
return NextResponse.json(record);
|
||||
} catch (error) {
|
||||
console.error('HealthRecord GET [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 existing = await db.healthRecord.findFirst({
|
||||
where: { id, deletedAt: null },
|
||||
});
|
||||
|
||||
if (!existing) {
|
||||
return NextResponse.json({ error: 'Not found' }, { status: 404 });
|
||||
}
|
||||
|
||||
if (session.user?.role !== 'SUPER_ADMIN' && session.user?.schoolId !== existing.schoolId) {
|
||||
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
||||
}
|
||||
|
||||
const body = await request.json();
|
||||
const {
|
||||
bloodType,
|
||||
allergies,
|
||||
medications,
|
||||
conditions,
|
||||
doctorName,
|
||||
doctorPhone,
|
||||
insuranceNumber,
|
||||
insuranceProvider,
|
||||
lastCheckup,
|
||||
notes,
|
||||
isConfidential,
|
||||
} = body;
|
||||
|
||||
const updated = await db.healthRecord.update({
|
||||
where: { id },
|
||||
data: {
|
||||
...(bloodType !== undefined && { bloodType: bloodType || null }),
|
||||
...(allergies !== undefined && { allergies: allergies ? JSON.stringify(allergies) : null }),
|
||||
...(medications !== undefined && { medications: medications ? JSON.stringify(medications) : null }),
|
||||
...(conditions !== undefined && { conditions: conditions ? JSON.stringify(conditions) : null }),
|
||||
...(doctorName !== undefined && { doctorName: doctorName || null }),
|
||||
...(doctorPhone !== undefined && { doctorPhone: doctorPhone || null }),
|
||||
...(insuranceNumber !== undefined && { insuranceNumber: insuranceNumber || null }),
|
||||
...(insuranceProvider !== undefined && { insuranceProvider: insuranceProvider || null }),
|
||||
...(lastCheckup !== undefined && { lastCheckup: lastCheckup ? new Date(lastCheckup) : null }),
|
||||
...(notes !== undefined && { notes }),
|
||||
...(isConfidential !== undefined && { isConfidential }),
|
||||
},
|
||||
include: {
|
||||
student: {
|
||||
select: { id: true, firstName: true, lastName: true },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return NextResponse.json(updated);
|
||||
} catch (error) {
|
||||
console.error('HealthRecord PUT [id] 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 existing = await db.healthRecord.findFirst({
|
||||
where: { id, deletedAt: null },
|
||||
});
|
||||
|
||||
if (!existing) {
|
||||
return NextResponse.json({ error: 'Not found' }, { status: 404 });
|
||||
}
|
||||
|
||||
if (session.user?.role !== 'SUPER_ADMIN' && session.user?.schoolId !== existing.schoolId) {
|
||||
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
||||
}
|
||||
|
||||
// Soft delete
|
||||
await db.healthRecord.update({
|
||||
where: { id },
|
||||
data: { deletedAt: new Date() },
|
||||
});
|
||||
|
||||
return NextResponse.json({ success: true });
|
||||
} catch (error) {
|
||||
console.error('HealthRecord DELETE [id] error:', error);
|
||||
return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { db } from '@/lib/db';
|
||||
import { getSession } from '@/lib/auth';
|
||||
|
||||
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');
|
||||
const studentId = searchParams.get('studentId');
|
||||
|
||||
if (!schoolId) {
|
||||
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,
|
||||
deletedAt: null,
|
||||
};
|
||||
|
||||
if (studentId) where.studentId = studentId;
|
||||
|
||||
const records = await db.healthRecord.findMany({
|
||||
where,
|
||||
orderBy: [{ createdAt: 'desc' }],
|
||||
include: {
|
||||
student: {
|
||||
select: { id: true, firstName: true, lastName: true },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return NextResponse.json(records);
|
||||
} catch (error) {
|
||||
console.error('HealthRecord GET error:', error);
|
||||
return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
bloodType,
|
||||
allergies,
|
||||
medications,
|
||||
conditions,
|
||||
doctorName,
|
||||
doctorPhone,
|
||||
insuranceNumber,
|
||||
insuranceProvider,
|
||||
lastCheckup,
|
||||
notes,
|
||||
isConfidential,
|
||||
isDemo,
|
||||
} = body;
|
||||
|
||||
if (!schoolId || !studentId) {
|
||||
return NextResponse.json(
|
||||
{ error: 'schoolId and studentId are required' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
if (session.user?.role !== 'SUPER_ADMIN' && session.user?.schoolId !== schoolId) {
|
||||
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
||||
}
|
||||
|
||||
const record = await db.healthRecord.create({
|
||||
data: {
|
||||
schoolId,
|
||||
studentId,
|
||||
bloodType: bloodType || null,
|
||||
allergies: allergies ? JSON.stringify(allergies) : null,
|
||||
medications: medications ? JSON.stringify(medications) : null,
|
||||
conditions: conditions ? JSON.stringify(conditions) : null,
|
||||
doctorName: doctorName || null,
|
||||
doctorPhone: doctorPhone || null,
|
||||
insuranceNumber: insuranceNumber || null,
|
||||
insuranceProvider: insuranceProvider || null,
|
||||
lastCheckup: lastCheckup ? new Date(lastCheckup) : null,
|
||||
notes: notes || null,
|
||||
isConfidential: isConfidential ?? true,
|
||||
isDemo: isDemo ?? false,
|
||||
},
|
||||
include: {
|
||||
student: {
|
||||
select: { id: true, firstName: true, lastName: true },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return NextResponse.json(record, { status: 201 });
|
||||
} catch (error) {
|
||||
console.error('HealthRecord POST error:', error);
|
||||
return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { db } from '@/lib/db';
|
||||
import { getSession } from '@/lib/auth';
|
||||
|
||||
function isTeacherOrAdmin(role: string | undefined): boolean {
|
||||
return role === 'TEACHER' || role === 'SCHOOL_ADMIN' || role === 'SUPER_ADMIN';
|
||||
}
|
||||
|
||||
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 });
|
||||
if (!isTeacherOrAdmin(session.user?.role)) return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
||||
|
||||
const { id } = await params;
|
||||
const studentBadge = await db.studentBadge.findUnique({
|
||||
where: { id },
|
||||
include: {
|
||||
badge: true,
|
||||
student: { select: { id: true, firstName: true, lastName: true } },
|
||||
awardedByUser: { select: { id: true, firstName: true, lastName: true } },
|
||||
},
|
||||
});
|
||||
|
||||
if (!studentBadge) return NextResponse.json({ error: 'Student badge not found' }, { status: 404 });
|
||||
return NextResponse.json(studentBadge);
|
||||
} catch (error) {
|
||||
console.error('StudentBadge GET 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 });
|
||||
if (!isTeacherOrAdmin(session.user?.role)) return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
||||
|
||||
const { id } = await params;
|
||||
|
||||
const existing = await db.studentBadge.findUnique({ where: { id } });
|
||||
if (!existing) return NextResponse.json({ error: 'Student badge not found' }, { status: 404 });
|
||||
|
||||
await db.studentBadge.delete({ where: { id } });
|
||||
|
||||
return NextResponse.json({ success: true });
|
||||
} catch (error) {
|
||||
console.error('StudentBadge DELETE error:', error);
|
||||
return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { z } from 'zod';
|
||||
import { db } from '@/lib/db';
|
||||
import { getSession } from '@/lib/auth';
|
||||
|
||||
const awardBadgeSchema = z.object({
|
||||
schoolId: z.string().min(1),
|
||||
studentId: z.string().min(1),
|
||||
badgeId: z.string().min(1),
|
||||
notes: z.string().max(500).optional().nullable(),
|
||||
});
|
||||
|
||||
function isTeacherOrAdmin(role: string | undefined): boolean {
|
||||
return role === 'TEACHER' || role === 'SCHOOL_ADMIN' || role === 'SUPER_ADMIN';
|
||||
}
|
||||
|
||||
export async function GET(request: Request) {
|
||||
try {
|
||||
const session = await getSession();
|
||||
if (!session) return NextResponse.json({ error: 'Not authenticated' }, { status: 401 });
|
||||
if (!isTeacherOrAdmin(session.user?.role)) return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
||||
|
||||
const { searchParams } = new URL(request.url);
|
||||
const schoolIdParam = searchParams.get('schoolId');
|
||||
const studentIdParam = searchParams.get('studentId');
|
||||
const badgeIdParam = searchParams.get('badgeId');
|
||||
|
||||
let schoolId: string | undefined;
|
||||
if (session.user?.role === 'SCHOOL_ADMIN') {
|
||||
schoolId = session.user.schoolId ?? undefined;
|
||||
} else {
|
||||
schoolId = schoolIdParam ?? session.user?.schoolId ?? undefined;
|
||||
}
|
||||
|
||||
if (!schoolId) return NextResponse.json([]);
|
||||
|
||||
const where: Record<string, unknown> = { schoolId };
|
||||
if (studentIdParam) where.studentId = studentIdParam;
|
||||
if (badgeIdParam) where.badgeId = badgeIdParam;
|
||||
|
||||
const studentBadges = await db.studentBadge.findMany({
|
||||
where,
|
||||
orderBy: { awardedAt: 'desc' },
|
||||
include: {
|
||||
badge: true,
|
||||
student: { select: { id: true, firstName: true, lastName: true } },
|
||||
awardedByUser: { select: { id: true, firstName: true, lastName: true } },
|
||||
},
|
||||
});
|
||||
|
||||
return NextResponse.json(studentBadges);
|
||||
} catch (error) {
|
||||
console.error('StudentBadges GET error:', error);
|
||||
return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
export async function POST(request: Request) {
|
||||
try {
|
||||
const session = await getSession();
|
||||
if (!session) return NextResponse.json({ error: 'Not authenticated' }, { status: 401 });
|
||||
if (!isTeacherOrAdmin(session.user?.role)) return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
||||
|
||||
const body = await request.json();
|
||||
const parsed = awardBadgeSchema.safeParse(body);
|
||||
if (!parsed.success) {
|
||||
return NextResponse.json({ error: 'Validation failed', details: parsed.error.issues }, { status: 400 });
|
||||
}
|
||||
|
||||
const { schoolId, studentId, badgeId } = parsed.data;
|
||||
|
||||
// Verify badge exists
|
||||
const badge = await db.badge.findUnique({ where: { id: badgeId, deletedAt: null } });
|
||||
if (!badge) return NextResponse.json({ error: 'Badge not found' }, { status: 404 });
|
||||
|
||||
// Verify student exists
|
||||
const student = await db.student.findUnique({ where: { id: studentId, deletedAt: null } });
|
||||
if (!student) return NextResponse.json({ error: 'Student not found' }, { status: 404 });
|
||||
|
||||
// Check if already awarded
|
||||
const existing = await db.studentBadge.findUnique({
|
||||
where: { studentId_badgeId: { studentId, badgeId } },
|
||||
});
|
||||
if (existing) {
|
||||
return NextResponse.json({ error: 'Badge already awarded to this student' }, { status: 409 });
|
||||
}
|
||||
|
||||
const studentBadge = await db.studentBadge.create({
|
||||
data: {
|
||||
schoolId,
|
||||
studentId,
|
||||
badgeId,
|
||||
awardedBy: session.userId,
|
||||
notes: parsed.data.notes ?? null,
|
||||
},
|
||||
include: {
|
||||
badge: true,
|
||||
student: { select: { id: true, firstName: true, lastName: true } },
|
||||
},
|
||||
});
|
||||
|
||||
return NextResponse.json(studentBadge, { status: 201 });
|
||||
} catch (error) {
|
||||
console.error('StudentBadges POST error:', error);
|
||||
return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { db } from '@/lib/db';
|
||||
import { getSession } from '@/lib/auth';
|
||||
|
||||
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 transport = await db.studentTransport.findFirst({
|
||||
where: { id, deletedAt: null },
|
||||
include: {
|
||||
student: {
|
||||
select: { id: true, firstName: true, lastName: true },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!transport) {
|
||||
return NextResponse.json({ error: 'Not found' }, { status: 404 });
|
||||
}
|
||||
|
||||
if (session.user?.role !== 'SUPER_ADMIN' && session.user?.schoolId !== transport.schoolId) {
|
||||
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
||||
}
|
||||
|
||||
return NextResponse.json(transport);
|
||||
} catch (error) {
|
||||
console.error('StudentTransport GET [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 existing = await db.studentTransport.findFirst({
|
||||
where: { id, deletedAt: null },
|
||||
});
|
||||
|
||||
if (!existing) {
|
||||
return NextResponse.json({ error: 'Not found' }, { status: 404 });
|
||||
}
|
||||
|
||||
if (session.user?.role !== 'SUPER_ADMIN' && session.user?.schoolId !== existing.schoolId) {
|
||||
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
||||
}
|
||||
|
||||
const body = await request.json();
|
||||
const {
|
||||
transportType,
|
||||
routeNumber,
|
||||
stopName,
|
||||
pickupTime,
|
||||
dropoffTime,
|
||||
driverName,
|
||||
driverPhone,
|
||||
distanceKm,
|
||||
notes,
|
||||
} = body;
|
||||
|
||||
const updated = await db.studentTransport.update({
|
||||
where: { id },
|
||||
data: {
|
||||
...(transportType !== undefined && { transportType }),
|
||||
...(routeNumber !== undefined && { routeNumber: routeNumber || null }),
|
||||
...(stopName !== undefined && { stopName: stopName || null }),
|
||||
...(pickupTime !== undefined && { pickupTime: pickupTime || null }),
|
||||
...(dropoffTime !== undefined && { dropoffTime: dropoffTime || null }),
|
||||
...(driverName !== undefined && { driverName: driverName || null }),
|
||||
...(driverPhone !== undefined && { driverPhone: driverPhone || null }),
|
||||
...(distanceKm !== undefined && { distanceKm: distanceKm != null ? distanceKm : null }),
|
||||
...(notes !== undefined && { notes }),
|
||||
},
|
||||
include: {
|
||||
student: {
|
||||
select: { id: true, firstName: true, lastName: true },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return NextResponse.json(updated);
|
||||
} catch (error) {
|
||||
console.error('StudentTransport PUT [id] 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 existing = await db.studentTransport.findFirst({
|
||||
where: { id, deletedAt: null },
|
||||
});
|
||||
|
||||
if (!existing) {
|
||||
return NextResponse.json({ error: 'Not found' }, { status: 404 });
|
||||
}
|
||||
|
||||
if (session.user?.role !== 'SUPER_ADMIN' && session.user?.schoolId !== existing.schoolId) {
|
||||
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
||||
}
|
||||
|
||||
// Soft delete
|
||||
await db.studentTransport.update({
|
||||
where: { id },
|
||||
data: { deletedAt: new Date() },
|
||||
});
|
||||
|
||||
return NextResponse.json({ success: true });
|
||||
} catch (error) {
|
||||
console.error('StudentTransport DELETE [id] error:', error);
|
||||
return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { db } from '@/lib/db';
|
||||
import { getSession } from '@/lib/auth';
|
||||
|
||||
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');
|
||||
const studentId = searchParams.get('studentId');
|
||||
|
||||
if (!schoolId) {
|
||||
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,
|
||||
deletedAt: null,
|
||||
};
|
||||
|
||||
if (studentId) where.studentId = studentId;
|
||||
|
||||
const transports = await db.studentTransport.findMany({
|
||||
where,
|
||||
orderBy: [{ createdAt: 'desc' }],
|
||||
include: {
|
||||
student: {
|
||||
select: { id: true, firstName: true, lastName: true },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return NextResponse.json(transports);
|
||||
} catch (error) {
|
||||
console.error('StudentTransport GET error:', error);
|
||||
return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
transportType,
|
||||
routeNumber,
|
||||
stopName,
|
||||
pickupTime,
|
||||
dropoffTime,
|
||||
driverName,
|
||||
driverPhone,
|
||||
distanceKm,
|
||||
notes,
|
||||
isDemo,
|
||||
} = body;
|
||||
|
||||
if (!schoolId || !studentId || !transportType) {
|
||||
return NextResponse.json(
|
||||
{ error: 'schoolId, studentId, and transportType are required' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
if (session.user?.role !== 'SUPER_ADMIN' && session.user?.schoolId !== schoolId) {
|
||||
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
||||
}
|
||||
|
||||
const transport = await db.studentTransport.create({
|
||||
data: {
|
||||
schoolId,
|
||||
studentId,
|
||||
transportType,
|
||||
routeNumber: routeNumber || null,
|
||||
stopName: stopName || null,
|
||||
pickupTime: pickupTime || null,
|
||||
dropoffTime: dropoffTime || null,
|
||||
driverName: driverName || null,
|
||||
driverPhone: driverPhone || null,
|
||||
distanceKm: distanceKm != null ? distanceKm : null,
|
||||
notes: notes || null,
|
||||
isDemo: isDemo ?? false,
|
||||
},
|
||||
include: {
|
||||
student: {
|
||||
select: { id: true, firstName: true, lastName: true },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return NextResponse.json(transport, { status: 201 });
|
||||
} catch (error) {
|
||||
console.error('StudentTransport POST error:', error);
|
||||
return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user