311 lines
8.6 KiB
TypeScript
311 lines
8.6 KiB
TypeScript
// CompetenceTrack — Notifications API (DB-backed)
|
|
import { NextResponse } from 'next/server';
|
|
import { db } from '@/lib/db';
|
|
import { getSession } from '@/lib/auth';
|
|
|
|
export async function GET() {
|
|
const session = await getSession();
|
|
if (!session?.user) {
|
|
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
|
}
|
|
|
|
const userId = session.user.id;
|
|
const schoolId = session.user.schoolId;
|
|
|
|
if (!schoolId) {
|
|
return NextResponse.json({
|
|
notifications: [],
|
|
unreadCount: 0,
|
|
});
|
|
}
|
|
|
|
// Fetch DB notifications for the current user
|
|
const notifications = await db.notification.findMany({
|
|
where: {
|
|
userId,
|
|
deletedAt: null,
|
|
},
|
|
orderBy: { createdAt: 'desc' },
|
|
take: 50,
|
|
});
|
|
|
|
const unreadCount = notifications.filter((n) => !n.isRead).length;
|
|
|
|
// Also generate dynamic notifications from existing data
|
|
const now = new Date();
|
|
const sevenDaysFromNow = new Date(now.getTime() + 7 * 24 * 60 * 60 * 1000);
|
|
const fourteenDaysAgo = new Date(now.getTime() - 14 * 24 * 60 * 60 * 1000);
|
|
const role = session.user.role;
|
|
|
|
// Get teacher's class IDs
|
|
const teacherClasses = await db.classGroupTeacher.findMany({
|
|
where: { userId },
|
|
select: { classGroupId: true },
|
|
});
|
|
const classGroupIds = teacherClasses.map((tc) => tc.classGroupId);
|
|
|
|
// For admin, get all classes in the school
|
|
let targetClassIds = classGroupIds;
|
|
if (role === 'SCHOOL_ADMIN' || role === 'SUPER_ADMIN') {
|
|
const schoolClasses = await db.classGroup.findMany({
|
|
where: { schoolId },
|
|
select: { id: true },
|
|
});
|
|
targetClassIds = schoolClasses.map((c) => c.id);
|
|
}
|
|
|
|
// For students, get their enrolled class IDs
|
|
if (role === 'STUDENT') {
|
|
const student = await db.student.findFirst({
|
|
where: { schoolId },
|
|
include: {
|
|
enrollments: {
|
|
where: { endDate: null },
|
|
select: { classGroupId: true },
|
|
},
|
|
},
|
|
});
|
|
if (student) {
|
|
targetClassIds = student.enrollments.map((e) => e.classGroupId);
|
|
}
|
|
}
|
|
|
|
// Upcoming assessments (next 7 days)
|
|
const upcomingAssessments: Array<{
|
|
id: string;
|
|
type: string;
|
|
title: string;
|
|
message: string;
|
|
isRead: boolean;
|
|
actionUrl: string | null;
|
|
relatedId: string | null;
|
|
createdAt: string;
|
|
}> = [];
|
|
|
|
if (targetClassIds.length > 0) {
|
|
const assessments = await db.assessment.findMany({
|
|
where: {
|
|
classGroupId: { in: targetClassIds },
|
|
date: {
|
|
gte: now,
|
|
lte: sevenDaysFromNow,
|
|
},
|
|
},
|
|
include: {
|
|
classGroup: { select: { name: true } },
|
|
subject: { select: { name: true } },
|
|
assessmentResults: { select: { id: true } },
|
|
},
|
|
orderBy: { date: 'asc' },
|
|
take: 10,
|
|
});
|
|
|
|
for (const a of assessments) {
|
|
const diffMs = new Date(a.date).getTime() - now.getTime();
|
|
const daysUntil = Math.max(0, Math.ceil(diffMs / (1000 * 60 * 60 * 24)));
|
|
// Check if we already have a DB notification for this assessment
|
|
const existing = notifications.find(
|
|
(n) => n.type === 'ASSESSMENT_DUE' && n.relatedId === a.id
|
|
);
|
|
if (!existing) {
|
|
upcomingAssessments.push({
|
|
id: `assessment-${a.id}`,
|
|
type: 'ASSESSMENT_DUE',
|
|
title: a.title,
|
|
message: `${a.classGroup.name} - ${a.subject.name}`,
|
|
isRead: false,
|
|
actionUrl: 'assessments',
|
|
relatedId: a.id,
|
|
createdAt: new Date().toISOString(),
|
|
});
|
|
}
|
|
}
|
|
|
|
// Missing observations (students with no progress in 14 days) - only for teachers
|
|
if (role !== 'STUDENT') {
|
|
const activeEnrollments = await db.enrollment.findMany({
|
|
where: {
|
|
classGroupId: { in: targetClassIds },
|
|
endDate: null,
|
|
},
|
|
include: {
|
|
student: {
|
|
select: {
|
|
id: true,
|
|
firstName: true,
|
|
lastName: true,
|
|
learningProgressEntries: {
|
|
where: {
|
|
date: { gte: fourteenDaysAgo },
|
|
},
|
|
select: { id: true },
|
|
take: 1,
|
|
},
|
|
},
|
|
},
|
|
classGroup: { select: { id: true, name: true } },
|
|
},
|
|
});
|
|
|
|
for (const e of activeEnrollments) {
|
|
if (e.student.learningProgressEntries.length === 0) {
|
|
const existing = notifications.find(
|
|
(n) => n.type === 'MISSING_OBSERVATION' && n.relatedId === e.student.id
|
|
);
|
|
if (!existing) {
|
|
upcomingAssessments.push({
|
|
id: `missing-${e.student.id}-${e.classGroupId}`,
|
|
type: 'MISSING_OBSERVATION',
|
|
title: `${e.student.firstName} ${e.student.lastName}`,
|
|
message: e.classGroup.name,
|
|
isRead: false,
|
|
actionUrl: 'progress',
|
|
relatedId: e.student.id,
|
|
createdAt: new Date().toISOString(),
|
|
});
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Combine DB notifications with dynamic ones
|
|
const dbNotifications = notifications.map((n) => ({
|
|
id: n.id,
|
|
type: n.type,
|
|
title: n.title,
|
|
message: n.message,
|
|
isRead: n.isRead,
|
|
actionUrl: n.actionUrl,
|
|
relatedId: n.relatedId,
|
|
createdAt: n.createdAt.toISOString(),
|
|
}));
|
|
|
|
const allNotifications = [...dbNotifications, ...upcomingAssessments];
|
|
|
|
return NextResponse.json({
|
|
notifications: allNotifications,
|
|
unreadCount: allNotifications.filter((n) => !n.isRead).length,
|
|
});
|
|
}
|
|
|
|
export async function PUT(request: Request) {
|
|
const session = await getSession();
|
|
if (!session?.user) {
|
|
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
|
}
|
|
|
|
try {
|
|
const body = await request.json();
|
|
const { ids, markAll } = body as { ids?: string[]; markAll?: boolean };
|
|
|
|
if (markAll) {
|
|
// Mark all notifications as read
|
|
await db.notification.updateMany({
|
|
where: {
|
|
userId: session.user.id,
|
|
isRead: false,
|
|
deletedAt: null,
|
|
},
|
|
data: { isRead: true },
|
|
});
|
|
return NextResponse.json({ success: true });
|
|
}
|
|
|
|
if (ids && Array.isArray(ids)) {
|
|
// Mark specific DB notifications as read
|
|
const dbIds = ids.filter((id) => !id.startsWith('assessment-') && !id.startsWith('missing-'));
|
|
if (dbIds.length > 0) {
|
|
await db.notification.updateMany({
|
|
where: {
|
|
id: { in: dbIds },
|
|
userId: session.user.id,
|
|
},
|
|
data: { isRead: true },
|
|
});
|
|
}
|
|
return NextResponse.json({ success: true });
|
|
}
|
|
|
|
return NextResponse.json({ error: 'Invalid request' }, { status: 400 });
|
|
} catch {
|
|
return NextResponse.json({ error: 'Invalid request' }, { status: 400 });
|
|
}
|
|
}
|
|
|
|
export async function POST(request: Request) {
|
|
const session = await getSession();
|
|
if (!session?.user) {
|
|
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
|
}
|
|
|
|
try {
|
|
const body = await request.json();
|
|
const { schoolId, userId, type, title, message, actionUrl, relatedId } = body as {
|
|
schoolId: string;
|
|
userId: string;
|
|
type: string;
|
|
title: string;
|
|
message: string;
|
|
actionUrl?: string;
|
|
relatedId?: string;
|
|
};
|
|
|
|
if (!schoolId || !userId || !type || !title || !message) {
|
|
return NextResponse.json({ error: 'Missing required fields' }, { status: 400 });
|
|
}
|
|
|
|
const notification = await db.notification.create({
|
|
data: {
|
|
schoolId,
|
|
userId,
|
|
type,
|
|
title,
|
|
message,
|
|
actionUrl: actionUrl ?? null,
|
|
relatedId: relatedId ?? null,
|
|
},
|
|
});
|
|
|
|
return NextResponse.json({ notification }, { status: 201 });
|
|
} catch {
|
|
return NextResponse.json({ error: 'Invalid request' }, { status: 400 });
|
|
}
|
|
}
|
|
|
|
export async function DELETE(request: Request) {
|
|
const session = await getSession();
|
|
if (!session?.user) {
|
|
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
|
}
|
|
|
|
try {
|
|
const body = await request.json().catch(() => ({}));
|
|
const { ids } = body as { ids?: string[] };
|
|
|
|
if (ids && Array.isArray(ids)) {
|
|
// Soft-delete specific notifications
|
|
await db.notification.updateMany({
|
|
where: {
|
|
id: { in: ids },
|
|
userId: session.user.id,
|
|
},
|
|
data: { deletedAt: new Date() },
|
|
});
|
|
} else {
|
|
// Soft-delete all notifications for the user
|
|
await db.notification.updateMany({
|
|
where: {
|
|
userId: session.user.id,
|
|
deletedAt: null,
|
|
},
|
|
data: { deletedAt: new Date() },
|
|
});
|
|
}
|
|
|
|
return NextResponse.json({ success: true });
|
|
} catch {
|
|
return NextResponse.json({ error: 'Invalid request' }, { status: 400 });
|
|
}
|
|
}
|