Round 23: Report Card Generator, Seating Chart, School Library, Parent Portal Enhancement

- Report Card Generator: Professional report cards with PDF export
  - Subject grades table with competency levels
  - Competency radar chart, teacher comments
  - Template management, batch generation
  - Status workflow: draft → review → published → archived

- Seating Chart View: Visual classroom layout
  - Grid-based classroom with desk positions
  - Multiple layout templates (rows, groups, U-shape, circle)
  - Click-to-assign student placement
  - Randomize and smart arrange features
  - Student info popover

- School Library View: Book lending system
  - Book catalog with search and filter
  - Checkout system with due dates and renewals
  - Overdue tracking with notifications
  - Reservation queue system
  - Statistics dashboard with charts

- Enhanced Parent Portal: Comprehensive dashboard
  - Child overview with multiple children selector
  - Academic progress with competency radar chart
  - Attendance & illness with approval
  - Communication, schedule, and reports tabs

- Bug fixes: i18n breadcrumb keys for new views
- Styling: Gradient headers, animations, hover effects across all new views
- 200+ i18n keys added, 15+ API routes, 3+ Prisma models
This commit is contained in:
Mustafa Cetin committed 2026-07-29 23:42:26 +00:00
1 parent 490bd0650f
commit 6e5af13778
28 files changed
+10252 -494

No files matched your search

+144
View File
@@ -0,0 +1,144 @@
import { NextResponse } from 'next/server';
import { db } from '@/lib/db';
import { getSession } from '@/lib/auth';
// ── GET: Get a single book ──────────────────────────────────────────
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 book = await db.libraryBook.findUnique({
where: { id },
include: {
checkouts: {
where: { status: { in: ['active', 'overdue'] } },
include: {
student: { select: { id: true, firstName: true, lastName: true } },
},
orderBy: { checkoutDate: 'desc' },
},
reservations: {
where: { status: 'waiting' },
include: {
student: { select: { id: true, firstName: true, lastName: true } },
user: { select: { id: true, firstName: true, lastName: true } },
},
orderBy: { createdAt: 'asc' },
},
},
});
if (!book) {
return NextResponse.json({ error: 'Book not found' }, { status: 404 });
}
return NextResponse.json(book);
} catch (error) {
console.error('LibraryBook GET error:', error);
return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
}
}
// ── PUT: Update a book ──────────────────────────────────────────────
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 userRole = session.user?.role;
if (
userRole !== 'SCHOOL_ADMIN' &&
userRole !== 'VICE_PRINCIPAL' &&
userRole !== 'SUPER_ADMIN'
) {
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
}
const { id } = await params;
const body = await request.json();
const existing = await db.libraryBook.findUnique({ where: { id } });
if (!existing) {
return NextResponse.json({ error: 'Book not found' }, { status: 404 });
}
const updateData: Record<string, unknown> = {};
const allowedFields = [
'title', 'author', 'isbn', 'publisher', 'publishYear',
'category', 'readingLevel', 'language', 'description',
'totalCopies', 'availableCopies', 'location',
];
for (const field of allowedFields) {
if (body[field] !== undefined) updateData[field] = body[field];
}
if (body.coverGradient) updateData.coverGradient = JSON.stringify(body.coverGradient);
if (body.tags) updateData.tags = JSON.stringify(body.tags);
const book = await db.libraryBook.update({
where: { id },
data: updateData,
});
return NextResponse.json(book);
} catch (error) {
console.error('LibraryBook PUT error:', error);
return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
}
}
// ── DELETE: Remove a book ──────────────────────────────────────────
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 userRole = session.user?.role;
if (
userRole !== 'SCHOOL_ADMIN' &&
userRole !== 'VICE_PRINCIPAL' &&
userRole !== 'SUPER_ADMIN'
) {
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
}
const { id } = await params;
// Check for active checkouts
const activeCheckouts = await db.bookCheckout.count({
where: { bookId: id, status: { in: ['active', 'overdue'] } },
});
if (activeCheckouts > 0) {
return NextResponse.json(
{ error: 'Cannot delete book with active checkouts' },
{ status: 400 }
);
}
await db.libraryBook.delete({ where: { id } });
return NextResponse.json({ success: true });
} catch (error) {
console.error('LibraryBook DELETE error:', error);
return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
}
}
+163
View File
@@ -0,0 +1,163 @@
import { NextResponse } from 'next/server';
import { db } from '@/lib/db';
import { getSession } from '@/lib/auth';
// ── GET: List/search books ──────────────────────────────────────────
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 search = searchParams.get('search');
const category = searchParams.get('category');
const readingLevel = searchParams.get('readingLevel');
const availability = searchParams.get('availability'); // all, available, checked_out
const limit = parseInt(searchParams.get('limit') ?? '50');
const offset = parseInt(searchParams.get('offset') ?? '0');
if (!schoolId) {
return NextResponse.json({ error: 'schoolId is required' }, { status: 400 });
}
const where: Record<string, unknown> = { schoolId };
if (search) {
where.OR = [
{ title: { contains: search } },
{ author: { contains: search } },
{ isbn: { contains: search } },
{ category: { contains: search } },
];
}
if (category) where.category = category;
if (readingLevel) where.readingLevel = readingLevel;
if (availability === 'available') {
where.availableCopies = { gt: 0 };
} else if (availability === 'checked_out') {
where.availableCopies = 0;
}
const [books, total] = await Promise.all([
db.libraryBook.findMany({
where,
include: {
_count: { select: { checkouts: true, reservations: true } },
},
orderBy: { title: 'asc' },
take: limit,
skip: offset,
}),
db.libraryBook.count({ where }),
]);
// Get active checkouts count per book
const activeCheckouts = await db.bookCheckout.groupBy({
by: ['bookId'],
where: { schoolId, status: 'active' },
_count: { id: true },
});
const activeCheckoutMap = Object.fromEntries(
activeCheckouts.map((c) => [c.bookId, c._count.id])
);
// Get waiting reservations count per book
const waitingReservations = await db.bookReservation.groupBy({
by: ['bookId'],
where: { schoolId, status: 'waiting' },
_count: { id: true },
});
const waitingReservationMap = Object.fromEntries(
waitingReservations.map((r) => [r.bookId, r._count.id])
);
const enriched = books.map((book) => ({
...book,
activeCheckouts: activeCheckoutMap[book.id] || 0,
waitingReservations: waitingReservationMap[book.id] || 0,
isAvailable: book.availableCopies > 0,
}));
return NextResponse.json({ books: enriched, total });
} catch (error) {
console.error('LibraryBooks GET error:', error);
return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
}
}
// ── POST: Add a new book ──────────────────────────────────────────
export async function POST(request: Request) {
try {
const session = await getSession();
if (!session) {
return NextResponse.json({ error: 'Not authenticated' }, { status: 401 });
}
const userRole = session.user?.role;
if (
userRole !== 'SCHOOL_ADMIN' &&
userRole !== 'VICE_PRINCIPAL' &&
userRole !== 'SUPER_ADMIN' &&
userRole !== 'TEACHER'
) {
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
}
const body = await request.json();
const {
title,
author,
isbn,
publisher,
publishYear,
category,
readingLevel,
language,
description,
coverGradient,
totalCopies,
location,
tags,
schoolId,
} = body;
if (!title || !author || !schoolId || !category) {
return NextResponse.json(
{ error: 'title, author, schoolId, and category are required' },
{ status: 400 }
);
}
const book = await db.libraryBook.create({
data: {
schoolId,
title,
author,
isbn: isbn || null,
publisher: publisher || null,
publishYear: publishYear || null,
category,
readingLevel: readingLevel || null,
language: language || 'de',
description: description || null,
coverGradient: coverGradient ? JSON.stringify(coverGradient) : null,
totalCopies: totalCopies ?? 1,
availableCopies: totalCopies ?? 1,
location: location || null,
tags: tags ? JSON.stringify(tags) : null,
},
});
return NextResponse.json(book, { status: 201 });
} catch (error) {
console.error('LibraryBooks POST error:', error);
return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
}
}
+163
View File
@@ -0,0 +1,163 @@
import { NextResponse } from 'next/server';
import { db } from '@/lib/db';
import { getSession } from '@/lib/auth';
// ── PUT: Return a book (or renew) ──────────────────────────────────
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 userRole = session.user?.role;
if (
userRole !== 'TEACHER' &&
userRole !== 'SCHOOL_ADMIN' &&
userRole !== 'VICE_PRINCIPAL' &&
userRole !== 'SUPER_ADMIN'
) {
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
}
const { id } = await params;
const body = await request.json();
const { action, returnCondition, notes, fineAmount } = body;
const checkout = await db.bookCheckout.findUnique({
where: { id },
include: { book: true },
});
if (!checkout) {
return NextResponse.json({ error: 'Checkout not found' }, { status: 404 });
}
if (action === 'return') {
// Return the book
const updated = await db.bookCheckout.update({
where: { id },
data: {
status: 'returned',
returnDate: new Date(),
returnCondition: returnCondition || checkout.condition,
notes: notes || checkout.notes,
fineAmount: fineAmount ?? checkout.fineAmount,
},
include: {
book: { select: { id: true, title: true, author: true } },
student: { select: { id: true, firstName: true, lastName: true } },
},
});
// Increase available copies
await db.libraryBook.update({
where: { id: checkout.bookId },
data: { availableCopies: { increment: 1 } },
});
// Check if there are waiting reservations to notify
const nextReservation = await db.bookReservation.findFirst({
where: { bookId: checkout.bookId, status: 'waiting' },
orderBy: { createdAt: 'asc' },
});
if (nextReservation) {
await db.bookReservation.update({
where: { id: nextReservation.id },
data: { status: 'notified', notifiedAt: new Date() },
});
}
return NextResponse.json(updated);
} else if (action === 'renew') {
// Renew the checkout (extend due date by 14 days)
if (checkout.status === 'returned' || checkout.status === 'lost') {
return NextResponse.json(
{ error: 'Cannot renew a returned or lost checkout' },
{ status: 400 }
);
}
if (checkout.renewalCount >= 3) {
return NextResponse.json(
{ error: 'Maximum renewals reached (3)' },
{ status: 400 }
);
}
const newDueDate = new Date(checkout.dueDate);
newDueDate.setDate(newDueDate.getDate() + 14);
const updated = await db.bookCheckout.update({
where: { id },
data: {
dueDate: newDueDate,
renewalCount: { increment: 1 },
status: 'active',
},
include: {
book: { select: { id: true, title: true, author: true } },
student: { select: { id: true, firstName: true, lastName: true } },
},
});
return NextResponse.json(updated);
} else if (action === 'mark_overdue') {
const updated = await db.bookCheckout.update({
where: { id },
data: { status: 'overdue' },
});
return NextResponse.json(updated);
} else if (action === 'mark_lost') {
const updated = await db.bookCheckout.update({
where: { id },
data: { status: 'lost' },
});
// Decrease total copies
await db.libraryBook.update({
where: { id: checkout.bookId },
data: { totalCopies: { decrement: 1 } },
});
return NextResponse.json(updated);
}
return NextResponse.json({ error: 'Invalid action' }, { status: 400 });
} catch (error) {
console.error('BookCheckout PUT error:', error);
return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
}
}
// ── DELETE: Remove a checkout record ──────────────────────────────
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 userRole = session.user?.role;
if (
userRole !== 'SCHOOL_ADMIN' &&
userRole !== 'VICE_PRINCIPAL' &&
userRole !== 'SUPER_ADMIN'
) {
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
}
const { id } = await params;
await db.bookCheckout.delete({ where: { id } });
return NextResponse.json({ success: true });
} catch (error) {
console.error('BookCheckout DELETE error:', error);
return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
}
}
+192
View File
@@ -0,0 +1,192 @@
import { NextResponse } from 'next/server';
import { db } from '@/lib/db';
import { getSession } from '@/lib/auth';
// ── GET: List checkouts ──────────────────────────────────────────────
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 status = searchParams.get('status');
const studentId = searchParams.get('studentId');
const bookId = searchParams.get('bookId');
const overdue = searchParams.get('overdue') === 'true';
const limit = parseInt(searchParams.get('limit') ?? '50');
const offset = parseInt(searchParams.get('offset') ?? '0');
if (!schoolId) {
return NextResponse.json({ error: 'schoolId is required' }, { status: 400 });
}
const where: Record<string, unknown> = { schoolId };
if (status) where.status = status;
if (studentId) where.studentId = studentId;
if (bookId) where.bookId = bookId;
if (overdue) {
where.status = 'active';
where.dueDate = { lt: new Date() };
}
// Role-based filtering
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 [checkouts, total] = await Promise.all([
db.bookCheckout.findMany({
where,
include: {
book: { select: { id: true, title: true, author: true, category: true } },
student: { select: { id: true, firstName: true, lastName: true } },
checkedOutByUser: { select: { id: true, firstName: true, lastName: true } },
},
orderBy: { checkoutDate: 'desc' },
take: limit,
skip: offset,
}),
db.bookCheckout.count({ where }),
]);
// Mark overdue checkouts
const now = new Date();
const enriched = checkouts.map((checkout) => {
const isOverdue = checkout.status === 'active' && new Date(checkout.dueDate) < now;
const daysOverdue = isOverdue
? Math.floor((now.getTime() - new Date(checkout.dueDate).getTime()) / (1000 * 60 * 60 * 24))
: 0;
return {
...checkout,
isOverdue,
daysOverdue,
};
});
// Auto-update overdue status in the background
if (status === 'active' || !status) {
const overdueIds = enriched
.filter((c) => c.isOverdue && c.status === 'active')
.map((c) => c.id);
if (overdueIds.length > 0) {
await db.bookCheckout.updateMany({
where: { id: { in: overdueIds } },
data: { status: 'overdue' },
});
}
}
return NextResponse.json({ checkouts: enriched, total });
} catch (error) {
console.error('BookCheckouts GET error:', error);
return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
}
}
// ── POST: Checkout a book ──────────────────────────────────────────
export async function POST(request: Request) {
try {
const session = await getSession();
if (!session) {
return NextResponse.json({ error: 'Not authenticated' }, { status: 401 });
}
const userRole = session.user?.role;
if (
userRole !== 'TEACHER' &&
userRole !== 'SCHOOL_ADMIN' &&
userRole !== 'VICE_PRINCIPAL' &&
userRole !== 'SUPER_ADMIN'
) {
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
}
const body = await request.json();
const { bookId, studentId, schoolId, dueDate, condition, notes } = body;
if (!bookId || !studentId || !schoolId) {
return NextResponse.json(
{ error: 'bookId, studentId, and schoolId are required' },
{ status: 400 }
);
}
// Check book availability
const book = await db.libraryBook.findUnique({ where: { id: bookId } });
if (!book) {
return NextResponse.json({ error: 'Book not found' }, { status: 404 });
}
if (book.availableCopies <= 0) {
return NextResponse.json({ error: 'No copies available' }, { status: 400 });
}
// Check if student already has this book checked out
const existingCheckout = await db.bookCheckout.findFirst({
where: {
bookId,
studentId,
status: { in: ['active', 'overdue'] },
},
});
if (existingCheckout) {
return NextResponse.json(
{ error: 'Student already has this book checked out' },
{ status: 400 }
);
}
// Calculate due date (default 14 days from now)
const checkoutDueDate = dueDate
? new Date(dueDate)
: new Date(Date.now() + 14 * 24 * 60 * 60 * 1000);
const checkout = await db.bookCheckout.create({
data: {
schoolId,
bookId,
studentId,
checkedOutBy: session.userId,
dueDate: checkoutDueDate,
condition: condition || 'good',
notes: notes || null,
},
include: {
book: { select: { id: true, title: true, author: true } },
student: { select: { id: true, firstName: true, lastName: true } },
checkedOutByUser: { select: { id: true, firstName: true, lastName: true } },
},
});
// Decrease available copies
await db.libraryBook.update({
where: { id: bookId },
data: { availableCopies: { decrement: 1 } },
});
return NextResponse.json(checkout, { status: 201 });
} catch (error) {
console.error('BookCheckout POST error:', error);
return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
}
}
@@ -0,0 +1,59 @@
import { NextResponse } from 'next/server';
import { db } from '@/lib/db';
import { getSession } from '@/lib/auth';
// ── DELETE: Cancel a reservation ──────────────────────────────────
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 reservation = await db.bookReservation.findUnique({
where: { id },
});
if (!reservation) {
return NextResponse.json({ error: 'Reservation not found' }, { status: 404 });
}
// Only the user who made the reservation, or admin, can cancel
const userRole = session.user?.role;
const isOwner = reservation.userId === session.userId;
const isAdmin = userRole === 'SCHOOL_ADMIN' || userRole === 'VICE_PRINCIPAL' || userRole === 'SUPER_ADMIN';
if (!isOwner && !isAdmin) {
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
}
await db.bookReservation.update({
where: { id },
data: { status: 'cancelled' },
});
// Re-calculate queue positions for remaining reservations
const remaining = await db.bookReservation.findMany({
where: { bookId: reservation.bookId, status: 'waiting' },
orderBy: { createdAt: 'asc' },
});
await Promise.all(
remaining.map((r, index) =>
db.bookReservation.update({
where: { id: r.id },
data: { queuePosition: index + 1 },
})
)
);
return NextResponse.json({ success: true });
} catch (error) {
console.error('BookReservation DELETE error:', error);
return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
}
}
+134
View File
@@ -0,0 +1,134 @@
import { NextResponse } from 'next/server';
import { db } from '@/lib/db';
import { getSession } from '@/lib/auth';
// ── GET: List reservations ──────────────────────────────────────────
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 bookId = searchParams.get('bookId');
const studentId = searchParams.get('studentId');
const status = searchParams.get('status');
if (!schoolId) {
return NextResponse.json({ error: 'schoolId is required' }, { status: 400 });
}
const where: Record<string, unknown> = { schoolId };
if (bookId) where.bookId = bookId;
if (studentId) where.studentId = studentId;
if (status) where.status = status;
// Role-based filtering
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 reservations = await db.bookReservation.findMany({
where,
include: {
book: { select: { id: true, title: true, author: true, availableCopies: true, totalCopies: true } },
student: { select: { id: true, firstName: true, lastName: true } },
user: { select: { id: true, firstName: true, lastName: true } },
},
orderBy: { createdAt: 'asc' },
});
return NextResponse.json({ reservations });
} catch (error) {
console.error('BookReservations GET error:', error);
return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
}
}
// ── POST: Reserve a book ──────────────────────────────────────────
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 { bookId, studentId, schoolId } = body;
if (!bookId || !studentId || !schoolId) {
return NextResponse.json(
{ error: 'bookId, studentId, and schoolId are required' },
{ status: 400 }
);
}
// Check book exists
const book = await db.libraryBook.findUnique({ where: { id: bookId } });
if (!book) {
return NextResponse.json({ error: 'Book not found' }, { status: 404 });
}
// Check if already reserved
const existingReservation = await db.bookReservation.findFirst({
where: {
bookId,
studentId,
status: { in: ['waiting', 'notified'] },
},
});
if (existingReservation) {
return NextResponse.json(
{ error: 'Already reserved this book' },
{ status: 400 }
);
}
// Calculate queue position
const maxPosition = await db.bookReservation.findFirst({
where: { bookId, status: 'waiting' },
orderBy: { queuePosition: 'desc' },
select: { queuePosition: true },
});
const queuePosition = (maxPosition?.queuePosition ?? 0) + 1;
const reservation = await db.bookReservation.create({
data: {
schoolId,
bookId,
studentId,
userId: session.userId,
queuePosition,
status: 'waiting',
},
include: {
book: { select: { id: true, title: true, author: true } },
student: { select: { id: true, firstName: true, lastName: true } },
user: { select: { id: true, firstName: true, lastName: true } },
},
});
return NextResponse.json(reservation, { status: 201 });
} catch (error) {
console.error('BookReservation POST error:', error);
return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
}
}
+142
View File
@@ -0,0 +1,142 @@
import { NextResponse } from 'next/server';
import { db } from '@/lib/db';
import { getSession } from '@/lib/auth';
// ── GET: Library statistics ──────────────────────────────────────────
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 });
}
const now = new Date();
// Basic counts
const [
totalBooks,
totalCopies,
availableCopies,
activeCheckouts,
overdueCheckouts,
totalCheckouts,
totalReservations,
waitingReservations,
] = await Promise.all([
db.libraryBook.count({ where: { schoolId } }),
db.libraryBook.aggregate({ where: { schoolId }, _sum: { totalCopies: true } }),
db.libraryBook.aggregate({ where: { schoolId }, _sum: { availableCopies: true } }),
db.bookCheckout.count({ where: { schoolId, status: 'active' } }),
db.bookCheckout.count({ where: { schoolId, status: 'overdue' } }),
db.bookCheckout.count({ where: { schoolId } }),
db.bookReservation.count({ where: { schoolId } }),
db.bookReservation.count({ where: { schoolId, status: 'waiting' } }),
]);
// Overdue rate
const overdueRate = totalCheckouts > 0
? Math.round((overdueCheckouts / totalCheckouts) * 100)
: 0;
// Most popular books (by checkout count)
const popularBooks = await db.bookCheckout.groupBy({
by: ['bookId'],
where: { schoolId },
_count: { id: true },
orderBy: { _count: { id: 'desc' } },
take: 10,
});
const popularBookIds = popularBooks.map((b) => b.bookId);
const popularBookDetails = await db.libraryBook.findMany({
where: { id: { in: popularBookIds } },
select: { id: true, title: true, author: true, category: true },
});
const popularBookMap = Object.fromEntries(
popularBookDetails.map((b) => [b.id, b])
);
const popularBooksWithCount = popularBooks.map((b) => ({
...popularBookMap[b.bookId],
checkoutCount: b._count.id,
}));
// Category distribution
const categoryDistribution = await db.libraryBook.groupBy({
by: ['category'],
where: { schoolId },
_count: { id: true },
});
// Checkout trends (last 30 days, by day)
const thirtyDaysAgo = new Date(now);
thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30);
const recentCheckouts = await db.bookCheckout.findMany({
where: {
schoolId,
checkoutDate: { gte: thirtyDaysAgo },
},
select: { checkoutDate: true },
});
// Group by day
const checkoutTrends: Record<string, number> = {};
for (const checkout of recentCheckouts) {
const dateKey = new Date(checkout.checkoutDate).toISOString().split('T')[0];
checkoutTrends[dateKey] = (checkoutTrends[dateKey] || 0) + 1;
}
const trendData = Object.entries(checkoutTrends)
.map(([date, count]) => ({ date, count }))
.sort((a, b) => a.date.localeCompare(b.date));
// Overdue checkouts list
const overdueList = await db.bookCheckout.findMany({
where: { schoolId, status: 'overdue' },
include: {
book: { select: { id: true, title: true, author: true } },
student: { select: { id: true, firstName: true, lastName: true } },
},
orderBy: { dueDate: 'asc' },
take: 20,
});
const overdueWithDays = overdueList.map((checkout) => {
const daysOverdue = Math.floor(
(now.getTime() - new Date(checkout.dueDate).getTime()) / (1000 * 60 * 60 * 24)
);
return { ...checkout, daysOverdue };
});
return NextResponse.json({
totalBooks,
totalCopies: totalCopies._sum.totalCopies || 0,
availableCopies: availableCopies._sum.availableCopies || 0,
checkedOut: activeCheckouts,
overdue: overdueCheckouts,
totalCheckouts,
totalReservations,
waitingReservations,
overdueRate,
popularBooks: popularBooksWithCount,
categoryDistribution: categoryDistribution.map((c) => ({
category: c.category,
count: c._count.id,
})),
checkoutTrends: trendData,
overdueList: overdueWithDays,
});
} catch (error) {
console.error('LibraryStats GET error:', error);
return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
}
}
@@ -0,0 +1,217 @@
import { NextResponse } from 'next/server';
import { db } from '@/lib/db';
import { getSession } from '@/lib/auth';
// GET: Returns detailed academic progress for a child
export async function GET(request: Request) {
try {
const session = await getSession();
if (!session) {
return NextResponse.json({ error: 'Not authenticated' }, { status: 401 });
}
if (session.user?.role !== 'PARENT') {
return NextResponse.json({ error: 'Forbidden - Parent role required' }, { status: 403 });
}
const parentId = session.userId;
const schoolId = session.user?.schoolId;
const { searchParams } = new URL(request.url);
const studentId = searchParams.get('studentId');
if (!studentId || !schoolId) {
return NextResponse.json({ error: 'studentId and schoolId are required' }, { status: 400 });
}
// Verify parent is linked to this student
const parentLink = await db.parentStudentLink.findFirst({
where: { parentId, studentId },
});
if (!parentLink) {
return NextResponse.json({ error: 'You can only view progress for your children' }, { status: 403 });
}
// Get all computed grades for the student
const grades = await db.computedGrade.findMany({
where: { studentId },
include: {
subject: { select: { id: true, name: true, code: true } },
classGroup: { select: { id: true, name: true } },
schoolYear: { select: { id: true, name: true } },
},
orderBy: { computedAt: 'desc' },
});
// Group grades by subject
const subjectGrades: Record<string, {
subject: { id: string; name: string; code: string | null };
grades: Array<{
id: string;
computedValue: number;
period: string;
computedAt: string;
classGroup: { id: string; name: string };
schoolYear: { id: string; name: string };
}>;
average: number;
trend: 'up' | 'down' | 'stable';
}> = {};
for (const grade of grades) {
const sid = grade.subjectId;
if (!subjectGrades[sid]) {
subjectGrades[sid] = {
subject: grade.subject,
grades: [],
average: 0,
trend: 'stable',
};
}
subjectGrades[sid].grades.push({
id: grade.id,
computedValue: grade.computedValue,
period: grade.period,
computedAt: grade.computedAt.toISOString(),
classGroup: grade.classGroup,
schoolYear: grade.schoolYear,
});
}
// Calculate averages and trends
for (const sid of Object.keys(subjectGrades)) {
const sg = subjectGrades[sid];
const values = sg.grades.map((g) => g.computedValue);
sg.average = values.length > 0
? Math.round((values.reduce((a, b) => a + b, 0) / values.length) * 10) / 10
: 0;
// Trend: compare last two periods
if (values.length >= 2) {
const latest = values[0];
const previous = values[1];
if (latest > previous + 0.3) sg.trend = 'up';
else if (latest < previous - 0.3) sg.trend = 'down';
else sg.trend = 'stable';
}
}
// Class averages for comparison
const studentEnrollments = await db.enrollment.findMany({
where: { studentId, endDate: null },
select: { classGroupId: true },
});
const classIds = studentEnrollments.map((e) => e.classGroupId);
const classAverages: Record<string, number> = {};
if (classIds.length > 0) {
const classGrades = await db.computedGrade.findMany({
where: {
classGroupId: { in: classIds },
student: { enrollments: { some: { classGroupId: { in: classIds }, endDate: null } } },
},
include: { subject: { select: { id: true } } },
});
const bySubject: Record<string, number[]> = {};
for (const cg of classGrades) {
const sid = cg.subjectId;
if (!bySubject[sid]) bySubject[sid] = [];
bySubject[sid].push(cg.computedValue);
}
for (const [sid, vals] of Object.entries(bySubject)) {
classAverages[sid] = Math.round((vals.reduce((a, b) => a + b, 0) / vals.length) * 10) / 10;
}
}
// Competency progress data for radar chart
const learningProgress = await db.learningProgress.findMany({
where: { studentId },
include: {
competency: { select: { id: true, code: true, title: true, subject: { select: { id: true, name: true } } } },
},
orderBy: { assessedAt: 'desc' },
});
// Group by subject for radar chart
const competencyBySubject: Record<string, {
subject: { id: string; name: string };
competencies: Array<{
id: string;
code: string;
title: string;
level: number;
assessedAt: string;
}>;
averageLevel: number;
}> = {};
for (const lp of learningProgress) {
const sid = lp.competency.subject?.id ?? 'unknown';
if (!competencyBySubject[sid]) {
competencyBySubject[sid] = {
subject: lp.competency.subject ?? { id: sid, name: 'Unknown' },
competencies: [],
averageLevel: 0,
};
}
// Only keep the latest for each competency
const existing = competencyBySubject[sid].competencies.find((c) => c.id === lp.competencyId);
if (!existing) {
competencyBySubject[sid].competencies.push({
id: lp.competencyId,
code: lp.competency.code,
title: lp.competency.title,
level: lp.level,
assessedAt: lp.assessedAt.toISOString(),
});
}
}
for (const sid of Object.keys(competencyBySubject)) {
const cs = competencyBySubject[sid];
const levels = cs.competencies.map((c) => c.level);
cs.averageLevel = levels.length > 0
? Math.round((levels.reduce((a, b) => a + b, 0) / levels.length) * 10) / 10
: 0;
}
// Assessment results
const assessments = await db.assessmentResult.findMany({
where: {
studentId,
assessment: { schoolId },
},
include: {
assessment: {
select: {
id: true,
title: true,
type: true,
date: true,
subject: { select: { id: true, name: true } },
},
},
},
orderBy: { assessedAt: 'desc' },
take: 20,
});
return NextResponse.json({
subjectGrades: Object.values(subjectGrades),
classAverages,
competencyBySubject: Object.values(competencyBySubject),
assessments: assessments.map((a) => ({
id: a.id,
score: a.score,
maxScore: a.maxScore,
grade: a.grade,
feedback: a.feedback,
assessedAt: a.assessedAt.toISOString(),
assessment: a.assessment,
})),
});
} catch (error) {
console.error('Child Progress GET error:', error);
return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
}
}
@@ -0,0 +1,184 @@
import { NextResponse } from 'next/server';
import { db } from '@/lib/db';
import { getSession } from '@/lib/auth';
import { differenceInCalendarDays } from 'date-fns';
// GET: Returns the weekly schedule for a child
export async function GET(request: Request) {
try {
const session = await getSession();
if (!session) {
return NextResponse.json({ error: 'Not authenticated' }, { status: 401 });
}
if (session.user?.role !== 'PARENT') {
return NextResponse.json({ error: 'Forbidden - Parent role required' }, { status: 403 });
}
const parentId = session.userId;
const schoolId = session.user?.schoolId;
const { searchParams } = new URL(request.url);
const studentId = searchParams.get('studentId');
if (!studentId || !schoolId) {
return NextResponse.json({ error: 'studentId and schoolId are required' }, { status: 400 });
}
// Verify parent is linked to this student
const parentLink = await db.parentStudentLink.findFirst({
where: { parentId, studentId },
});
if (!parentLink) {
return NextResponse.json({ error: 'You can only view schedule for your children' }, { status: 403 });
}
// Get student's class groups
const enrollments = await db.enrollment.findMany({
where: { studentId, endDate: null },
select: { classGroupId: true, classGroup: { select: { id: true, name: true } } },
});
const classIds = enrollments.map((e) => e.classGroupId);
// Timetable slots
const timetableSlots = classIds.length > 0
? await db.timetableSlot.findMany({
where: {
schoolId,
classGroupId: { in: classIds },
deletedAt: null,
},
include: {
subject: { select: { id: true, name: true, code: true } },
teacher: { select: { id: true, firstName: true, lastName: true } },
classGroup: { select: { id: true, name: true } },
},
orderBy: [{ dayOfWeek: 'asc' }, { period: 'asc' }],
})
: [];
// Upcoming exams
const upcomingExams = classIds.length > 0
? await db.calendarEvent.findMany({
where: {
schoolId,
eventType: 'exam',
classGroupId: { in: classIds },
date: { gte: new Date() },
},
include: {
subject: { select: { id: true, name: true } },
classGroup: { select: { id: true, name: true } },
},
orderBy: { date: 'asc' },
take: 15,
})
: [];
// School events
const schoolEvents = await db.schoolEvent.findMany({
where: {
schoolId,
deletedAt: null,
startDate: { gte: new Date() },
},
orderBy: { startDate: 'asc' },
take: 10,
select: {
id: true,
title: true,
description: true,
startDate: true,
endDate: true,
eventType: true,
location: true,
organizer: { select: { id: true, firstName: true, lastName: true } },
},
});
// Counseling appointments
const student = await db.student.findUnique({
where: { id: studentId },
select: { userId: true },
});
const counselingAppointments = student?.userId
? await db.counselingAppointment.findMany({
where: { studentId: student.userId, schoolId },
orderBy: { scheduledAt: 'desc' },
take: 10,
select: {
id: true,
requestType: true,
status: true,
scheduledAt: true,
description: true,
counselor: { select: { firstName: true, lastName: true } },
},
})
: [];
// Homework due
const homeworkDue = classIds.length > 0
? await db.homework.findMany({
where: {
schoolId,
classGroupId: { in: classIds },
dueDate: { gte: new Date() },
isPublished: true,
deletedAt: null,
},
include: {
subject: { select: { id: true, name: true } },
classGroup: { select: { id: true, name: true } },
},
orderBy: { dueDate: 'asc' },
take: 10,
})
: [];
return NextResponse.json({
timetableSlots: timetableSlots.map((s) => ({
id: s.id,
dayOfWeek: s.dayOfWeek,
period: s.period,
startTime: s.startTime,
endTime: s.endTime,
isBreak: s.isBreak,
subject: s.subject ? { id: s.subject.id, name: s.subject.name, code: s.subject.code } : null,
teacher: s.teacher ? { id: s.teacher.id, firstName: s.teacher.firstName, lastName: s.teacher.lastName } : null,
classGroup: { id: s.classGroup.id, name: s.classGroup.name },
})),
upcomingExams: upcomingExams.map((exam) => ({
id: exam.id,
title: exam.title,
date: exam.date.toISOString(),
startTime: exam.startTime,
endTime: exam.endTime,
subject: exam.subject ? { id: exam.subject.id, name: exam.subject.name } : null,
classGroup: exam.classGroup ? { id: exam.classGroup.id, name: exam.classGroup.name } : null,
daysUntil: differenceInCalendarDays(new Date(exam.date), new Date()),
})),
schoolEvents: schoolEvents.map((e) => ({
...e,
startDate: e.startDate.toISOString(),
endDate: e.endDate?.toISOString() ?? null,
})),
counselingAppointments: counselingAppointments.map((a) => ({
...a,
scheduledAt: a.scheduledAt?.toISOString() ?? null,
})),
homeworkDue: homeworkDue.map((h) => ({
id: h.id,
title: h.title,
description: h.description,
dueDate: h.dueDate.toISOString(),
homeworkType: h.homeworkType,
subject: h.subject ? { id: h.subject.id, name: h.subject.name } : null,
classGroup: { id: h.classGroup.id, name: h.classGroup.name },
})),
});
} catch (error) {
console.error('Child Schedule GET error:', error);
return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
}
}
+117 -1
View File
@@ -59,6 +59,7 @@ export async function GET(request: Request) {
for (const child of children) {
const studentId = child.studentId;
const classIds = child.classGroups.map((c) => c.id);
// Illness reports
const illnessReports = await db.illnessReport.findMany({
@@ -74,12 +75,12 @@ export async function GET(request: Request) {
parentApprovalStatus: true,
status: true,
reporterType: true,
documentUrl: true,
createdAt: true,
},
});
// Upcoming exams (calendar events with type 'exam')
const classIds = child.classGroups.map((c) => c.id);
const upcomingExams = classIds.length > 0
? await db.calendarEvent.findMany({
where: {
@@ -157,6 +158,24 @@ export async function GET(request: Request) {
},
});
// Homework due
const homeworkDue = classIds.length > 0
? await db.homework.findMany({
where: {
schoolId,
classGroupId: { in: classIds },
dueDate: { gte: new Date() },
isPublished: true,
deletedAt: null,
},
include: {
subject: { select: { id: true, name: true } },
},
orderBy: { dueDate: 'asc' },
take: 10,
})
: [];
childrenData.push({
...child,
illnessReports,
@@ -168,6 +187,12 @@ export async function GET(request: Request) {
attendanceSummary,
counselingAppointments,
disciplinaryCases,
homeworkDue: homeworkDue.map((h) => ({
id: h.id,
title: h.title,
dueDate: h.dueDate.toISOString(),
subject: h.subject ? { id: h.subject.id, name: h.subject.name } : null,
})),
});
}
@@ -184,9 +209,100 @@ export async function GET(request: Request) {
orderBy: { createdAt: 'desc' },
});
// School announcements for parents
const announcements = await db.announcement.findMany({
where: {
schoolId,
deletedAt: null,
OR: [
{ targetAudience: 'all' },
{ targetAudience: 'parents' },
],
OR: [
{ expiresAt: null },
{ expiresAt: { gt: new Date() } },
],
},
orderBy: { createdAt: 'desc' },
take: 10,
include: {
author: { select: { id: true, firstName: true, lastName: true } },
},
});
// Communication rooms for this parent's children
const childUserIds = children
.filter((c) => c.userId)
.map((c) => c.userId as string);
const communicationRooms = childUserIds.length > 0
? await db.communicationRoom.findMany({
where: {
studentId: { in: childUserIds },
status: { in: ['requested', 'active'] },
},
orderBy: { updatedAt: 'desc' },
include: {
student: { select: { id: true, firstName: true, lastName: true } },
teacher: { select: { id: true, firstName: true, lastName: true } },
messages: {
orderBy: { createdAt: 'desc' },
take: 1,
select: { id: true, content: true, createdAt: true, senderId: true },
},
},
})
: [];
// School events
const schoolEvents = await db.schoolEvent.findMany({
where: {
schoolId,
deletedAt: null,
startDate: { gte: new Date() },
},
orderBy: { startDate: 'asc' },
take: 5,
select: {
id: true,
title: true,
startDate: true,
eventType: true,
location: true,
},
});
return NextResponse.json({
children: childrenData,
pendingApprovals,
announcements: announcements.map((a) => ({
id: a.id,
title: a.title,
content: a.content,
priority: a.priority,
announcementType: a.announcementType,
isPinned: a.isPinned,
createdAt: a.createdAt.toISOString(),
author: a.author ? { firstName: a.author.firstName, lastName: a.author.lastName } : null,
})),
communicationRooms: communicationRooms.map((r) => ({
id: r.id,
status: r.status,
roomType: r.roomType,
student: r.student,
teacher: r.teacher,
lastMessage: r.messages[0] ? {
id: r.messages[0].id,
content: r.messages[0].content,
createdAt: r.messages[0].createdAt.toISOString(),
senderId: r.messages[0].senderId,
} : null,
updatedAt: r.updatedAt.toISOString(),
})),
schoolEvents: schoolEvents.map((e) => ({
...e,
startDate: e.startDate.toISOString(),
})),
});
} catch (error) {
console.error('Parent Portal GET error:', error);
+219
View File
@@ -0,0 +1,219 @@
import { NextResponse } from 'next/server';
import { z } from 'zod';
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 report = await db.report.findUnique({
where: { id },
include: {
student: { select: { id: true, firstName: true, lastName: true, avatarUrl: true, dateOfBirth: true } },
classGroup: {
select: { id: true, name: true, gradeLevel: true, schoolType: true, responsibleTeacher: { select: { id: true, firstName: true, lastName: true } } },
},
schoolYear: { select: { id: true, label: true, startDate: true, endDate: true } },
generatedByUser: { select: { id: true, firstName: true, lastName: true } },
sections: {
orderBy: { order: 'asc' },
include: {
competencyCategory: { select: { id: true, name: true, color: true } },
},
},
template: { select: { id: true, name: true, layout: true, gradingScale: true, sections: true } },
},
});
if (!report) {
return NextResponse.json({ error: 'Report not found' }, { status: 404 });
}
// Role-based access check
if (session.user?.role === 'STUDENT') {
const student = await db.student.findFirst({ where: { userId: session.userId } });
if (!student || student.id !== report.studentId || report.status !== 'PUBLISHED') {
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
}
} else if (session.user?.role === 'PARENT') {
const link = await db.parentStudentLink.findFirst({
where: { parentId: session.userId, studentId: report.studentId },
});
if (!link || report.status !== 'PUBLISHED') {
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
}
}
// Fetch computed grades for this student/class/schoolYear
const computedGrades = await db.computedGrade.findMany({
where: {
studentId: report.studentId,
classGroupId: report.classGroupId,
schoolYearId: report.schoolYearId,
},
include: {
subject: { select: { id: true, name: true } },
},
orderBy: { subject: { name: 'asc' } },
});
return NextResponse.json({ ...report, computedGrades });
} catch (error) {
console.error('ReportCard GET error:', error);
return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
}
}
const updateReportCardSchema = z.object({
status: z.enum(['DRAFT', 'REVIEW', 'PUBLISHED', 'ARCHIVED', 'FINAL']).optional(),
includesGrades: z.boolean().optional(),
pdfFilePath: z.string().optional(),
teacherComments: z.string().nullable().optional(),
attendanceSummary: z.string().nullable().optional(),
overallAssessment: z.string().nullable().optional(),
templateId: z.string().nullable().optional(),
period: z.string().optional(),
sections: z
.array(
z.object({
id: z.string().optional(),
competencyCategoryId: z.string().nullable().optional(),
generatedText: z.string().min(1),
order: z.number().int().default(0),
})
)
.optional(),
});
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 (
session.user?.role !== 'SUPER_ADMIN' &&
session.user?.role !== 'SCHOOL_ADMIN' &&
session.user?.role !== 'VICE_PRINCIPAL' &&
session.user?.role !== 'TEACHER'
) {
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
}
const { id } = await params;
const body = await request.json();
const parsed = updateReportCardSchema.safeParse(body);
if (!parsed.success) {
return NextResponse.json(
{ error: 'Validation failed', details: parsed.error.issues },
{ status: 400 }
);
}
const { sections, ...updateData } = parsed.data;
// Check if teacher owns this report
if (session.user?.role === 'TEACHER') {
const existing = await db.report.findUnique({ where: { id } });
if (!existing || existing.generatedByUserId !== session.userId) {
return NextResponse.json({ error: 'You can only edit your own reports' }, { status: 403 });
}
// Teachers can only submit for review, not publish
if (updateData.status === 'PUBLISHED') {
return NextResponse.json({ error: 'Teachers cannot publish reports directly' }, { status: 403 });
}
}
// Handle status transitions
const statusUpdate: Record<string, unknown> = {};
if (updateData.status === 'REVIEW') {
statusUpdate.reviewedByUserId = session.userId;
statusUpdate.reviewedAt = new Date();
}
if (updateData.status === 'PUBLISHED') {
statusUpdate.publishedAt = new Date();
}
// Update sections if provided
if (sections && sections.length > 0) {
// Delete existing sections and recreate
await db.reportSection.deleteMany({ where: { reportId: id } });
await db.reportSection.createMany({
data: sections.map((s) => ({
reportId: id,
competencyCategoryId: s.competencyCategoryId ?? null,
generatedText: s.generatedText,
order: s.order,
})),
});
}
const report = await db.report.update({
where: { id },
data: {
...updateData,
...statusUpdate,
},
include: {
student: { select: { id: true, firstName: true, lastName: true, avatarUrl: true } },
classGroup: { select: { id: true, name: true, gradeLevel: true } },
schoolYear: { select: { id: true, label: true } },
generatedByUser: { select: { id: true, firstName: true, lastName: true } },
sections: {
orderBy: { order: 'asc' },
include: {
competencyCategory: { select: { id: true, name: true, color: true } },
},
},
template: { select: { id: true, name: true, layout: true } },
},
});
return NextResponse.json(report);
} catch (error) {
console.error('ReportCard 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 (
session.user?.role !== 'SUPER_ADMIN' &&
session.user?.role !== 'SCHOOL_ADMIN' &&
session.user?.role !== 'VICE_PRINCIPAL'
) {
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
}
const { id } = await params;
await db.reportSection.deleteMany({ where: { reportId: id } });
await db.report.delete({ where: { id } });
return NextResponse.json({ success: true });
} catch (error) {
console.error('ReportCard DELETE error:', error);
return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
}
}
+101
View File
@@ -0,0 +1,101 @@
import { NextResponse } from 'next/server';
import { z } from 'zod';
import { db } from '@/lib/db';
import { getSession } from '@/lib/auth';
const exportSchema = z.object({
reportIds: z.array(z.string().min(1)).min(1),
});
export async function POST(request: Request) {
try {
const session = await getSession();
if (!session) {
return NextResponse.json({ error: 'Not authenticated' }, { status: 401 });
}
if (
session.user?.role !== 'SUPER_ADMIN' &&
session.user?.role !== 'SCHOOL_ADMIN' &&
session.user?.role !== 'VICE_PRINCIPAL' &&
session.user?.role !== 'TEACHER'
) {
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
}
const body = await request.json();
const parsed = exportSchema.safeParse(body);
if (!parsed.success) {
return NextResponse.json(
{ error: 'Validation failed', details: parsed.error.issues },
{ status: 400 }
);
}
const { reportIds } = parsed.data;
const reports = await db.report.findMany({
where: { id: { in: reportIds } },
include: {
student: { select: { id: true, firstName: true, lastName: true, avatarUrl: true, dateOfBirth: true } },
classGroup: {
select: {
id: true, name: true, gradeLevel: true, schoolType: true,
responsibleTeacher: { select: { id: true, firstName: true, lastName: true } },
},
},
schoolYear: { select: { id: true, label: true } },
generatedByUser: { select: { id: true, firstName: true, lastName: true } },
sections: {
orderBy: { order: 'asc' },
include: {
competencyCategory: { select: { id: true, name: true, color: true } },
},
},
template: { select: { id: true, name: true, layout: true, gradingScale: true } },
},
});
// Get school info for branding
const schoolId = session.user?.schoolId;
let school = null;
if (schoolId) {
school = await db.school.findUnique({
where: { id: schoolId },
select: {
id: true, name: true, motto: true, logoUrl: true,
primaryColor: true, secondaryColor: true, address: true, phone: true,
},
});
}
// Get computed grades for each report
const reportsWithGrades = await Promise.all(
reports.map(async (report) => {
const computedGrades = await db.computedGrade.findMany({
where: {
studentId: report.studentId,
classGroupId: report.classGroupId,
schoolYearId: report.schoolYearId,
},
include: {
subject: { select: { id: true, name: true } },
},
orderBy: { subject: { name: 'asc' } },
});
return { ...report, computedGrades };
})
);
// Return the data for client-side PDF generation
// The client will use window.print() or a print-specific CSS
return NextResponse.json({
reports: reportsWithGrades,
school,
exportDate: new Date().toISOString(),
});
} catch (error) {
console.error('ReportCards export error:', error);
return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
}
}
+184
View File
@@ -0,0 +1,184 @@
import { NextResponse } from 'next/server';
import { z } from 'zod';
import { db } from '@/lib/db';
import { getSession } from '@/lib/auth';
const batchGenerateSchema = z.object({
classGroupId: z.string().min(1),
schoolYearId: z.string().min(1),
period: z.string().min(1),
templateId: z.string().optional(),
includesGrades: z.boolean().default(true),
});
export async function POST(request: Request) {
try {
const session = await getSession();
if (!session) {
return NextResponse.json({ error: 'Not authenticated' }, { status: 401 });
}
if (
session.user?.role !== 'SUPER_ADMIN' &&
session.user?.role !== 'SCHOOL_ADMIN' &&
session.user?.role !== 'VICE_PRINCIPAL'
) {
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
}
const body = await request.json();
const parsed = batchGenerateSchema.safeParse(body);
if (!parsed.success) {
return NextResponse.json(
{ error: 'Validation failed', details: parsed.error.issues },
{ status: 400 }
);
}
const { classGroupId, schoolYearId, period, templateId, includesGrades } = parsed.data;
// Get all enrolled students in the class
const enrollments = await db.enrollment.findMany({
where: {
classGroupId,
schoolYearId,
endDate: null,
},
include: {
student: { select: { id: true, firstName: true, lastName: true } },
},
});
if (enrollments.length === 0) {
return NextResponse.json({ error: 'No students found in this class' }, { status: 400 });
}
// Get competency categories for the class
const competencyAssignments = await db.classCompetencyAssignment.findMany({
where: { classGroupId, schoolYearId },
include: {
competencyTemplate: {
include: {
categories: {
include: { competencies: true },
orderBy: { order: 'asc' },
},
},
},
subject: { select: { id: true, name: true } },
},
});
// Get computed grades for all students
const computedGrades = await db.computedGrade.findMany({
where: { classGroupId, schoolYearId },
include: { subject: { select: { id: true, name: true } } },
});
const createdReports = [];
for (const enrollment of enrollments) {
// Check if report already exists for this student/period
const existing = await db.report.findFirst({
where: {
studentId: enrollment.studentId,
classGroupId,
schoolYearId,
period,
},
});
if (existing) continue; // Skip if report already exists
// Build sections from competency categories
const sections: { competencyCategoryId: string | null; generatedText: string; order: number }[] = [];
for (const assignment of competencyAssignments) {
const studentGrades = computedGrades.filter(
(g) => g.studentId === enrollment.studentId && g.subjectId === assignment.subjectId
);
for (const category of assignment.competencyTemplate.categories) {
// Get learning progress for this student in this category
const progressEntries = await db.learningProgressEntry.findMany({
where: {
studentId: enrollment.studentId,
classGroupId,
competency: { categoryId: category.id },
},
orderBy: { date: 'desc' },
take: 5,
});
const avgMastery =
progressEntries.length > 0
? progressEntries.reduce((sum, e) => sum + e.masteryLevelValue, 0) / progressEntries.length
: 0;
const gradeText = studentGrades.length > 0
? ` | Note: ${studentGrades[0].overriddenValue ?? studentGrades[0].computedValue}`
: '';
const masteryLabel = avgMastery >= 3.5 ? 'weit entwickelt' : avgMastery >= 2.5 ? 'kompetent' : avgMastery >= 1.5 ? 'entwickelt sich' : 'noch nicht erreicht';
sections.push({
competencyCategoryId: category.id,
generatedText: `${category.name}: ${masteryLabel}${gradeText}`,
order: sections.length,
});
}
}
// Get attendance summary
const attendanceRecords = await db.attendanceRecord.findMany({
where: {
studentId: enrollment.studentId,
session: {
classGroupId,
date: { gte: new Date(new Date().getFullYear(), 0, 1) },
},
},
});
const attendanceSummary = JSON.stringify({
present: attendanceRecords.filter((r) => r.status === 'PRESENT').length,
absent: attendanceRecords.filter((r) => r.status === 'ABSENT').length,
excused: attendanceRecords.filter((r) => r.status === 'EXCUSED').length,
late: attendanceRecords.filter((r) => r.status === 'LATE').length,
total: attendanceRecords.length,
});
const report = await db.report.create({
data: {
studentId: enrollment.studentId,
classGroupId,
schoolYearId,
period,
generatedByUserId: session.userId,
includesGrades,
status: 'DRAFT',
templateId: templateId ?? null,
attendanceSummary,
sections: sections.length > 0
? { create: sections }
: undefined,
},
include: {
student: { select: { id: true, firstName: true, lastName: true } },
classGroup: { select: { id: true, name: true } },
},
});
createdReports.push(report);
}
return NextResponse.json({
generated: createdReports.length,
skipped: enrollments.length - createdReports.length,
reports: createdReports,
});
} catch (error) {
console.error('ReportCards batch generate error:', error);
return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
}
}
+199
View File
@@ -0,0 +1,199 @@
import { NextResponse } from 'next/server';
import { z } from 'zod';
import { db } from '@/lib/db';
import { getSession } from '@/lib/auth';
const createReportCardSchema = z.object({
studentId: z.string().min(1),
classGroupId: z.string().min(1),
schoolYearId: z.string().min(1),
period: z.string().min(1),
includesGrades: z.boolean().default(false),
teacherComments: z.string().optional(),
attendanceSummary: z.string().optional(),
overallAssessment: z.string().optional(),
templateId: z.string().optional(),
sections: z
.array(
z.object({
competencyCategoryId: z.string().optional(),
generatedText: z.string().min(1),
order: z.number().int().default(0),
})
)
.optional(),
});
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 classGroupId = searchParams.get('classGroupId');
const schoolYearId = searchParams.get('schoolYearId');
const status = searchParams.get('status');
const period = searchParams.get('period');
const templateId = searchParams.get('templateId');
const where: Record<string, unknown> = {};
if (studentId) where.studentId = studentId;
if (classGroupId) where.classGroupId = classGroupId;
if (schoolYearId) where.schoolYearId = schoolYearId;
if (status) where.status = status;
if (period) where.period = period;
if (templateId) where.templateId = templateId;
// Role-based filtering
if (session.user?.role === 'STUDENT') {
// Students can only see their own published reports
const student = await db.student.findFirst({
where: { userId: session.userId },
});
if (student) {
where.studentId = student.id;
where.status = 'PUBLISHED';
} else {
return NextResponse.json([]);
}
} else if (session.user?.role === 'PARENT') {
// Parents can only see their children's published reports
const parentLinks = await db.parentStudentLink.findMany({
where: { parentId: session.userId },
});
const childIds = parentLinks.map((l) => l.studentId);
where.studentId = { in: childIds };
where.status = 'PUBLISHED';
} else if (session.user?.role === 'TEACHER') {
// Teachers see reports for their classes
if (!classGroupId) {
const teacherClasses = await db.classGroupTeacher.findMany({
where: { userId: session.userId },
});
const classIds = teacherClasses.map((c) => c.classGroupId);
where.classGroupId = { in: classIds };
}
}
const reports = await db.report.findMany({
where,
orderBy: { generatedAt: 'desc' },
include: {
student: { select: { id: true, firstName: true, lastName: true, avatarUrl: true } },
classGroup: { select: { id: true, name: true, gradeLevel: true } },
schoolYear: { select: { id: true, label: true } },
generatedByUser: { select: { id: true, firstName: true, lastName: true } },
sections: {
orderBy: { order: 'asc' },
include: {
competencyCategory: { select: { id: true, name: true, color: true } },
},
},
template: { select: { id: true, name: true, layout: true } },
},
});
return NextResponse.json(reports);
} catch (error) {
console.error('ReportCards 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 (
session.user?.role !== 'SUPER_ADMIN' &&
session.user?.role !== 'SCHOOL_ADMIN' &&
session.user?.role !== 'VICE_PRINCIPAL' &&
session.user?.role !== 'TEACHER'
) {
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
}
const body = await request.json();
const parsed = createReportCardSchema.safeParse(body);
if (!parsed.success) {
return NextResponse.json(
{ error: 'Validation failed', details: parsed.error.issues },
{ status: 400 }
);
}
const { sections, ...reportData } = parsed.data;
// If attendance summary not provided, try to compute it
let attendanceSummary = reportData.attendanceSummary;
if (!attendanceSummary) {
const attendanceRecords = await db.attendanceRecord.findMany({
where: {
studentId: reportData.studentId,
session: {
classGroupId: reportData.classGroupId,
date: { gte: new Date(new Date().getFullYear(), 0, 1) },
},
},
include: { session: true },
});
const summary = {
present: attendanceRecords.filter((r) => r.status === 'PRESENT').length,
absent: attendanceRecords.filter((r) => r.status === 'ABSENT').length,
excused: attendanceRecords.filter((r) => r.status === 'EXCUSED').length,
late: attendanceRecords.filter((r) => r.status === 'LATE').length,
total: attendanceRecords.length,
};
attendanceSummary = JSON.stringify(summary);
}
const report = await db.report.create({
data: {
studentId: reportData.studentId,
classGroupId: reportData.classGroupId,
schoolYearId: reportData.schoolYearId,
period: reportData.period,
generatedByUserId: session.userId,
includesGrades: reportData.includesGrades,
status: 'DRAFT',
teacherComments: reportData.teacherComments,
attendanceSummary,
overallAssessment: reportData.overallAssessment,
templateId: reportData.templateId,
sections: sections
? {
create: sections.map((s) => ({
competencyCategoryId: s.competencyCategoryId ?? null,
generatedText: s.generatedText,
order: s.order,
})),
}
: undefined,
},
include: {
student: { select: { id: true, firstName: true, lastName: true, avatarUrl: true } },
classGroup: { select: { id: true, name: true } },
schoolYear: { select: { id: true, label: true } },
generatedByUser: { select: { id: true, firstName: true, lastName: true } },
sections: {
orderBy: { order: 'asc' },
include: {
competencyCategory: { select: { id: true, name: true } },
},
},
template: { select: { id: true, name: true, layout: true } },
},
});
return NextResponse.json(report, { status: 201 });
} catch (error) {
console.error('ReportCards POST error:', error);
return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
}
}
@@ -0,0 +1,95 @@
import { NextResponse } from 'next/server';
import { z } from 'zod';
import { db } from '@/lib/db';
import { getSession } from '@/lib/auth';
const createTemplateSchema = z.object({
name: z.string().min(1),
description: z.string().optional(),
sections: z.string().default('[]'),
gradingScale: z.string().default('{}'),
layout: z.enum(['default', 'detailed', 'compact']).default('default'),
isDefault: z.boolean().default(false),
});
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;
if (!schoolId) {
return NextResponse.json({ error: 'School ID required' }, { status: 400 });
}
const templates = await db.reportCardTemplate.findMany({
where: { schoolId },
orderBy: [{ isDefault: 'desc' }, { name: 'asc' }],
});
return NextResponse.json(templates);
} catch (error) {
console.error('ReportCardTemplates 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 (
session.user?.role !== 'SUPER_ADMIN' &&
session.user?.role !== 'SCHOOL_ADMIN' &&
session.user?.role !== 'VICE_PRINCIPAL'
) {
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
}
const schoolId = session.user?.schoolId;
if (!schoolId) {
return NextResponse.json({ error: 'School ID required' }, { status: 400 });
}
const body = await request.json();
const parsed = createTemplateSchema.safeParse(body);
if (!parsed.success) {
return NextResponse.json(
{ error: 'Validation failed', details: parsed.error.issues },
{ status: 400 }
);
}
// If this is set as default, unset other defaults
if (parsed.data.isDefault) {
await db.reportCardTemplate.updateMany({
where: { schoolId, isDefault: true },
data: { isDefault: false },
});
}
const template = await db.reportCardTemplate.create({
data: {
schoolId,
name: parsed.data.name,
description: parsed.data.description,
sections: parsed.data.sections,
gradingScale: parsed.data.gradingScale,
layout: parsed.data.layout,
isDefault: parsed.data.isDefault,
},
});
return NextResponse.json(template, { status: 201 });
} catch (error) {
console.error('ReportCardTemplates POST error:', error);
return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
}
}
@@ -0,0 +1,226 @@
import { NextResponse } from 'next/server';
import { db } from '@/lib/db';
import { getSession } from '@/lib/auth';
// ── PUT: Update seating arrangement (drag-and-drop, randomize, smart arrange) ──
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 userRole = session.user?.role;
if (
userRole !== 'TEACHER' &&
userRole !== 'SCHOOL_ADMIN' &&
userRole !== 'VICE_PRINCIPAL' &&
userRole !== 'SUPER_ADMIN'
) {
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
}
const { id } = await params;
const chart = await db.seatingChart.findFirst({ where: { id } });
if (!chart) {
return NextResponse.json({ error: 'Seating chart not found' }, { status: 404 });
}
// Only the creator or admin can edit
if (userRole === 'TEACHER' && chart.teacherId !== session.userId) {
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
}
const body = await request.json();
const { action, arrangement, studentId, fromRow, fromCol, toRow, toCol } = body;
let currentArrangement: Array<{ studentId: string; row: number; col: number }> = [];
if (chart.arrangement) {
try {
currentArrangement = JSON.parse(chart.arrangement);
} catch { /* ignore */ }
}
if (action === 'move' && studentId) {
// Move a single student from one seat to another
const existingTarget = currentArrangement.find(
(a) => a.row === toRow && a.col === toCol
);
const existingSource = currentArrangement.find(
(a) => a.studentId === studentId
);
if (existingSource) {
if (existingTarget) {
// Swap: move target student to source position
existingTarget.row = existingSource.row;
existingTarget.col = existingSource.col;
}
existingSource.row = toRow;
existingSource.col = toCol;
} else {
// New placement
if (existingTarget) {
// Remove target from their seat (they become unassigned)
currentArrangement = currentArrangement.filter(
(a) => a.studentId !== existingTarget.studentId
);
}
currentArrangement.push({ studentId, row: toRow, col: toCol });
}
} else if (action === 'randomize') {
// Shuffle all assigned students randomly
const assignedStudents = currentArrangement.map((a) => a.studentId);
// Fisher-Yates shuffle
for (let i = assignedStudents.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[assignedStudents[i], assignedStudents[j]] = [assignedStudents[j], assignedStudents[i]];
}
// Reassign to seats
const positions = currentArrangement.map((a) => ({ row: a.row, col: a.col }));
currentArrangement = assignedStudents.map((studentId, i) => ({
studentId,
row: positions[i]?.row ?? Math.floor(i / chart.columns),
col: positions[i]?.col ?? i % chart.columns,
}));
} else if (action === 'smart-arrange') {
// Smart arrange: keep problem students apart based on behavior data
const enrolledStudents = await db.enrollment.findMany({
where: { classGroupId: chart.classGroupId, endDate: null },
include: {
student: { select: { id: true, firstName: true, lastName: true } },
},
});
const studentIds = enrolledStudents.map((e) => e.student.id);
// Get behavior data
const behaviorIncidents = await db.behaviorIncident.findMany({
where: {
studentId: { in: studentIds },
severity: { in: ['major', 'severe'] },
},
select: { studentId: true, severity: true },
});
const behaviorScore = new Map<string, number>();
for (const incident of behaviorIncidents) {
const weight = incident.severity === 'severe' ? 3 : 1;
behaviorScore.set(incident.studentId, (behaviorScore.get(incident.studentId) || 0) + weight);
}
// Sort: high behavior score students first, to place them first
const sortedStudents = [...studentIds].sort((a, b) => {
return (behaviorScore.get(b) || 0) - (behaviorScore.get(a) || 0);
});
// Generate all seat positions
const allPositions: Array<{ row: number; col: number }> = [];
for (let r = 0; r < chart.rows; r++) {
for (let c = 0; c < chart.columns; c++) {
allPositions.push({ row: r, col: c });
}
}
// Place high-behavior students with maximum distance
const newArrangement: Array<{ studentId: string; row: number; col: number }> = [];
const usedPositions = new Set<string>();
for (const sId of sortedStudents) {
let bestPos = allPositions[0];
let bestMinDist = -1;
for (const pos of allPositions) {
const key = `${pos.row}-${pos.col}`;
if (usedPositions.has(key)) continue;
// Calculate minimum distance to already-placed high-behavior students
let minDist = Infinity;
for (const placed of newArrangement) {
const placedScore = behaviorScore.get(placed.studentId) || 0;
if (placedScore >= 2) {
const dist = Math.abs(placed.row - pos.row) + Math.abs(placed.col - pos.col);
minDist = Math.min(minDist, dist);
}
}
if (minDist > bestMinDist) {
bestMinDist = minDist;
bestPos = pos;
}
}
const posKey = `${bestPos.row}-${bestPos.col}`;
usedPositions.add(posKey);
newArrangement.push({ studentId: sId, row: bestPos.row, col: bestPos.col });
}
currentArrangement = newArrangement;
} else if (action === 'assign' && arrangement) {
// Full arrangement replacement
currentArrangement = arrangement;
} else if (action === 'clear') {
// Clear all assignments
currentArrangement = [];
} else if (action === 'assign-all') {
// Assign all unassigned students to empty seats
const enrolledStudents = await db.enrollment.findMany({
where: { classGroupId: chart.classGroupId, endDate: null },
include: {
student: { select: { id: true } },
},
});
const assignedIds = new Set(currentArrangement.map((a) => a.studentId));
const unassigned = enrolledStudents
.map((e) => e.student.id)
.filter((sId) => !assignedIds.has(sId));
// Find empty positions
const usedPositions = new Set(
currentArrangement.map((a) => `${a.row}-${a.col}`)
);
const emptyPositions: Array<{ row: number; col: number }> = [];
for (let r = 0; r < chart.rows; r++) {
for (let c = 0; c < chart.columns; c++) {
if (!usedPositions.has(`${r}-${c}`)) {
emptyPositions.push({ row: r, col: c });
}
}
}
// Assign unassigned students to empty positions
for (let i = 0; i < Math.min(unassigned.length, emptyPositions.length); i++) {
currentArrangement.push({
studentId: unassigned[i],
row: emptyPositions[i].row,
col: emptyPositions[i].col,
});
}
}
// Save the updated arrangement
await db.seatingChart.update({
where: { id },
data: { arrangement: JSON.stringify(currentArrangement) },
});
// Also update ClassGroup's seatingOrder for backward compatibility
await db.classGroup.update({
where: { id: chart.classGroupId },
data: { seatingOrder: JSON.stringify(currentArrangement) },
});
return NextResponse.json({
success: true,
arrangement: currentArrangement,
});
} catch (error) {
console.error('SeatingChart Arrange PUT error:', error);
return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
}
}
+233
View File
@@ -0,0 +1,233 @@
import { NextResponse } from 'next/server';
import { db } from '@/lib/db';
import { getSession } from '@/lib/auth';
// ── GET: Get a single seating chart ──────────────────────────────────────────
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 chart = await db.seatingChart.findFirst({
where: { id },
include: {
classGroup: {
select: {
id: true,
name: true,
gradeLevel: true,
schoolId: true,
},
},
teacher: { select: { id: true, firstName: true, lastName: true } },
},
});
if (!chart) {
return NextResponse.json({ error: 'Seating chart not found' }, { status: 404 });
}
// Access check
if (session.user?.role !== 'SUPER_ADMIN' && session.user?.schoolId !== chart.schoolId) {
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
}
// Get enrolled students for the class
const enrollments = await db.enrollment.findMany({
where: { classGroupId: chart.classGroupId, endDate: null },
include: {
student: {
select: {
id: true,
firstName: true,
lastName: true,
avatarUrl: true,
avatarInitials: true,
},
},
},
});
const students = enrollments.map((e) => e.student);
// Get behavior data for smart arrange
const behaviorIncidents = await db.behaviorIncident.findMany({
where: {
studentId: { in: students.map((s) => s.id) },
severity: { in: ['major', 'severe'] },
},
select: { studentId: true, severity: true },
});
const behaviorMap = new Map<string, number>();
for (const incident of behaviorIncidents) {
const weight = incident.severity === 'severe' ? 3 : 1;
behaviorMap.set(incident.studentId, (behaviorMap.get(incident.studentId) || 0) + weight);
}
return NextResponse.json({
...chart,
arrangement: chart.arrangement ? JSON.parse(chart.arrangement) : [],
students: students.map((s) => ({
...s,
behaviorScore: behaviorMap.get(s.id) || 0,
})),
});
} catch (error) {
console.error('SeatingChart GET error:', error);
return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
}
}
// ── PUT: Update a seating chart ──────────────────────────────────────────
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 userRole = session.user?.role;
if (
userRole !== 'TEACHER' &&
userRole !== 'SCHOOL_ADMIN' &&
userRole !== 'VICE_PRINCIPAL' &&
userRole !== 'SUPER_ADMIN'
) {
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
}
const { id } = await params;
const chart = await db.seatingChart.findFirst({ where: { id } });
if (!chart) {
return NextResponse.json({ error: 'Seating chart not found' }, { status: 404 });
}
// Only the creator or admin can edit
if (
userRole === 'TEACHER' &&
chart.teacherId !== session.userId
) {
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
}
const body = await request.json();
const {
name,
layoutType,
rows,
columns,
gap,
arrangement,
showDoor,
showWindows,
doorPosition,
windowPosition,
isTemplate,
isDefault,
notes,
} = body;
// If isDefault is being set to true, unset any existing default for this class
if (isDefault) {
await db.seatingChart.updateMany({
where: { classGroupId: chart.classGroupId, isDefault: true },
data: { isDefault: false },
});
}
const updated = await db.seatingChart.update({
where: { id },
data: {
...(name !== undefined && { name }),
...(layoutType !== undefined && { layoutType }),
...(rows !== undefined && { rows }),
...(columns !== undefined && { columns }),
...(gap !== undefined && { gap }),
...(arrangement !== undefined && { arrangement: JSON.stringify(arrangement) }),
...(showDoor !== undefined && { showDoor }),
...(showWindows !== undefined && { showWindows }),
...(doorPosition !== undefined && { doorPosition }),
...(windowPosition !== undefined && { windowPosition }),
...(isTemplate !== undefined && { isTemplate }),
...(isDefault !== undefined && { isDefault }),
...(notes !== undefined && { notes }),
},
include: {
classGroup: { select: { id: true, name: true, gradeLevel: true } },
teacher: { select: { id: true, firstName: true, lastName: true } },
},
});
// Also update the ClassGroup's seatingOrder for backward compatibility
if (arrangement && Array.isArray(arrangement)) {
await db.classGroup.update({
where: { id: chart.classGroupId },
data: { seatingOrder: JSON.stringify(arrangement) },
});
}
return NextResponse.json({
...updated,
arrangement: updated.arrangement ? JSON.parse(updated.arrangement) : [],
});
} catch (error) {
console.error('SeatingChart PUT error:', error);
return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
}
}
// ── DELETE: Delete a seating chart ──────────────────────────────────────────
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 userRole = session.user?.role;
if (
userRole !== 'TEACHER' &&
userRole !== 'SCHOOL_ADMIN' &&
userRole !== 'VICE_PRINCIPAL' &&
userRole !== 'SUPER_ADMIN'
) {
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
}
const { id } = await params;
const chart = await db.seatingChart.findFirst({ where: { id } });
if (!chart) {
return NextResponse.json({ error: 'Seating chart not found' }, { status: 404 });
}
// Only the creator or admin can delete
if (
userRole === 'TEACHER' &&
chart.teacherId !== session.userId
) {
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
}
await db.seatingChart.delete({ where: { id } });
return NextResponse.json({ success: true });
} catch (error) {
console.error('SeatingChart DELETE error:', error);
return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
}
}
+187
View File
@@ -0,0 +1,187 @@
import { NextResponse } from 'next/server';
import { db } from '@/lib/db';
import { getSession } from '@/lib/auth';
// ── GET: List seating charts with filters ──────────────────────────────────
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 classGroupId = searchParams.get('classGroupId');
const teacherId = searchParams.get('teacherId');
const isTemplate = searchParams.get('isTemplate');
if (!schoolId) {
return NextResponse.json({ error: 'schoolId is required' }, { status: 400 });
}
const where: Record<string, unknown> = { schoolId };
if (classGroupId) where.classGroupId = classGroupId;
if (teacherId) where.teacherId = teacherId;
if (isTemplate !== null && isTemplate !== undefined) {
where.isTemplate = isTemplate === 'true';
}
// Role-based access
if (session.user?.role === 'STUDENT') {
const student = await db.student.findFirst({
where: { userId: session.userId },
select: { id: true },
});
if (student) {
const enrollments = await db.enrollment.findMany({
where: { studentId: student.id },
select: { classGroupId: true },
});
const classIds = enrollments.map((e) => e.classGroupId);
where.classGroupId = { in: classIds };
}
} 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 enrollments = await db.enrollment.findMany({
where: { studentId: { in: studentIds } },
select: { classGroupId: true },
});
const classIds = enrollments.map((e) => e.classGroupId);
where.classGroupId = { in: classIds };
} else if (session.user?.role === 'TEACHER') {
// Teachers see their own charts by default, or all if classGroupId is specified
if (!teacherId && !classGroupId) {
where.teacherId = session.userId;
}
}
const charts = await db.seatingChart.findMany({
where,
include: {
classGroup: { select: { id: true, name: true, gradeLevel: true } },
teacher: { select: { id: true, firstName: true, lastName: true } },
},
orderBy: { updatedAt: 'desc' },
});
// Parse arrangement JSON for each chart
const result = charts.map((chart) => ({
...chart,
arrangement: chart.arrangement ? JSON.parse(chart.arrangement) : [],
}));
return NextResponse.json({ charts: result });
} catch (error) {
console.error('SeatingCharts GET error:', error);
return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
}
}
// ── POST: Create a seating chart ──────────────────────────────────────────
export async function POST(request: Request) {
try {
const session = await getSession();
if (!session) {
return NextResponse.json({ error: 'Not authenticated' }, { status: 401 });
}
const userRole = session.user?.role;
if (
userRole !== 'TEACHER' &&
userRole !== 'SCHOOL_ADMIN' &&
userRole !== 'VICE_PRINCIPAL' &&
userRole !== 'SUPER_ADMIN'
) {
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
}
const body = await request.json();
const {
name,
classGroupId,
schoolId,
layoutType,
rows,
columns,
gap,
arrangement,
showDoor,
showWindows,
doorPosition,
windowPosition,
isTemplate,
isDefault,
notes,
} = body;
if (!name || !schoolId || !classGroupId) {
return NextResponse.json(
{ error: 'name, schoolId, and classGroupId are required' },
{ status: 400 }
);
}
// Verify class belongs to school
const classGroup = await db.classGroup.findFirst({
where: { id: classGroupId, schoolId, deletedAt: null },
});
if (!classGroup) {
return NextResponse.json({ error: 'Class not found' }, { status: 404 });
}
// If isDefault is true, unset any existing default for this class
if (isDefault) {
await db.seatingChart.updateMany({
where: { classGroupId, isDefault: true },
data: { isDefault: false },
});
}
const chart = await db.seatingChart.create({
data: {
schoolId,
classGroupId,
teacherId: session.userId,
name,
layoutType: layoutType || 'rows',
rows: rows ?? 5,
columns: columns ?? 5,
gap: gap ?? 2,
arrangement: arrangement ? JSON.stringify(arrangement) : null,
showDoor: showDoor ?? true,
showWindows: showWindows ?? true,
doorPosition: doorPosition || 'left',
windowPosition: windowPosition || 'right',
isTemplate: isTemplate ?? false,
isDefault: isDefault ?? false,
notes: notes || null,
},
include: {
classGroup: { select: { id: true, name: true, gradeLevel: true } },
teacher: { select: { id: true, firstName: true, lastName: true } },
},
});
// Also update the ClassGroup's seatingOrder for backward compatibility
if (arrangement && Array.isArray(arrangement)) {
await db.classGroup.update({
where: { id: classGroupId },
data: { seatingOrder: JSON.stringify(arrangement) },
});
}
return NextResponse.json({
...chart,
arrangement: chart.arrangement ? JSON.parse(chart.arrangement) : [],
}, { status: 201 });
} catch (error) {
console.error('SeatingCharts POST error:', error);
return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
}
}
+95
View File
@@ -3212,4 +3212,99 @@
.class-card-active:hover::before {
opacity: 1;
}
/* Report Card View - Badge shimmer animation */
@keyframes shimmer {
0% { transform: translateX(-100%); }
100% { transform: translateX(100%); }
}
/* Report Card View - Print styles */
@media print {
.report-card-print {
box-shadow: none !important;
border: 1px solid #e5e7eb !important;
}
.report-card-print * {
color-adjust: exact;
-webkit-print-color-adjust: exact;
print-color-adjust: exact;
}
.no-print {
display: none !important;
}
}
/* Seating Chart - Desk hover glow */
@keyframes desk-glow {
0%, 100% { box-shadow: 0 0 0 0 rgba(16, 185, 129, 0); }
50% { box-shadow: 0 0 8px 2px rgba(16, 185, 129, 0.3); }
}
/* Seating Chart - Empty desk dashed border animation */
@keyframes dash-march {
to { stroke-dashoffset: -12; }
}
/* Library - Book card hover lift */
.book-card-hover {
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
.book-card-hover:hover {
transform: translateY(-4px) scale(1.01);
box-shadow: 0 12px 24px -8px rgba(16, 185, 129, 0.15), 0 4px 8px -2px rgba(0, 0, 0, 0.06);
}
/* Library - Availability pulse */
@keyframes availability-pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.6; }
}
.availability-pulse {
animation: availability-pulse 2s ease-in-out infinite;
}
/* Library - Overdue gradient border */
.overdue-gradient-border {
position: relative;
}
.overdue-gradient-border::before {
content: '';
position: absolute;
inset: 0;
border-radius: 0.5rem;
padding: 2px;
background: linear-gradient(135deg, #ef4444, #f97316, #ef4444);
mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0);
mask-composite: exclude;
-webkit-mask-composite: xor;
pointer-events: none;
}
/* Parent Portal - Circular progress gradient */
@keyframes progress-fill-animate {
from { stroke-dashoffset: var(--circumference); }
to { stroke-dashoffset: var(--offset); }
}
/* Parent Portal - Tab transition */
.tab-content-enter {
animation: tab-enter 0.3s ease-out;
}
@keyframes tab-enter {
from { opacity: 0; transform: translateY(8px); }
to { opacity: 1; transform: translateY(0); }
}
/* Glassmorphism card style */
.glass-card {
background: rgba(255, 255, 255, 0.7);
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
border: 1px solid rgba(255, 255, 255, 0.3);
}
.dark .glass-card {
background: rgba(24, 24, 27, 0.7);
border: 1px solid rgba(255, 255, 255, 0.06);
}
}
+16
View File
@@ -66,6 +66,8 @@ import {
Tablet,
CalendarClock,
Timer,
LayoutGrid as LayoutGridIcon,
BookOpen as BookOpenIconNav,
} from 'lucide-react';
import {
Sidebar,
@@ -177,6 +179,9 @@ import TabletGradingView from './tablet-grading-view';
import ExamCalendarView from './exam-calendar-view';
import StudentAchievementsView from './student-achievements-view';
import StudentStudyPlannerView from './student-study-planner-view';
import ReportCardView from './report-card-view';
import SchoolLibraryView from './school-library-view';
import SeatingChartView from './seating-chart-view';
// Dynamic imports for heavy components with loading skeletons
const AnalyticsView = dynamic(() => import('./analytics-view'), {
@@ -243,6 +248,9 @@ const navSections: NavSection[] = [
{ key: 'ai-tests', icon: Brain, labelKey: 'ai_tests.title' },
{ key: 'tablet-grading', icon: Tablet, labelKey: 'nav.tablet_grading' },
{ key: 'exam-calendar', icon: CalendarClock, labelKey: 'nav.exam_calendar' },
{ key: 'report-cards', icon: FileText, labelKey: 'nav.report_cards' },
{ key: 'seating-chart', icon: LayoutGridIcon, labelKey: 'nav.seating_chart' },
{ key: 'school-library', icon: LibraryIcon, labelKey: 'nav.school_library' },
{ key: 'notification-center', icon: Bell, labelKey: 'nav.notification_center' },
{ key: 'announcements', icon: Megaphone, labelKey: 'nav.announcements' },
],
@@ -281,9 +289,12 @@ const studentNavSections: NavSection[] = [
{ key: 'ai-tests', icon: Brain, labelKey: 'ai_tests.title' },
{ key: 'tablet-grading', icon: Tablet, labelKey: 'nav.tablet_grading' },
{ key: 'exam-calendar', icon: CalendarClock, labelKey: 'nav.exam_calendar' },
{ key: 'report-cards', icon: FileText, labelKey: 'nav.report_cards' },
{ key: 'seating-chart', icon: LayoutGridIcon, labelKey: 'nav.seating_chart' },
{ key: 'student-achievements', icon: Trophy, labelKey: 'achievements.title' },
{ key: 'student-study-planner', icon: Timer, labelKey: 'study_planner.title' },
{ key: 'resources', icon: FolderOpen, labelKey: 'nav.resources' },
{ key: 'school-library', icon: LibraryIcon, labelKey: 'nav.school_library' },
{ key: 'notification-center', icon: Bell, labelKey: 'nav.notification_center' },
{ key: 'announcements', icon: Megaphone, labelKey: 'nav.announcements' },
{ key: 'settings', icon: Settings, labelKey: 'nav.settings' },
@@ -308,6 +319,8 @@ const parentNavSections: NavSection[] = [
{ key: 'communication', icon: MessageSquare, labelKey: 'nav.communication' },
{ key: 'student-achievements', icon: Trophy, labelKey: 'achievements.title' },
{ key: 'student-study-planner', icon: Timer, labelKey: 'study_planner.title' },
{ key: 'report-cards', icon: FileText, labelKey: 'nav.report_cards' },
{ key: 'school-library', icon: LibraryIcon, labelKey: 'nav.school_library' },
{ key: 'notification-center', icon: Bell, labelKey: 'nav.notification_center' },
{ key: 'announcements', icon: Megaphone, labelKey: 'nav.announcements' },
],
@@ -455,6 +468,9 @@ function renderView(view: ViewName) {
case 'exam-calendar': return <ExamCalendarView />;
case 'student-achievements': return <StudentAchievementsView />;
case 'student-study-planner': return <StudentStudyPlannerView />;
case 'seating-chart': return <SeatingChartView />;
case 'school-library': return <SchoolLibraryView />;
case 'report-cards': return <ReportCardView />;
default: return <DashboardView />;
}
}
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
+694
View File
@@ -4614,6 +4614,72 @@ const de: TranslationDict = {
'parent_portal.days_present': 'Tage anwesend',
'parent_portal.attendance_rate': 'Anwesenheitsrate',
'parent_portal.all_children': 'Alle Kinder',
'parent_portal.academic_progress': 'Akademischer Fortschritt',
'parent_portal.attendance_illness': 'Anwesenheit & Krankheit',
'parent_portal.communication': 'Kommunikation',
'parent_portal.schedule': 'Stundenplan',
'parent_portal.reports_docs': 'Berichte & Dokumente',
'parent_portal.subject_grades': 'Faecher & Noten',
'parent_portal.class_average': 'Klassendurchschnitt',
'parent_portal.trend_up': 'Steigend',
'parent_portal.trend_down': 'Fallend',
'parent_portal.trend_stable': 'Stabil',
'parent_portal.competency_radar': 'Kompetenzradar',
'parent_portal.grade_history': 'Notenverlauf',
'parent_portal.assessment_results': 'Leistungskontrollergebnisse',
'parent_portal.attendance_rate_circular': 'Anwesenheitsrate',
'parent_portal.illness_history': 'Krankheitsverlauf',
'parent_portal.upload_certificate': 'Attest hochladen',
'parent_portal.messages_teachers': 'Nachrichten von Lehrern',
'parent_portal.conversation_rooms': 'Gespraechesraeume',
'parent_portal.school_announcements': 'Schulankuendigungen',
'parent_portal.weekly_timetable': 'Wochenstundenplan',
'parent_portal.exam_dates': 'Pruefungstermine',
'parent_portal.counseling_appointments_short': 'Beratungstermine',
'parent_portal.school_events': 'Schulveranstaltungen',
'parent_portal.homework_due': 'Hausaufgaben faellig',
'parent_portal.report_cards': 'Zeugnisse',
'parent_portal.portfolio_highlights': 'Portfolio-Highlights',
'parent_portal.request_meeting': 'Elterngespraech anfragen',
'parent_portal.reserve_book': 'Buch reservieren',
'parent_portal.view_announcements': 'Ankuendigungen anzeigen',
'parent_portal.child_dashboard': 'Dashboard',
'parent_portal.quick_stats': 'Schnellstatistiken',
'parent_portal.upcoming_deadlines': 'Anstehende Termine',
'parent_portal.recent_activity': 'Letzte Aktivitaet',
'parent_portal.no_announcements': 'Keine Ankuendigungen',
'parent_portal.no_messages': 'Keine Nachrichten',
'parent_portal.no_schedule': 'Kein Stundenplan verfuegbar',
'parent_portal.no_homework': 'Keine Hausaufgaben',
'parent_portal.no_events': 'Keine Veranstaltungen',
'parent_portal.no_portfolio': 'Keine Portfolio-Eintraege',
'parent_portal.no_assessments': 'Keine Leistungskontrollen',
'parent_portal.monday': 'Montag',
'parent_portal.tuesday': 'Dienstag',
'parent_portal.wednesday': 'Mittwoch',
'parent_portal.thursday': 'Donnerstag',
'parent_portal.friday': 'Freitag',
'parent_portal.period': 'Stunde',
'parent_portal.break': 'Pause',
'parent_portal.score': 'Punkte',
'parent_portal.feedback': 'Feedback',
'parent_portal.overall_average': 'Gesamtdurchschnitt',
'parent_portal.subjects_count': 'Faecher',
'parent_portal.latest_grade': 'Aktuellste Note',
'parent_portal.days_excused': 'Tage entschuldigt',
'parent_portal.days_late': 'Tage verspaetet',
'parent_portal.unread_messages': 'Ungelesene Nachrichten',
'parent_portal.active_conversations': 'Aktive Gespraeche',
'parent_portal.pinned': 'Angeheftet',
'parent_portal.urgent': 'Dringend',
'parent_portal.from': 'Von',
'parent_portal.due': 'Faellig',
'parent_portal.view': 'Ansehen',
'parent_portal.download': 'Herunterladen',
'parent_portal.grade_trend': 'Notentrend',
'parent_portal.comparison': 'Vergleich',
'parent_portal.student': 'Schueler/in',
'parent_portal.class': 'Klasse',
// ─── Content Crawl ──────────────────────────────────────────────────
'subjects.crawl': 'Inhalte crawlen',
@@ -5290,6 +5356,287 @@ const de: TranslationDict = {
'achievements.to_next_level': 'bis zur naechsten Stufe',
'achievements.time_remaining': 'Verbleibende Zeit',
'achievements.days': 'Tage',
// Seating Chart
'nav.seating_chart': 'Sitzplan',
'seating.title': 'Sitzplan',
'seating.subtitle': 'Sitzplaene erstellen und bearbeiten',
'seating.select_class': 'Klasse auswählen',
'seating.select_chart': 'Sitzplan auswählen',
'seating.new_chart': 'Neuer Sitzplan',
'seating.default': 'Standard',
'seating.no_chart_title': 'Kein Sitzplan vorhanden',
'seating.no_chart_subtitle': 'Erstellen Sie einen Sitzplan, um die Schülerplätze zu verwalten.',
'seating.create_chart_title': 'Neuen Sitzplan erstellen',
'seating.create_chart_desc': 'Erstellen Sie einen neuen Sitzplan für diese Klasse.',
'seating.chart_name': 'Name des Sitzplans',
'seating.chart_name_placeholder': 'z.B. Sitzplan Q1',
'seating.chart_created': 'Sitzplan erstellt',
'seating.chart_deleted': 'Sitzplan gelöscht',
'seating.delete_chart_title': 'Sitzplan löschen',
'seating.delete_chart_desc': 'Möchten Sie diesen Sitzplan wirklich löschen? Diese Aktion kann nicht rückgängig gemacht werden.',
'seating.unassigned_students': 'Nicht zugewiesene Schüler',
'seating.all_students_assigned': 'Alle Schüler sind zugewiesen.',
'seating.assign_all': 'Alle zuweisen',
'seating.randomize': 'Zufällig mischen',
'seating.smart_arrange': 'Intelligent anordnen',
'seating.clear_all': 'Alle entfernen',
'seating.arrangement_saved': 'Sitzplan gespeichert',
'seating.arrangement_updated': 'Sitzplan aktualisiert',
'seating.teacher_desk': 'Lehrertisch',
'seating.door': 'Tür',
'seating.rows': 'Zeilen',
'seating.columns': 'Spalten',
'seating.layout_type': 'Layout-Typ',
'seating.layout_rows': 'Reihen',
'seating.layout_groups': 'Gruppen',
'seating.layout_ushape': 'U-Form',
'seating.layout_circle': 'Kreis',
'seating.layout_custom': 'Frei',
'seating.settings': 'Einstellungen',
'seating.show_door': 'Tür anzeigen',
'seating.show_windows': 'Fenster anzeigen',
'seating.door_position': 'Türposition',
'seating.window_position': 'Fensterposition',
'seating.pos_left': 'Links',
'seating.pos_right': 'Rechts',
'seating.pos_back': 'Hinten',
'seating.pos_both': 'Beide Seiten',
'seating.assigned_count': '{assigned} von {total} zugewiesen',
'seating.seat_position': 'Reihe {row}, Platz {col}',
'seating.view_profile': 'Profil anzeigen',
'seating.behavior_note': 'Verhaltenshinweis',
'seating.legend_assigned': 'Zugewiesen',
'seating.legend_empty': 'Leer',
'seating.legend_behavior': 'Verhaltenshinweis',
// School Library (DE)
'library.title': 'Schulbibliothek',
'library.subtitle': 'Buecherkatalog, Ausleihe und Reservierung',
'library.loading': 'Wird geladen...',
'library.add_book': 'Buch hinzufuegen',
'library.add_book_desc': 'Neues Buch zur Bibliothek hinzufuegen',
'library.book_added': 'Buch erfolgreich hinzugefuegt',
'library.error_adding_book': 'Fehler beim Hinzufuegen des Buches',
'library.catalog': 'Katalog',
'library.checkouts': 'Ausleihen',
'library.overdue_tab': 'Ueberfaellig',
'library.reservations': 'Reservierungen',
'library.statistics': 'Statistiken',
'library.search_placeholder': 'Titel, Autor, ISBN oder Kategorie suchen...',
'library.category': 'Kategorie',
'library.availability': 'Verfuegbarkeit',
'library.reading_level': 'Lesestufe',
'library.all_categories': 'Alle Kategorien',
'library.all': 'Alle',
'library.available': 'Verfuegbar',
'library.checked_out': 'Ausgeliehen',
'library.unavailable': 'Nicht verfuegbar',
'library.all_levels': 'Alle Stufen',
'library.level_elementary': 'Grundschule',
'library.level_middle': 'Mittelstufe',
'library.level_high': 'Oberstufe',
'library.level_advanced': 'Fortgeschritten',
'library.copies_available': 'Exemplare verfuegbar',
'library.no_books': 'Keine Buecher gefunden',
'library.add_first_book': 'Erstes Buch hinzufuegen',
'library.checkout': 'Ausleihen',
'library.reserve': 'Reservieren',
'library.active_checkouts': 'Aktive Ausleihen',
'library.new_checkout': 'Neue Ausleihe',
'library.no_active_checkouts': 'Keine aktiven Ausleihen',
'library.return_history': 'Rueckgabeverlauf',
'library.overdue_books': 'Ueberfaellige Buecher',
'library.no_overdue': 'Keine ueberfaelligen Buecher!',
'library.days_overdue': 'Tage ueberfaellig',
'library.fine_estimate': 'Geschaetzte Gebuehr',
'library.return': 'Zurueckgeben',
'library.renew': 'Verlaengern',
'library.renewed': 'verlaengert',
'library.renewal_success': 'Ausleihe erfolgreich verlaengert',
'library.error_renewal': 'Fehler bei der Verlaengerung',
'library.checkout_success': 'Buch erfolgreich ausgeliehen',
'library.error_checkout': 'Fehler bei der Ausleihe',
'library.return_success': 'Buch erfolgreich zurueckgegeben',
'library.error_return': 'Fehler bei der Rueckgabe',
'library.reservation_success': 'Buch erfolgreich reserviert',
'library.error_reservation': 'Fehler bei der Reservierung',
'library.reservation_cancelled': 'Reservierung storniert',
'library.error_cancel_reservation': 'Fehler beim Stornieren',
'library.no_student_selected': 'Kein Schueler ausgewaehlt',
'library.no_reservations': 'Keine Reservierungen',
'library.available_now': 'Jetzt verfuegbar',
'library.queue_position': 'Warteposition',
'library.reserved_on': 'Reserviert am',
'library.cancel': 'Stornieren',
'library.total_books': 'Buecher gesamt',
'library.total_copies': 'Exemplare gesamt',
'library.available_copies': 'Verfuegbar',
'library.checked_out_count': 'Ausgeliehen',
'library.overdue': 'Ueberfaellig',
'library.overdue_rate': 'Ueberfaellig-Quote',
'library.popular_books': 'Beliebte Buecher',
'library.category_distribution': 'Kategorieverteilung',
'library.checkout_trends': 'Ausleihtrends',
'library.no_data': 'Keine Daten verfuegbar',
'library.book_title': 'Titel',
'library.author': 'Autor',
'library.isbn': 'ISBN',
'library.publisher': 'Verlag',
'library.publish_year': 'Erscheinungsjahr',
'library.total_copies_field': 'Anzahl Exemplare',
'library.location': 'Standort',
'library.description': 'Beschreibung',
'library.student': 'Schueler',
'library.due_date': 'Faelligkeitsdatum',
'library.checkout_date': 'Ausleihdatum',
'library.condition': 'Zustand',
'library.condition_excellent': 'Sehr gut',
'library.condition_good': 'Gut',
'library.condition_fair': 'Befriedigend',
'library.condition_poor': 'Schlecht',
'library.notes': 'Notizen',
'library.notes_placeholder': 'Optionale Notizen...',
'library.checkout_book': 'Buch ausleihen',
'library.checkout_desc': 'Buch fuer einen Schueler ausleihen',
'library.select_book': 'Buch auswaehlen',
'library.select_student': 'Schueler auswaehlen',
'library.default_14_days': 'Standard: 14 Tage ab heute',
'library.return_book': 'Buch zurueckgeben',
'library.return_condition': 'Rueckgabezustand',
'library.fine_amount': 'Gebuehrenbetrag',
'library.overdue_fine_note': 'Dieses Buch ist ueberfaellig. Bitte Gebuehr pruefen.',
'library.checkout_receipt': 'Ausleihbeleg',
'library.confirm_delete_book': 'Buch wirklich loeschen?',
'library.book_deleted': 'Buch geloescht',
'library.error_delete_book': 'Fehler beim Loeschen',
'library.currently_checked_out': 'Aktuell ausgeliehen',
'library.reservation_queue': 'Reservierungswarteschlange',
'nav.school_library': 'Bibliothek',
'nav.school-library': 'Bibliothek',
'nav.seating_chart': 'Sitzplan',
'nav.seating-chart': 'Sitzplan',
// Report Card
'nav.report_cards': 'Zeugnisse',
'nav.report-cards': 'Zeugnisse',
'report_card.title': 'Zeugnisgenerator',
'report_card.subtitle': 'Zeugnisse erstellen, bearbeiten und verwalten',
'report_card.total_reports': 'Zeugnisse gesamt',
'report_card.drafts': 'Entwürfe',
'report_card.in_review': 'In Prüfung',
'report_card.published': 'Veröffentlicht',
'report_card.search_placeholder': 'Zeugnisse suchen...',
'report_card.filter_class': 'Klasse',
'report_card.filter_status': 'Status',
'report_card.filter_period': 'Zeitraum',
'report_card.all_classes': 'Alle Klassen',
'report_card.all_statuses': 'Alle Status',
'report_card.all_periods': 'Alle Zeiträume',
'report_card.create': 'Zeugnis erstellen',
'report_card.batch_generate': 'Klassensatz erstellen',
'report_card.manage_templates': 'Vorlagen verwalten',
'report_card.selected': 'ausgewählt',
'report_card.publish': 'Veröffentlichen',
'report_card.archive': 'Archivieren',
'report_card.no_reports': 'Keine Zeugnisse gefunden',
'report_card.create_first': 'Erstes Zeugnis erstellen',
'report_card.more': 'mehr',
'report_card.with_grades': 'Mit Noten',
'report_card.status_draft': 'Entwurf',
'report_card.status_review': 'In Prüfung',
'report_card.status_published': 'Veröffentlicht',
'report_card.status_archived': 'Archiviert',
'report_card.status_final': 'Final',
'report_card.submit_review': 'Zur Prüfung einreichen',
'report_card.finalize': 'Finalisieren',
'report_card.export_pdf': 'PDF exportieren',
'report_card.edit_title': 'Zeugnis bearbeiten',
'report_card.student_info': 'Schülerinformationen',
'report_card.student_name': 'Name',
'report_card.class_name': 'Klasse',
'report_card.school_year': 'Schuljahr',
'report_card.period': 'Zeitraum',
'report_card.date': 'Datum',
'report_card.subject_grades': 'Fachnoten',
'report_card.subject': 'Fach',
'report_card.grade': 'Note',
'report_card.label': 'Bewertung',
'report_card.status_col': 'Status',
'report_card.evaluation': 'Bewertung',
'report_card.competency_sections': 'Kompetenzbereiche',
'report_card.section': 'Bereich',
'report_card.no_sections': 'Keine Kompetenzbereiche vorhanden',
'report_card.teacher_comments': 'Lehrerbemerkungen',
'report_card.comments_placeholder': 'Bemerkungen zum Schüler eingeben...',
'report_card.overall_assessment': 'Gesamtbewertung',
'report_card.assessment_placeholder': 'Gesamtbewertung eingeben...',
'report_card.competency_radar': 'Kompetenzradar',
'report_card.competency_level': 'Kompetenzstufe',
'report_card.attendance_summary': 'Anwesenheit',
'report_card.present': 'Anwesend',
'report_card.absent': 'Fehlend',
'report_card.excused': 'Entschuldigt',
'report_card.late': 'Verspätet',
'report_card.actions': 'Aktionen',
'report_card.preview_title': 'Zeugnisvorschau',
'report_card.report_card_title': 'Zeugnis',
'report_card.competency_overview': 'Kompetenzübersicht',
'report_card.competency_details': 'Kompetenzdetails',
'report_card.teacher_signature': 'Unterschrift Lehrkraft',
'report_card.parent_signature': 'Unterschrift Eltern',
'report_card.grading_scale': 'Notenskala (1-6)',
'report_card.create_title': 'Neues Zeugnis erstellen',
'report_card.create_desc': 'Erstellen Sie ein neues Zeugnis für einen Schüler.',
'report_card.select_class': 'Klasse auswählen',
'report_card.select_student': 'Schüler auswählen',
'report_card.select_year': 'Schuljahr auswählen',
'report_card.template': 'Vorlage',
'report_card.select_template': 'Vorlage auswählen',
'report_card.no_template': 'Keine Vorlage',
'report_card.include_grades': 'Noten einbeziehen',
'report_card.batch_generate_title': 'Klassensatz erstellen',
'report_card.batch_generate_desc': 'Erstellen Sie Zeugnisse für alle Schüler einer Klasse.',
'report_card.generate': 'Erstellen',
'report_card.template_management': 'Vorlagenverwaltung',
'report_card.template_desc': 'Verwalten und erstellen Sie Zeugnisvorlagen.',
'report_card.existing_templates': 'Bestehende Vorlagen',
'report_card.default': 'Standard',
'report_card.template_name': 'Vorlagenname',
'report_card.template_name_placeholder': 'z.B. Standard-Grundschulzeugnis',
'report_card.template_description': 'Beschreibung',
'report_card.template_desc_placeholder': 'Kurze Beschreibung der Vorlage',
'report_card.layout': 'Layout',
'report_card.layout_default': 'Standard',
'report_card.layout_detailed': 'Detailliert',
'report_card.layout_compact': 'Kompakt',
'report_card.set_as_default': 'Als Standard festlegen',
'report_card.create_template': 'Vorlage erstellen',
'report_card.delete_confirm_title': 'Zeugnis löschen',
'report_card.delete_confirm_desc': 'Möchten Sie dieses Zeugnis wirklich löschen? Diese Aktion kann nicht rückgängig gemacht werden.',
'report_card.created': 'Zeugnis erfolgreich erstellt',
'report_card.create_error': 'Fehler beim Erstellen des Zeugnisses',
'report_card.batch_generated': '{count} Zeugnisse erstellt',
'report_card.batch_error': 'Fehler beim Erstellen des Klassensatzes',
'report_card.saved': 'Zeugnis gespeichert',
'report_card.save_error': 'Fehler beim Speichern',
'report_card.status_updated': 'Status aktualisiert',
'report_card.status_error': 'Fehler beim Aktualisieren des Status',
'report_card.deleted': 'Zeugnis gelöscht',
'report_card.delete_error': 'Fehler beim Löschen',
'report_card.bulk_action_success': 'Massenaktion erfolgreich',
'report_card.bulk_action_error': 'Fehler bei der Massenaktion',
'report_card.export_success': 'Export erfolgreich',
'report_card.export_error': 'Fehler beim Export',
'report_card.load_error': 'Fehler beim Laden der Zeugnisse',
'report_card.template_created': 'Vorlage erstellt',
'report_card.template_error': 'Fehler beim Erstellen der Vorlage',
'report_card.section_header': 'Kopfzeile',
'report_card.section_grades': 'Noten',
'report_card.section_competencies': 'Kompetenzen',
'report_card.section_comments': 'Bemerkungen',
'report_card.section_attendance': 'Anwesenheit',
'report_card.section_signature': 'Unterschrift',
};
const en: TranslationDict = {
@@ -9902,6 +10249,72 @@ const en: TranslationDict = {
'parent_portal.days_present': 'Days Present',
'parent_portal.attendance_rate': 'Attendance Rate',
'parent_portal.all_children': 'All Children',
'parent_portal.academic_progress': 'Academic Progress',
'parent_portal.attendance_illness': 'Attendance & Illness',
'parent_portal.communication': 'Communication',
'parent_portal.schedule': 'Schedule',
'parent_portal.reports_docs': 'Reports & Documents',
'parent_portal.subject_grades': 'Subjects & Grades',
'parent_portal.class_average': 'Class Average',
'parent_portal.trend_up': 'Improving',
'parent_portal.trend_down': 'Declining',
'parent_portal.trend_stable': 'Stable',
'parent_portal.competency_radar': 'Competency Radar',
'parent_portal.grade_history': 'Grade History',
'parent_portal.assessment_results': 'Assessment Results',
'parent_portal.attendance_rate_circular': 'Attendance Rate',
'parent_portal.illness_history': 'Illness History',
'parent_portal.upload_certificate': 'Upload Certificate',
'parent_portal.messages_teachers': 'Messages from Teachers',
'parent_portal.conversation_rooms': 'Conversation Rooms',
'parent_portal.school_announcements': 'School Announcements',
'parent_portal.weekly_timetable': 'Weekly Timetable',
'parent_portal.exam_dates': 'Exam Dates',
'parent_portal.counseling_appointments_short': 'Counseling',
'parent_portal.school_events': 'School Events',
'parent_portal.homework_due': 'Homework Due',
'parent_portal.report_cards': 'Report Cards',
'parent_portal.portfolio_highlights': 'Portfolio Highlights',
'parent_portal.request_meeting': 'Request Meeting',
'parent_portal.reserve_book': 'Reserve Book',
'parent_portal.view_announcements': 'View Announcements',
'parent_portal.child_dashboard': 'Dashboard',
'parent_portal.quick_stats': 'Quick Stats',
'parent_portal.upcoming_deadlines': 'Upcoming Deadlines',
'parent_portal.recent_activity': 'Recent Activity',
'parent_portal.no_announcements': 'No announcements',
'parent_portal.no_messages': 'No messages',
'parent_portal.no_schedule': 'No schedule available',
'parent_portal.no_homework': 'No homework',
'parent_portal.no_events': 'No events',
'parent_portal.no_portfolio': 'No portfolio entries',
'parent_portal.no_assessments': 'No assessments',
'parent_portal.monday': 'Monday',
'parent_portal.tuesday': 'Tuesday',
'parent_portal.wednesday': 'Wednesday',
'parent_portal.thursday': 'Thursday',
'parent_portal.friday': 'Friday',
'parent_portal.period': 'Period',
'parent_portal.break': 'Break',
'parent_portal.score': 'Score',
'parent_portal.feedback': 'Feedback',
'parent_portal.overall_average': 'Overall Average',
'parent_portal.subjects_count': 'Subjects',
'parent_portal.latest_grade': 'Latest Grade',
'parent_portal.days_excused': 'Days Excused',
'parent_portal.days_late': 'Days Late',
'parent_portal.unread_messages': 'Unread Messages',
'parent_portal.active_conversations': 'Active Conversations',
'parent_portal.pinned': 'Pinned',
'parent_portal.urgent': 'Urgent',
'parent_portal.from': 'From',
'parent_portal.due': 'Due',
'parent_portal.view': 'View',
'parent_portal.download': 'Download',
'parent_portal.grade_trend': 'Grade Trend',
'parent_portal.comparison': 'Comparison',
'parent_portal.student': 'Student',
'parent_portal.class': 'Class',
// ─── Content Crawl ──────────────────────────────────────────────────
'subjects.crawl': 'Crawl Content',
@@ -10578,6 +10991,287 @@ const en: TranslationDict = {
'achievements.to_next_level': 'to next level',
'achievements.time_remaining': 'Time remaining',
'achievements.days': 'days',
// Seating Chart
'nav.seating_chart': 'Seating Chart',
'seating.title': 'Seating Chart',
'seating.subtitle': 'Create and manage seating arrangements',
'seating.select_class': 'Select class',
'seating.select_chart': 'Select chart',
'seating.new_chart': 'New Chart',
'seating.default': 'Default',
'seating.no_chart_title': 'No seating chart found',
'seating.no_chart_subtitle': 'Create a seating chart to manage student seats.',
'seating.create_chart_title': 'Create New Seating Chart',
'seating.create_chart_desc': 'Create a new seating chart for this class.',
'seating.chart_name': 'Chart Name',
'seating.chart_name_placeholder': 'e.g. Seating Q1',
'seating.chart_created': 'Chart created',
'seating.chart_deleted': 'Chart deleted',
'seating.delete_chart_title': 'Delete Seating Chart',
'seating.delete_chart_desc': 'Are you sure you want to delete this seating chart? This action cannot be undone.',
'seating.unassigned_students': 'Unassigned Students',
'seating.all_students_assigned': 'All students are assigned.',
'seating.assign_all': 'Assign All',
'seating.randomize': 'Randomize',
'seating.smart_arrange': 'Smart Arrange',
'seating.clear_all': 'Clear All',
'seating.arrangement_saved': 'Seating chart saved',
'seating.arrangement_updated': 'Seating arrangement updated',
'seating.teacher_desk': "Teacher's Desk",
'seating.door': 'Door',
'seating.rows': 'Rows',
'seating.columns': 'Columns',
'seating.layout_type': 'Layout Type',
'seating.layout_rows': 'Rows',
'seating.layout_groups': 'Groups',
'seating.layout_ushape': 'U-Shape',
'seating.layout_circle': 'Circle',
'seating.layout_custom': 'Custom',
'seating.settings': 'Settings',
'seating.show_door': 'Show Door',
'seating.show_windows': 'Show Windows',
'seating.door_position': 'Door Position',
'seating.window_position': 'Window Position',
'seating.pos_left': 'Left',
'seating.pos_right': 'Right',
'seating.pos_back': 'Back',
'seating.pos_both': 'Both Sides',
'seating.assigned_count': '{assigned} of {total} assigned',
'seating.seat_position': 'Row {row}, Seat {col}',
'seating.view_profile': 'View Profile',
'seating.behavior_note': 'Behavior note',
'seating.legend_assigned': 'Assigned',
'seating.legend_empty': 'Empty',
'seating.legend_behavior': 'Behavior note',
// School Library (EN)
'library.title': 'School Library',
'library.subtitle': 'Book catalog, checkouts, and reservations',
'library.loading': 'Loading...',
'library.add_book': 'Add Book',
'library.add_book_desc': 'Add a new book to the library',
'library.book_added': 'Book added successfully',
'library.error_adding_book': 'Error adding book',
'library.catalog': 'Catalog',
'library.checkouts': 'Checkouts',
'library.overdue_tab': 'Overdue',
'library.reservations': 'Reservations',
'library.statistics': 'Statistics',
'library.search_placeholder': 'Search by title, author, ISBN, or category...',
'library.category': 'Category',
'library.availability': 'Availability',
'library.reading_level': 'Reading Level',
'library.all_categories': 'All Categories',
'library.all': 'All',
'library.available': 'Available',
'library.checked_out': 'Checked Out',
'library.unavailable': 'Unavailable',
'library.all_levels': 'All Levels',
'library.level_elementary': 'Elementary',
'library.level_middle': 'Middle',
'library.level_high': 'High',
'library.level_advanced': 'Advanced',
'library.copies_available': 'copies available',
'library.no_books': 'No books found',
'library.add_first_book': 'Add your first book',
'library.checkout': 'Checkout',
'library.reserve': 'Reserve',
'library.active_checkouts': 'Active Checkouts',
'library.new_checkout': 'New Checkout',
'library.no_active_checkouts': 'No active checkouts',
'library.return_history': 'Return History',
'library.overdue_books': 'Overdue Books',
'library.no_overdue': 'No overdue books!',
'library.days_overdue': 'days overdue',
'library.fine_estimate': 'Estimated fine',
'library.return': 'Return',
'library.renew': 'Renew',
'library.renewed': 'renewed',
'library.renewal_success': 'Checkout renewed successfully',
'library.error_renewal': 'Error renewing checkout',
'library.checkout_success': 'Book checked out successfully',
'library.error_checkout': 'Error checking out book',
'library.return_success': 'Book returned successfully',
'library.error_return': 'Error returning book',
'library.reservation_success': 'Book reserved successfully',
'library.error_reservation': 'Error reserving book',
'library.reservation_cancelled': 'Reservation cancelled',
'library.error_cancel_reservation': 'Error cancelling reservation',
'library.no_student_selected': 'No student selected',
'library.no_reservations': 'No reservations',
'library.available_now': 'Available now',
'library.queue_position': 'Queue position',
'library.reserved_on': 'Reserved on',
'library.cancel': 'Cancel',
'library.total_books': 'Total Books',
'library.total_copies': 'Total Copies',
'library.available_copies': 'Available',
'library.checked_out_count': 'Checked Out',
'library.overdue': 'Overdue',
'library.overdue_rate': 'Overdue Rate',
'library.popular_books': 'Popular Books',
'library.category_distribution': 'Category Distribution',
'library.checkout_trends': 'Checkout Trends',
'library.no_data': 'No data available',
'library.book_title': 'Title',
'library.author': 'Author',
'library.isbn': 'ISBN',
'library.publisher': 'Publisher',
'library.publish_year': 'Publish Year',
'library.total_copies_field': 'Total Copies',
'library.location': 'Location',
'library.description': 'Description',
'library.student': 'Student',
'library.due_date': 'Due Date',
'library.checkout_date': 'Checkout Date',
'library.condition': 'Condition',
'library.condition_excellent': 'Excellent',
'library.condition_good': 'Good',
'library.condition_fair': 'Fair',
'library.condition_poor': 'Poor',
'library.notes': 'Notes',
'library.notes_placeholder': 'Optional notes...',
'library.checkout_book': 'Checkout Book',
'library.checkout_desc': 'Checkout a book for a student',
'library.select_book': 'Select Book',
'library.select_student': 'Select Student',
'library.default_14_days': 'Default: 14 days from today',
'library.return_book': 'Return Book',
'library.return_condition': 'Return Condition',
'library.fine_amount': 'Fine Amount',
'library.overdue_fine_note': 'This book is overdue. Please check the fine amount.',
'library.checkout_receipt': 'Checkout Receipt',
'library.confirm_delete_book': 'Are you sure you want to delete this book?',
'library.book_deleted': 'Book deleted',
'library.error_delete_book': 'Error deleting book',
'library.currently_checked_out': 'Currently Checked Out',
'library.reservation_queue': 'Reservation Queue',
'nav.school_library': 'Library',
'nav.school-library': 'Library',
'nav.seating_chart': 'Seating Chart',
'nav.seating-chart': 'Seating Chart',
// Report Card
'nav.report_cards': 'Report Cards',
'nav.report-cards': 'Report Cards',
'report_card.title': 'Report Card Generator',
'report_card.subtitle': 'Create, edit, and manage report cards',
'report_card.total_reports': 'Total Reports',
'report_card.drafts': 'Drafts',
'report_card.in_review': 'In Review',
'report_card.published': 'Published',
'report_card.search_placeholder': 'Search report cards...',
'report_card.filter_class': 'Class',
'report_card.filter_status': 'Status',
'report_card.filter_period': 'Period',
'report_card.all_classes': 'All Classes',
'report_card.all_statuses': 'All Statuses',
'report_card.all_periods': 'All Periods',
'report_card.create': 'Create Report Card',
'report_card.batch_generate': 'Batch Generate',
'report_card.manage_templates': 'Manage Templates',
'report_card.selected': 'selected',
'report_card.publish': 'Publish',
'report_card.archive': 'Archive',
'report_card.no_reports': 'No report cards found',
'report_card.create_first': 'Create first report card',
'report_card.more': 'more',
'report_card.with_grades': 'With Grades',
'report_card.status_draft': 'Draft',
'report_card.status_review': 'In Review',
'report_card.status_published': 'Published',
'report_card.status_archived': 'Archived',
'report_card.status_final': 'Final',
'report_card.submit_review': 'Submit for Review',
'report_card.finalize': 'Finalize',
'report_card.export_pdf': 'Export PDF',
'report_card.edit_title': 'Edit Report Card',
'report_card.student_info': 'Student Information',
'report_card.student_name': 'Name',
'report_card.class_name': 'Class',
'report_card.school_year': 'School Year',
'report_card.period': 'Period',
'report_card.date': 'Date',
'report_card.subject_grades': 'Subject Grades',
'report_card.subject': 'Subject',
'report_card.grade': 'Grade',
'report_card.label': 'Label',
'report_card.status_col': 'Status',
'report_card.evaluation': 'Evaluation',
'report_card.competency_sections': 'Competency Sections',
'report_card.section': 'Section',
'report_card.no_sections': 'No competency sections available',
'report_card.teacher_comments': 'Teacher Comments',
'report_card.comments_placeholder': 'Enter comments about the student...',
'report_card.overall_assessment': 'Overall Assessment',
'report_card.assessment_placeholder': 'Enter overall assessment...',
'report_card.competency_radar': 'Competency Radar',
'report_card.competency_level': 'Competency Level',
'report_card.attendance_summary': 'Attendance',
'report_card.present': 'Present',
'report_card.absent': 'Absent',
'report_card.excused': 'Excused',
'report_card.late': 'Late',
'report_card.actions': 'Actions',
'report_card.preview_title': 'Report Card Preview',
'report_card.report_card_title': 'Report Card',
'report_card.competency_overview': 'Competency Overview',
'report_card.competency_details': 'Competency Details',
'report_card.teacher_signature': 'Teacher Signature',
'report_card.parent_signature': 'Parent Signature',
'report_card.grading_scale': 'Grading Scale (1-6)',
'report_card.create_title': 'Create New Report Card',
'report_card.create_desc': 'Create a new report card for a student.',
'report_card.select_class': 'Select class',
'report_card.select_student': 'Select student',
'report_card.select_year': 'Select school year',
'report_card.template': 'Template',
'report_card.select_template': 'Select template',
'report_card.no_template': 'No template',
'report_card.include_grades': 'Include grades',
'report_card.batch_generate_title': 'Batch Generate Report Cards',
'report_card.batch_generate_desc': 'Generate report cards for all students in a class.',
'report_card.generate': 'Generate',
'report_card.template_management': 'Template Management',
'report_card.template_desc': 'Manage and create report card templates.',
'report_card.existing_templates': 'Existing Templates',
'report_card.default': 'Default',
'report_card.template_name': 'Template Name',
'report_card.template_name_placeholder': 'e.g. Standard Elementary Report',
'report_card.template_description': 'Description',
'report_card.template_desc_placeholder': 'Brief description of the template',
'report_card.layout': 'Layout',
'report_card.layout_default': 'Default',
'report_card.layout_detailed': 'Detailed',
'report_card.layout_compact': 'Compact',
'report_card.set_as_default': 'Set as default',
'report_card.create_template': 'Create Template',
'report_card.delete_confirm_title': 'Delete Report Card',
'report_card.delete_confirm_desc': 'Are you sure you want to delete this report card? This action cannot be undone.',
'report_card.created': 'Report card created successfully',
'report_card.create_error': 'Error creating report card',
'report_card.batch_generated': '{count} report cards generated',
'report_card.batch_error': 'Error generating batch report cards',
'report_card.saved': 'Report card saved',
'report_card.save_error': 'Error saving',
'report_card.status_updated': 'Status updated',
'report_card.status_error': 'Error updating status',
'report_card.deleted': 'Report card deleted',
'report_card.delete_error': 'Error deleting',
'report_card.bulk_action_success': 'Bulk action successful',
'report_card.bulk_action_error': 'Error performing bulk action',
'report_card.export_success': 'Export successful',
'report_card.export_error': 'Error exporting',
'report_card.load_error': 'Error loading report cards',
'report_card.template_created': 'Template created',
'report_card.template_error': 'Error creating template',
'report_card.section_header': 'Header',
'report_card.section_grades': 'Grades',
'report_card.section_competencies': 'Competencies',
'report_card.section_comments': 'Comments',
'report_card.section_attendance': 'Attendance',
'report_card.section_signature': 'Signature',
};
const translations: Record<string, TranslationDict> = { de, en };
+4 -1
View File
@@ -44,7 +44,10 @@ export type ViewName =
| 'tablet-grading'
| 'exam-calendar'
| 'student-achievements'
| 'student-study-planner';
| 'student-study-planner'
| 'seating-chart'
| 'school-library'
| 'report-cards';
export interface CurrentUser {
id: string;